Skip to content

fix: sync resume sessions after CLI modifications#2814

Open
Masalale wants to merge 2 commits into
pingdotgg:mainfrom
Masalale:fix/cli-resume-context-sync
Open

fix: sync resume sessions after CLI modifications#2814
Masalale wants to merge 2 commits into
pingdotgg:mainfrom
Masalale:fix/cli-resume-context-sync

Conversation

@Masalale
Copy link
Copy Markdown

@Masalale Masalale commented May 26, 2026

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

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

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.turnCount could lag behind Claude’s on-disk session log.

On system:init, the adapter now runs detectExternalTurns: it reads ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl, counts user prompt lines (user messages that are not tool_result payloads), and if that count exceeds T3’s stored turnCount, it updates context.session.resumeCursor.turnCount so internal resume metadata matches the JSONL file. Read/parse failures are non-fatal and emit claude.session.sync-failed warnings.

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

  • Adds a detectExternalTurns helper in ClaudeAdapter.ts that reads the Claude session .jsonl file from ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl on session init.
  • Counts user-prompt turns (excluding tool_result entries) and updates resumeCursor.turnCount if the file reports a higher count than the current session state.
  • Logs a claude.session.sync-failed warning if the file cannot be read or parsed.
  • Behavioral Change: system:init message handling now triggers a best-effort external turn sync that may mutate resumeCursor.turnCount before the session begins.

Macroscope summarized 6256100.

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)
Copilot AI review requested due to automatic review settings May 26, 2026 15:55
@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels May 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 detectExternalTurns to 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.

Comment on lines +2067 to +2069
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`;
Comment on lines +2081 to +2083
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
const encoded = "-" + sessionCwd.replace(/^\//, "").replace(/\//g, "-");
const jsonlPath = `${home}/.claude/projects/${encoded}/${sessionId}.jsonl`;
Comment on lines +2089 to +2096
// 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++;
Comment on lines +2085 to +2110
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 };
}),
Comment on lines +2085 to +2110
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 };
}),
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 26, 2026

Approvability

Verdict: Needs human review

Unresolved review comments identify a potential path traversal vulnerability where sessionId is interpolated into a filesystem path without validation. Security concerns and new file-reading behavior warrant human review.

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c0fc71f6-450a-4c53-9939-b749ede15a94

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants