fix: sync resume sessions after CLI modifications#2814
Conversation
When a Claude Code session is resumed from the CLI (outside T3), new turns are appended to the session .jsonl file but T3's projection state is unaware of them. On the next session init, detect this by comparing T3's turnCount against the actual user-turn count in the JSONL file. If the file has more turns, update the resumeCursor so T3's metadata stays in sync. Claude already has full context via --resume on the next turn; this ensures T3's internal state reflects reality. Related: pingdotgg#2388 (AskUserQuestion resume key by question text)
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a sync step on Claude session init to detect and reconcile externally-added user turns by reading Claude’s local session .jsonl file.
Changes:
- Introduces
detectExternalTurnsto read Claude’s local project/session JSONL and compute turn-count deltas. - Updates the
"init"system message handling to invoke the sync logic before returning.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const sessionId = | ||
| (typeof initMessage.session_id === "string" && initMessage.session_id) || | ||
| context.resumeSessionId; |
| // Claude encodes the project path by prepending "-" and replacing "/" with "-". | ||
| const home = process.env.HOME ?? process.env.USERPROFILE ?? ""; | ||
| const encoded = "-" + sessionCwd.replace(/^\//, "").replace(/\//g, "-"); | ||
| const jsonlPath = `${home}/.claude/projects/${encoded}/${sessionId}.jsonl`; |
| const home = process.env.HOME ?? process.env.USERPROFILE ?? ""; | ||
| const encoded = "-" + sessionCwd.replace(/^\//, "").replace(/\//g, "-"); | ||
| const jsonlPath = `${home}/.claude/projects/${encoded}/${sessionId}.jsonl`; |
| // Count user-originated turns by looking for `"type":"user"` lines. | ||
| // Each user message in the JSONL represents one turn boundary. | ||
| const lines = content.split("\n"); | ||
| let fileTurnCount = 0; | ||
| for (const line of lines) { | ||
| if (line.length === 0) continue; | ||
| if (line.includes('"type":"user"')) { | ||
| fileTurnCount++; |
| yield* Effect.tryPromise(() => | ||
| import("node:fs/promises").then((fs) => fs.readFile(jsonlPath, "utf-8")), | ||
| ).pipe( | ||
| Effect.map((content) => { | ||
| // Count user-originated turns by looking for `"type":"user"` lines. | ||
| // Each user message in the JSONL represents one turn boundary. | ||
| const lines = content.split("\n"); | ||
| let fileTurnCount = 0; | ||
| for (const line of lines) { | ||
| if (line.length === 0) continue; | ||
| if (line.includes('"type":"user"')) { | ||
| fileTurnCount++; | ||
| } | ||
| } | ||
|
|
||
| if (fileTurnCount > t3TurnCount) { | ||
| const delta = fileTurnCount - t3TurnCount; | ||
| context.session.resumeCursor = { | ||
| ...context.session.resumeCursor, | ||
| turnCount: fileTurnCount, | ||
| }; | ||
| return { synced: true, delta }; | ||
| } | ||
|
|
||
| return { synced: false }; | ||
| }), |
| yield* Effect.tryPromise(() => | ||
| import("node:fs/promises").then((fs) => fs.readFile(jsonlPath, "utf-8")), | ||
| ).pipe( | ||
| Effect.map((content) => { | ||
| // Count user-originated turns by looking for `"type":"user"` lines. | ||
| // Each user message in the JSONL represents one turn boundary. | ||
| const lines = content.split("\n"); | ||
| let fileTurnCount = 0; | ||
| for (const line of lines) { | ||
| if (line.length === 0) continue; | ||
| if (line.includes('"type":"user"')) { | ||
| fileTurnCount++; | ||
| } | ||
| } | ||
|
|
||
| if (fileTurnCount > t3TurnCount) { | ||
| const delta = fileTurnCount - t3TurnCount; | ||
| context.session.resumeCursor = { | ||
| ...context.session.resumeCursor, | ||
| turnCount: fileTurnCount, | ||
| }; | ||
| return { synced: true, delta }; | ||
| } | ||
|
|
||
| return { synced: false }; | ||
| }), |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit acd516c. Configure here.
ApprovabilityVerdict: Needs human review Unresolved review comments identify a potential path traversal vulnerability where You can customize Macroscope's approvability policy. Learn more. |
Two issues identified by Cursor Bugbot on pingdotgg#2814: 1. enableTurnId runs before detectExternalTurns on init, calling updateResumeCursor which resets turnCount to context.turns.length (zero at init). The t3TurnCount === 0 guard then causes an early return, making the sync a no-op for all resumed sessions. Fix: remove the guard. The read is cheap and the comparison fileTurnCount > t3TurnCount handles the no-change case correctly. 2. JSONL counting included tool-result entries (type:user with tool_result content blocks) alongside user prompts, inflating the turn count for multi-tool sessions. Fix: exclude lines containing 'tool_result' from the count.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |

When a Claude Code session is resumed from the CLI (outside T3), new turns are appended to the session .jsonl file but T3's projection state is unaware of them. On the next session init, detect this by comparing T3's turnCount against the actual user-turn count in the JSONL file.
If the file has more turns, update the resumeCursor so T3's metadata stays in sync. Claude already has full context via --resume on the next turn; this ensures T3's internal state reflects reality.
Related: #2388 (AskUserQuestion resume key by question text)
What Changed
Why
UI Changes
Checklist
Note
Low Risk
Localized resume-metadata sync on init with best-effort file read; no auth or turn-execution path changes.
Overview
When a Claude Code session is resumed after turns were added outside T3 (e.g. CLI), T3’s
resumeCursor.turnCountcould lag behind Claude’s on-disk session log.On
system:init, the adapter now runsdetectExternalTurns: it reads~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl, counts user prompt lines (user messages that are nottool_resultpayloads), and if that count exceeds T3’s storedturnCount, it updatescontext.session.resumeCursor.turnCountso internal resume metadata matches the JSONL file. Read/parse failures are non-fatal and emitclaude.session.sync-failedwarnings.Reviewed by Cursor Bugbot for commit 6256100. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Sync resume session turn count from Claude's external session file on init
detectExternalTurnshelper in ClaudeAdapter.ts that reads the Claude session.jsonlfile from~/.claude/projects/<encoded-cwd>/<sessionId>.jsonlon session init.tool_resultentries) and updatesresumeCursor.turnCountif the file reports a higher count than the current session state.claude.session.sync-failedwarning if the file cannot be read or parsed.system:initmessage handling now triggers a best-effort external turn sync that may mutateresumeCursor.turnCountbefore the session begins.Macroscope summarized 6256100.