resolveTranscriptPath locates a session's jsonl under ~/.claude/projects. It first tries the deterministic cwd-encoded path (~/.claude/projects/ / .jsonl) — the fast, exact hit for ordinary sessions. If that file is absent it falls back to a glob on the globally-unique session id
(home, cwd, sessionID string)
| 48 | // cwd, so neither work_dir nor the agent's reported cwd reliably encodes |
| 49 | // the path. Since the session id is unique, the glob is unambiguous. |
| 50 | func resolveTranscriptPath(home, cwd, sessionID string) (string, error) { |
| 51 | projects := filepath.Join(home, ".claude", "projects") |
| 52 | det := filepath.Join(projects, EncodeCwd(cwd), sessionID+".jsonl") |
| 53 | if _, err := os.Stat(det); err == nil { |
| 54 | return det, nil |
| 55 | } |
| 56 | matches, _ := filepath.Glob(filepath.Join(projects, "*", sessionID+".jsonl")) |
| 57 | if len(matches) > 0 { |
| 58 | // The same session id can appear under two project dirs (e.g. a |
| 59 | // plain `claude --resume <id>` from a different cwd writes a second |
| 60 | // copy). Prefer the most recently modified — the live transcript, |
| 61 | // not a stale one. |
| 62 | newest, newestMod := matches[0], int64(-1) |
| 63 | for _, m := range matches { |
| 64 | if fi, err := os.Stat(m); err == nil && fi.ModTime().UnixNano() > newestMod { |
| 65 | newest, newestMod = m, fi.ModTime().UnixNano() |
| 66 | } |
| 67 | } |
| 68 | return newest, nil |
| 69 | } |
| 70 | return "", fmt.Errorf( |
| 71 | "claude transcript not found: no %s.jsonl under %s (tried cwd-encoded path %s and a glob on the session id)", |
| 72 | sessionID, projects, det, |
| 73 | ) |
| 74 | } |
| 75 | |
| 76 | // RenderJSONL renders a claude session jsonl byte-stream to w. Exposed |
| 77 | // (not just used by RenderTranscript) so tests can exercise the |