ScuttleBot

fix: relay session discovery — match by first-entry timestamp, not modTime When multiple Claude/Codex sessions run in the same CWD, all relays were latching onto the same (most recently modified) session file. Now each relay picks the session whose first entry timestamp is newest (closest to its own startedAt), so each relay tails its own subprocess.

lmata 2026-04-04 06:15 trunk
Commit ae413d9334d5084c7e299791134c4caca165f51ef33592b035b55d6c58b9a720
--- cmd/claude-relay/main.go
+++ cmd/claude-relay/main.go
@@ -341,21 +341,23 @@
341341
sanitized := strings.ReplaceAll(cwd, "/", "-")
342342
sanitized = strings.TrimLeft(sanitized, "-")
343343
return filepath.Join(home, ".claude", "projects", "-"+sanitized), nil
344344
}
345345
346
-// findLatestSessionPath finds the most recently modified .jsonl file in root
347
-// that contains an entry with cwd matching targetCWD and timestamp after since.
346
+// findLatestSessionPath finds the .jsonl file in root whose first entry
347
+// timestamp is closest to (but after) since — this ensures each relay
348
+// latches onto its own subprocess's session rather than whichever file
349
+// happens to be most actively written to.
348350
func findLatestSessionPath(root, targetCWD string, since time.Time) (string, error) {
349351
entries, err := os.ReadDir(root)
350352
if err != nil {
351353
return "", err
352354
}
353355
354356
type candidate struct {
355
- path string
356
- modTime time.Time
357
+ path string
358
+ firstEntry time.Time
357359
}
358360
var candidates []candidate
359361
for _, e := range entries {
360362
if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
361363
continue
@@ -365,36 +367,32 @@
365367
continue
366368
}
367369
if info.ModTime().Before(since) {
368370
continue
369371
}
370
- candidates = append(candidates, candidate{
371
- path: filepath.Join(root, e.Name()),
372
- modTime: info.ModTime(),
373
- })
372
+ p := filepath.Join(root, e.Name())
373
+ if first, ok := sessionFirstEntryTime(p, targetCWD, since); ok {
374
+ candidates = append(candidates, candidate{path: p, firstEntry: first})
375
+ }
374376
}
375377
if len(candidates) == 0 {
376378
return "", errors.New("no session files found")
377379
}
378
- // Sort newest first.
380
+ // Sort by first entry time, newest first — the session that started
381
+ // most recently (closest to our startedAt) is most likely ours.
379382
sort.Slice(candidates, func(i, j int) bool {
380
- return candidates[i].modTime.After(candidates[j].modTime)
383
+ return candidates[i].firstEntry.After(candidates[j].firstEntry)
381384
})
382
- // Return the first file that has an entry matching our cwd.
383
- for _, c := range candidates {
384
- if matchesSession(c.path, targetCWD, since) {
385
- return c.path, nil
386
- }
387
- }
388
- return "", errors.New("no matching session found")
385
+ return candidates[0].path, nil
389386
}
390387
391
-// matchesSession peeks at the first few lines of a JSONL file to verify cwd.
392
-func matchesSession(path, targetCWD string, since time.Time) bool {
388
+// sessionFirstEntryTime reads the first entry in a JSONL session file,
389
+// verifies it matches targetCWD and is after since, and returns its timestamp.
390
+func sessionFirstEntryTime(path, targetCWD string, since time.Time) (time.Time, bool) {
393391
f, err := os.Open(path)
394392
if err != nil {
395
- return false
393
+ return time.Time{}, false
396394
}
397395
defer f.Close()
398396
399397
scanner := bufio.NewScanner(f)
400398
checked := 0
@@ -402,16 +400,25 @@
402400
checked++
403401
var entry claudeSessionEntry
404402
if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
405403
continue
406404
}
407
- if entry.CWD == "" {
408
- continue
405
+ if entry.CWD != "" && entry.CWD != targetCWD {
406
+ return time.Time{}, false
407
+ }
408
+ if entry.Timestamp != "" {
409
+ t, err := time.Parse(time.RFC3339Nano, entry.Timestamp)
410
+ if err == nil && t.After(since) {
411
+ return t, true
412
+ }
413
+ t2, err := time.Parse(time.RFC3339, entry.Timestamp)
414
+ if err == nil && t2.After(since) {
415
+ return t2, true
416
+ }
409417
}
410
- return entry.CWD == targetCWD
411418
}
412
- return false
419
+ return time.Time{}, false
413420
}
414421
415422
func tailSessionFile(ctx context.Context, path string, mirrorReasoning bool, emit func(mirrorLine)) error {
416423
file, err := os.Open(path)
417424
if err != nil {
418425
--- cmd/claude-relay/main.go
+++ cmd/claude-relay/main.go
@@ -341,21 +341,23 @@
341 sanitized := strings.ReplaceAll(cwd, "/", "-")
342 sanitized = strings.TrimLeft(sanitized, "-")
343 return filepath.Join(home, ".claude", "projects", "-"+sanitized), nil
344 }
345
346 // findLatestSessionPath finds the most recently modified .jsonl file in root
347 // that contains an entry with cwd matching targetCWD and timestamp after since.
 
 
348 func findLatestSessionPath(root, targetCWD string, since time.Time) (string, error) {
349 entries, err := os.ReadDir(root)
350 if err != nil {
351 return "", err
352 }
353
354 type candidate struct {
355 path string
356 modTime time.Time
357 }
358 var candidates []candidate
359 for _, e := range entries {
360 if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
361 continue
@@ -365,36 +367,32 @@
365 continue
366 }
367 if info.ModTime().Before(since) {
368 continue
369 }
370 candidates = append(candidates, candidate{
371 path: filepath.Join(root, e.Name()),
372 modTime: info.ModTime(),
373 })
374 }
375 if len(candidates) == 0 {
376 return "", errors.New("no session files found")
377 }
378 // Sort newest first.
 
379 sort.Slice(candidates, func(i, j int) bool {
380 return candidates[i].modTime.After(candidates[j].modTime)
381 })
382 // Return the first file that has an entry matching our cwd.
383 for _, c := range candidates {
384 if matchesSession(c.path, targetCWD, since) {
385 return c.path, nil
386 }
387 }
388 return "", errors.New("no matching session found")
389 }
390
391 // matchesSession peeks at the first few lines of a JSONL file to verify cwd.
392 func matchesSession(path, targetCWD string, since time.Time) bool {
 
393 f, err := os.Open(path)
394 if err != nil {
395 return false
396 }
397 defer f.Close()
398
399 scanner := bufio.NewScanner(f)
400 checked := 0
@@ -402,16 +400,25 @@
402 checked++
403 var entry claudeSessionEntry
404 if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
405 continue
406 }
407 if entry.CWD == "" {
408 continue
 
 
 
 
 
 
 
 
 
 
409 }
410 return entry.CWD == targetCWD
411 }
412 return false
413 }
414
415 func tailSessionFile(ctx context.Context, path string, mirrorReasoning bool, emit func(mirrorLine)) error {
416 file, err := os.Open(path)
417 if err != nil {
418
--- cmd/claude-relay/main.go
+++ cmd/claude-relay/main.go
@@ -341,21 +341,23 @@
341 sanitized := strings.ReplaceAll(cwd, "/", "-")
342 sanitized = strings.TrimLeft(sanitized, "-")
343 return filepath.Join(home, ".claude", "projects", "-"+sanitized), nil
344 }
345
346 // findLatestSessionPath finds the .jsonl file in root whose first entry
347 // timestamp is closest to (but after) since — this ensures each relay
348 // latches onto its own subprocess's session rather than whichever file
349 // happens to be most actively written to.
350 func findLatestSessionPath(root, targetCWD string, since time.Time) (string, error) {
351 entries, err := os.ReadDir(root)
352 if err != nil {
353 return "", err
354 }
355
356 type candidate struct {
357 path string
358 firstEntry time.Time
359 }
360 var candidates []candidate
361 for _, e := range entries {
362 if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
363 continue
@@ -365,36 +367,32 @@
367 continue
368 }
369 if info.ModTime().Before(since) {
370 continue
371 }
372 p := filepath.Join(root, e.Name())
373 if first, ok := sessionFirstEntryTime(p, targetCWD, since); ok {
374 candidates = append(candidates, candidate{path: p, firstEntry: first})
375 }
376 }
377 if len(candidates) == 0 {
378 return "", errors.New("no session files found")
379 }
380 // Sort by first entry time, newest first — the session that started
381 // most recently (closest to our startedAt) is most likely ours.
382 sort.Slice(candidates, func(i, j int) bool {
383 return candidates[i].firstEntry.After(candidates[j].firstEntry)
384 })
385 return candidates[0].path, nil
 
 
 
 
 
 
386 }
387
388 // sessionFirstEntryTime reads the first entry in a JSONL session file,
389 // verifies it matches targetCWD and is after since, and returns its timestamp.
390 func sessionFirstEntryTime(path, targetCWD string, since time.Time) (time.Time, bool) {
391 f, err := os.Open(path)
392 if err != nil {
393 return time.Time{}, false
394 }
395 defer f.Close()
396
397 scanner := bufio.NewScanner(f)
398 checked := 0
@@ -402,16 +400,25 @@
400 checked++
401 var entry claudeSessionEntry
402 if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
403 continue
404 }
405 if entry.CWD != "" && entry.CWD != targetCWD {
406 return time.Time{}, false
407 }
408 if entry.Timestamp != "" {
409 t, err := time.Parse(time.RFC3339Nano, entry.Timestamp)
410 if err == nil && t.After(since) {
411 return t, true
412 }
413 t2, err := time.Parse(time.RFC3339, entry.Timestamp)
414 if err == nil && t2.After(since) {
415 return t2, true
416 }
417 }
 
418 }
419 return time.Time{}, false
420 }
421
422 func tailSessionFile(ctx context.Context, path string, mirrorReasoning bool, emit func(mirrorLine)) error {
423 file, err := os.Open(path)
424 if err != nil {
425
--- cmd/codex-relay/main.go
+++ cmd/codex-relay/main.go
@@ -1135,15 +1135,20 @@
11351135
return "", os.ErrNotExist
11361136
}
11371137
return match, nil
11381138
}
11391139
1140
+// findLatestSessionPath finds the .jsonl file in root whose first entry
1141
+// timestamp is closest to (but after) notBefore — this ensures each relay
1142
+// latches onto its own subprocess's session rather than whichever session
1143
+// happens to have the latest timestamp when multiple sessions share a CWD.
11401144
func findLatestSessionPath(root, target string, notBefore time.Time) (string, error) {
1141
- var (
1142
- bestPath string
1143
- bestTime time.Time
1144
- )
1145
+ type candidate struct {
1146
+ path string
1147
+ ts time.Time
1148
+ }
1149
+ var candidates []candidate
11451150
11461151
err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
11471152
if walkErr != nil || d.IsDir() || !strings.HasSuffix(path, ".jsonl") {
11481153
return nil
11491154
}
@@ -1155,23 +1160,25 @@
11551160
return nil
11561161
}
11571162
if ts.Before(notBefore) {
11581163
return nil
11591164
}
1160
- if bestPath == "" || ts.After(bestTime) {
1161
- bestPath = path
1162
- bestTime = ts
1163
- }
1165
+ candidates = append(candidates, candidate{path: path, ts: ts})
11641166
return nil
11651167
})
11661168
if err != nil {
11671169
return "", err
11681170
}
1169
- if bestPath == "" {
1171
+ if len(candidates) == 0 {
11701172
return "", os.ErrNotExist
11711173
}
1172
- return bestPath, nil
1174
+ // Sort newest first — the session that started most recently
1175
+ // (closest to our relay's startedAt) is most likely ours.
1176
+ sort.Slice(candidates, func(i, j int) bool {
1177
+ return candidates[i].ts.After(candidates[j].ts)
1178
+ })
1179
+ return candidates[0].path, nil
11731180
}
11741181
11751182
func readSessionMeta(path string) (sessionMetaPayload, time.Time, error) {
11761183
file, err := os.Open(path)
11771184
if err != nil {
11781185
--- cmd/codex-relay/main.go
+++ cmd/codex-relay/main.go
@@ -1135,15 +1135,20 @@
1135 return "", os.ErrNotExist
1136 }
1137 return match, nil
1138 }
1139
 
 
 
 
1140 func findLatestSessionPath(root, target string, notBefore time.Time) (string, error) {
1141 var (
1142 bestPath string
1143 bestTime time.Time
1144 )
 
1145
1146 err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
1147 if walkErr != nil || d.IsDir() || !strings.HasSuffix(path, ".jsonl") {
1148 return nil
1149 }
@@ -1155,23 +1160,25 @@
1155 return nil
1156 }
1157 if ts.Before(notBefore) {
1158 return nil
1159 }
1160 if bestPath == "" || ts.After(bestTime) {
1161 bestPath = path
1162 bestTime = ts
1163 }
1164 return nil
1165 })
1166 if err != nil {
1167 return "", err
1168 }
1169 if bestPath == "" {
1170 return "", os.ErrNotExist
1171 }
1172 return bestPath, nil
 
 
 
 
 
1173 }
1174
1175 func readSessionMeta(path string) (sessionMetaPayload, time.Time, error) {
1176 file, err := os.Open(path)
1177 if err != nil {
1178
--- cmd/codex-relay/main.go
+++ cmd/codex-relay/main.go
@@ -1135,15 +1135,20 @@
1135 return "", os.ErrNotExist
1136 }
1137 return match, nil
1138 }
1139
1140 // findLatestSessionPath finds the .jsonl file in root whose first entry
1141 // timestamp is closest to (but after) notBefore — this ensures each relay
1142 // latches onto its own subprocess's session rather than whichever session
1143 // happens to have the latest timestamp when multiple sessions share a CWD.
1144 func findLatestSessionPath(root, target string, notBefore time.Time) (string, error) {
1145 type candidate struct {
1146 path string
1147 ts time.Time
1148 }
1149 var candidates []candidate
1150
1151 err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
1152 if walkErr != nil || d.IsDir() || !strings.HasSuffix(path, ".jsonl") {
1153 return nil
1154 }
@@ -1155,23 +1160,25 @@
1160 return nil
1161 }
1162 if ts.Before(notBefore) {
1163 return nil
1164 }
1165 candidates = append(candidates, candidate{path: path, ts: ts})
 
 
 
1166 return nil
1167 })
1168 if err != nil {
1169 return "", err
1170 }
1171 if len(candidates) == 0 {
1172 return "", os.ErrNotExist
1173 }
1174 // Sort newest first — the session that started most recently
1175 // (closest to our relay's startedAt) is most likely ours.
1176 sort.Slice(candidates, func(i, j int) bool {
1177 return candidates[i].ts.After(candidates[j].ts)
1178 })
1179 return candidates[0].path, nil
1180 }
1181
1182 func readSessionMeta(path string) (sessionMetaPayload, time.Time, error) {
1183 file, err := os.Open(path)
1184 if err != nil {
1185

Keyboard Shortcuts

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