Repository navigation
fix(server): Claude threads recover when a restart interrupts the first turn - #968
Merged
Merged
Conversation
Contributor
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
rynfar
force-pushed
the
fix/claude-resume-unsaved-first-turn
branch
from
October 1, 2026 14:03
b15cd28 to
c1b82d8
Compare
rynfar
force-pushed
the
fix/claude-resume-unsaved-first-turn
branch
from
October 1, 2026 19:02
c1b82d8 to
92bb400
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
If the server stopped during a thread's first Claude turn, before Claude Code wrote the transcript, the thread broke for good. Pylon generates the Claude session id and persists the resume cursor before the prompt reaches Claude. That includes the turn's prompt id, so the cursor already reads
turnCount: 1. On the next start Pylon resumed an id Claude never saved, Claude replied "No conversation found with session ID", and every later message failed with a genericturn/setPermissionMode failed. Self-update continuation hit the same path. This came from T3 and is still unfixed there.Fix
When the check runs (
ClaudeAdapter.startSessionInternal): only an observed assistant message (resumeSessionAt) proves Claude saved the session. Turn counters are written before the prompt is sent. The check runs for a cursor Pylon wrote (it has aturnCount) with a resume id and noresumeSessionAt, outside exact-history fork starts. Imported sessions, whose cursors have no turn count, are never checked.How absence is proven: the session is treated as unsaved only when both of these find nothing, within 10s:
getSessionMessagescalled withoutdir, which the pinned 0.3.278 SDK documents and implements as a search of every project, matching how Claude Code's resume looks a session up;<CLAUDE_CONFIG_DIR>/projects/*/<id>.jsonl(claudeTranscriptExists). Unlike the SDK, which treats unreadable directories as empty, the scan fails when a directory can't be read.Any failure or timeout resumes as before, so a moved workspace or an imported session keeps its context. The scan is capped at 10,000 project directories; a larger tree counts as "unknown" and resumes.
Turn count is only a filter: the
turnCountgate keeps freshly imported cursors out of the check. A cursor the adapter has since rewritten also has a turn count, so the count is not proof of Pylon origin; the transcript scan is what keeps a saved session resuming.Unsaved session: Claude starts a new session with a fresh
sessionIdand noresume. The recorded turn ids and count carry over, so Pylon's turn count and rollback boundaries still line up. ProviderService persists the new cursor, andProviderSession.conversationReset(optional, in contracts) reports the reset.Self-update continuation (
serverRuntimeStartup): onconversationResetno continuation turn is sent, since "Continue where you left off." into an empty session has no context. The recovered incarnation is bound, and only the projection settles, with "The server restarted before this conversation was saved, so its context was lost. Send your message again to continue."Directory writes after recovery (
patchOwnedBinding): marker clears and failure settles no longer upsert a full, possibly stale binding.activeTurnId), through the directory's merging upsert. The ownership check runs again as acommitGuardinside the directory's mutation permit. A user turn that advanced the cursor meanwhile, or a replacement session's binding, is never overwritten.Error mapping:
toSessionErrormaps "No conversation found" toProviderAdapterSessionNotFoundError.Verification
ClaudeAdapter.test.ts:sendTurn, followed by stop and a restart with no saved transcript, starts fresh with carried turn ids, and the nextsendTurnsucceeds.dir).turnCount: 0unsaved, saved history, a read that throws, and the error mapping.claudeNativeHistoryFile.test.ts: the scan finds a transcript in any project, answers no when projects are missing or hold nothing, and fails when the directory listing fails.serverRuntimeStartup.reconcile.test.ts: the fakestartSessionnow writes the fresh cursor and incarnation into the directory. WithconversationReset, no continuation is sent, the error settle keeps the recovered incarnation, and the fresh cursor and incarnation survive settlement with the markers cleared.serverRuntimeStartup.reconcile.test.ts, new this round: the test fakes now model the directory's merge andcommitGuardsemantics.ProviderSessionDirectory.test.ts: against the real directory, a commit guard can read the binding inside the mutation permit, and a guarded partial patch changes only its fields.92bb4009): those three startup tests fail, and so does the multi-orphan test, which now asserts patch-only writes.ClaudeAdapter,serverRuntimeStartup.reconcile,serverRuntimeStartup,claudeNativeHistoryFileandProviderSessionDirectorytests pass 258/258.~/.claude.vp run -F t3 typecheckpasses, and scopedvp lint/vp fmtare clean.Claude Opus 5.5 in Claude Code