ScuttleBot

fix: handle --resume sessions — extract UUID from args, skip --session-id injection New sessions: relay injects --session-id <uuid> so file name is deterministic. Resumed sessions: relay extracts UUID from --resume/-r/--continue arg and looks for the existing {uuid}.jsonl file directly. Tests: extractResumeID covers --resume, -r, --continue, non-UUID values, missing values, and mixed args.

lmata 2026-04-04 19:11 trunk
Commit 6091f6da05a68fd3423b78ed4e9b943d6704f639c5cf1b8290fcb5e0540175d0
--- cmd/claude-relay/main.go
+++ cmd/claude-relay/main.go
@@ -188,15 +188,21 @@
188188
_ = relay.Close(closeCtx)
189189
}()
190190
}
191191
192192
startedAt := time.Now()
193
- args := make([]string, 0, len(cfg.Args)+2)
194
- args = append(args, "--session-id", cfg.ClaudeSessionID)
195
- args = append(args, cfg.Args...)
196
- fmt.Fprintf(os.Stderr, "claude-relay: session-id %s\n", cfg.ClaudeSessionID)
197
- cmd := exec.Command(cfg.ClaudeBin, args...)
193
+ // If resuming, extract the session ID from --resume arg. Otherwise use
194
+ // our generated UUID via --session-id for new sessions.
195
+ if resumeID := extractResumeID(cfg.Args); resumeID != "" {
196
+ cfg.ClaudeSessionID = resumeID
197
+ fmt.Fprintf(os.Stderr, "claude-relay: resuming session %s\n", resumeID)
198
+ } else {
199
+ // New session — inject --session-id so the file name is deterministic.
200
+ cfg.Args = append([]string{"--session-id", cfg.ClaudeSessionID}, cfg.Args...)
201
+ fmt.Fprintf(os.Stderr, "claude-relay: new session %s\n", cfg.ClaudeSessionID)
202
+ }
203
+ cmd := exec.Command(cfg.ClaudeBin, cfg.Args...)
198204
cmd.Env = append(os.Environ(),
199205
"SCUTTLEBOT_CONFIG_FILE="+cfg.ConfigFile,
200206
"SCUTTLEBOT_URL="+cfg.URL,
201207
"SCUTTLEBOT_TOKEN="+cfg.Token,
202208
"SCUTTLEBOT_CHANNEL="+cfg.Channel,
@@ -339,10 +345,25 @@
339345
return "", fmt.Errorf("session file %s not found after %v", target, defaultDiscoverWait)
340346
case <-ticker.C:
341347
}
342348
}
343349
}
350
+
351
+// extractResumeID finds --resume or -r in args and returns the session UUID
352
+// that follows it. Returns "" if not resuming or if the value isn't a UUID.
353
+func extractResumeID(args []string) string {
354
+ for i := 0; i < len(args)-1; i++ {
355
+ if args[i] == "--resume" || args[i] == "-r" || args[i] == "--continue" {
356
+ val := args[i+1]
357
+ // Must look like a UUID (contains dashes, right length)
358
+ if len(val) >= 32 && strings.Contains(val, "-") {
359
+ return val
360
+ }
361
+ }
362
+ }
363
+ return ""
364
+}
344365
345366
// claudeSessionsRoot returns ~/.claude/projects/<sanitized-cwd>/
346367
func claudeSessionsRoot(cwd string) (string, error) {
347368
home, err := os.UserHomeDir()
348369
if err != nil {
349370
--- cmd/claude-relay/main.go
+++ cmd/claude-relay/main.go
@@ -188,15 +188,21 @@
188 _ = relay.Close(closeCtx)
189 }()
190 }
191
192 startedAt := time.Now()
193 args := make([]string, 0, len(cfg.Args)+2)
194 args = append(args, "--session-id", cfg.ClaudeSessionID)
195 args = append(args, cfg.Args...)
196 fmt.Fprintf(os.Stderr, "claude-relay: session-id %s\n", cfg.ClaudeSessionID)
197 cmd := exec.Command(cfg.ClaudeBin, args...)
 
 
 
 
 
 
198 cmd.Env = append(os.Environ(),
199 "SCUTTLEBOT_CONFIG_FILE="+cfg.ConfigFile,
200 "SCUTTLEBOT_URL="+cfg.URL,
201 "SCUTTLEBOT_TOKEN="+cfg.Token,
202 "SCUTTLEBOT_CHANNEL="+cfg.Channel,
@@ -339,10 +345,25 @@
339 return "", fmt.Errorf("session file %s not found after %v", target, defaultDiscoverWait)
340 case <-ticker.C:
341 }
342 }
343 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
345 // claudeSessionsRoot returns ~/.claude/projects/<sanitized-cwd>/
346 func claudeSessionsRoot(cwd string) (string, error) {
347 home, err := os.UserHomeDir()
348 if err != nil {
349
--- cmd/claude-relay/main.go
+++ cmd/claude-relay/main.go
@@ -188,15 +188,21 @@
188 _ = relay.Close(closeCtx)
189 }()
190 }
191
192 startedAt := time.Now()
193 // If resuming, extract the session ID from --resume arg. Otherwise use
194 // our generated UUID via --session-id for new sessions.
195 if resumeID := extractResumeID(cfg.Args); resumeID != "" {
196 cfg.ClaudeSessionID = resumeID
197 fmt.Fprintf(os.Stderr, "claude-relay: resuming session %s\n", resumeID)
198 } else {
199 // New session — inject --session-id so the file name is deterministic.
200 cfg.Args = append([]string{"--session-id", cfg.ClaudeSessionID}, cfg.Args...)
201 fmt.Fprintf(os.Stderr, "claude-relay: new session %s\n", cfg.ClaudeSessionID)
202 }
203 cmd := exec.Command(cfg.ClaudeBin, cfg.Args...)
204 cmd.Env = append(os.Environ(),
205 "SCUTTLEBOT_CONFIG_FILE="+cfg.ConfigFile,
206 "SCUTTLEBOT_URL="+cfg.URL,
207 "SCUTTLEBOT_TOKEN="+cfg.Token,
208 "SCUTTLEBOT_CHANNEL="+cfg.Channel,
@@ -339,10 +345,25 @@
345 return "", fmt.Errorf("session file %s not found after %v", target, defaultDiscoverWait)
346 case <-ticker.C:
347 }
348 }
349 }
350
351 // extractResumeID finds --resume or -r in args and returns the session UUID
352 // that follows it. Returns "" if not resuming or if the value isn't a UUID.
353 func extractResumeID(args []string) string {
354 for i := 0; i < len(args)-1; i++ {
355 if args[i] == "--resume" || args[i] == "-r" || args[i] == "--continue" {
356 val := args[i+1]
357 // Must look like a UUID (contains dashes, right length)
358 if len(val) >= 32 && strings.Contains(val, "-") {
359 return val
360 }
361 }
362 }
363 return ""
364 }
365
366 // claudeSessionsRoot returns ~/.claude/projects/<sanitized-cwd>/
367 func claudeSessionsRoot(cwd string) (string, error) {
368 home, err := os.UserHomeDir()
369 if err != nil {
370
--- cmd/claude-relay/main_test.go
+++ cmd/claude-relay/main_test.go
@@ -117,10 +117,35 @@
117117
// Verify original slice not mutated
118118
if len(userArgs) != 2 {
119119
t.Errorf("userArgs mutated: len=%d", len(userArgs))
120120
}
121121
}
122
+
123
+func TestExtractResumeID(t *testing.T) {
124
+ tests := []struct {
125
+ name string
126
+ args []string
127
+ want string
128
+ }{
129
+ {"no resume", []string{"--dangerously-skip-permissions"}, ""},
130
+ {"--resume with UUID", []string{"--resume", "740fab38-b4c7-4dfc-a82a-2fe24b48baab"}, "740fab38-b4c7-4dfc-a82a-2fe24b48baab"},
131
+ {"-r with UUID", []string{"-r", "29f0a0bf-b2e8-4eee-bfd8-aabbd90b41fb"}, "29f0a0bf-b2e8-4eee-bfd8-aabbd90b41fb"},
132
+ {"--continue with UUID", []string{"--continue", "21b39df2-c032-4fb4-be1c-0b607a9ee702"}, "21b39df2-c032-4fb4-be1c-0b607a9ee702"},
133
+ {"--resume without value", []string{"--resume"}, ""},
134
+ {"--resume with non-UUID", []string{"--resume", "latest"}, ""},
135
+ {"--resume with short string", []string{"--resume", "abc"}, ""},
136
+ {"mixed args", []string{"--dangerously-skip-permissions", "--resume", "740fab38-b4c7-4dfc-a82a-2fe24b48baab", "--chrome"}, "740fab38-b4c7-4dfc-a82a-2fe24b48baab"},
137
+ }
138
+ for _, tt := range tests {
139
+ t.Run(tt.name, func(t *testing.T) {
140
+ got := extractResumeID(tt.args)
141
+ if got != tt.want {
142
+ t.Errorf("extractResumeID(%v) = %q, want %q", tt.args, got, tt.want)
143
+ }
144
+ })
145
+ }
146
+}
122147
123148
func TestDiscoverSessionPathFindsFile(t *testing.T) {
124149
tmpDir := t.TempDir()
125150
sessionID := uuid.New().String()
126151
127152
--- cmd/claude-relay/main_test.go
+++ cmd/claude-relay/main_test.go
@@ -117,10 +117,35 @@
117 // Verify original slice not mutated
118 if len(userArgs) != 2 {
119 t.Errorf("userArgs mutated: len=%d", len(userArgs))
120 }
121 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
123 func TestDiscoverSessionPathFindsFile(t *testing.T) {
124 tmpDir := t.TempDir()
125 sessionID := uuid.New().String()
126
127
--- cmd/claude-relay/main_test.go
+++ cmd/claude-relay/main_test.go
@@ -117,10 +117,35 @@
117 // Verify original slice not mutated
118 if len(userArgs) != 2 {
119 t.Errorf("userArgs mutated: len=%d", len(userArgs))
120 }
121 }
122
123 func TestExtractResumeID(t *testing.T) {
124 tests := []struct {
125 name string
126 args []string
127 want string
128 }{
129 {"no resume", []string{"--dangerously-skip-permissions"}, ""},
130 {"--resume with UUID", []string{"--resume", "740fab38-b4c7-4dfc-a82a-2fe24b48baab"}, "740fab38-b4c7-4dfc-a82a-2fe24b48baab"},
131 {"-r with UUID", []string{"-r", "29f0a0bf-b2e8-4eee-bfd8-aabbd90b41fb"}, "29f0a0bf-b2e8-4eee-bfd8-aabbd90b41fb"},
132 {"--continue with UUID", []string{"--continue", "21b39df2-c032-4fb4-be1c-0b607a9ee702"}, "21b39df2-c032-4fb4-be1c-0b607a9ee702"},
133 {"--resume without value", []string{"--resume"}, ""},
134 {"--resume with non-UUID", []string{"--resume", "latest"}, ""},
135 {"--resume with short string", []string{"--resume", "abc"}, ""},
136 {"mixed args", []string{"--dangerously-skip-permissions", "--resume", "740fab38-b4c7-4dfc-a82a-2fe24b48baab", "--chrome"}, "740fab38-b4c7-4dfc-a82a-2fe24b48baab"},
137 }
138 for _, tt := range tests {
139 t.Run(tt.name, func(t *testing.T) {
140 got := extractResumeID(tt.args)
141 if got != tt.want {
142 t.Errorf("extractResumeID(%v) = %q, want %q", tt.args, got, tt.want)
143 }
144 })
145 }
146 }
147
148 func TestDiscoverSessionPathFindsFile(t *testing.T) {
149 tmpDir := t.TempDir()
150 sessionID := uuid.New().String()
151
152

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button