fix(claude-sdk-oauth): persist the continuity binding across restarts - #809
Draft
1vivy wants to merge 2 commits into
Draft
fix(claude-sdk-oauth): persist the continuity binding across restarts#8091vivy wants to merge 2 commits into
1vivy wants to merge 2 commits into
Conversation
session-binding.ts has existed since db8e8cf but was never wired: nothing called latestBindingOnBranch() and nothing wrote a claude-sdk-oauth-binding entry, so its unit test was the only consumer. The sole surviving binding store was the process-memory Map in session-reattach.ts. After a restart both getSession() and getBinding() miss, decideNativeContinuity returns bootstrap, and the lane flattens. session-stream.ts synthesizes the reason registry_miss because bootstrap carries none, and session-observability reports it as flatten once firstTurn is false. A 697-message session re-sent 68.0KB on the first turn after every restart. Write the checkpoint at the message_end commit boundary, lift it off the branch at session_start, and rebuild the binding in createResidentAttempt only once the current prefix digest matches the recorded one and account, model, systemPromptHash and toolsetHash all agree. Every unproven case falls through to the existing cold path. fixes code-yeongyu#808
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.
Fixes #808.
What this fixes
session-binding.tshas been in the tree sincedb8e8cfeb("persist continuity bindings forverified restart resume"), and
AGENTS.mddocuments it as "Branch-local binding checkpoint forrestart-time resume verification". It was never wired. Nothing called
latestBindingOnBranch(),nothing called
verifyBindingAgainstTranscript(), and nothing ever wrote aclaude-sdk-oauth-bindingentry — that commit added the module and its unit test, 2 files, no callsites, and the unit test stayed its only consumer.
So the only binding store that runs is the process-memory
Mapinsession-reattach.ts. After arestart both
getSession()andgetBinding()miss,decideNativeContinuityreturnsbootstrap,and the lane flattens.
session-stream.tssynthesizes the reason asregistry_missbecausebootstrapcarries none, andsession-observability.tsreports the kind asflattenoncefirstTurnis false. On a 697-message session that is 68.0KB and ~60K tokens re-sent on the firstturn after every restart.
Approach
session-registry-wiring.tsappends aBindingCheckpointthroughpi.appendEntryatthe
message_endcommit boundary, only after the boundary reports a non-rewritten commit.registerSessionRegistrynow takesPick<ExtensionAPI, "on" | "appendEntry">.session_starthandler lifts the newest checkpoint offctx.sessionManager.getBranch()into a module map. The decision needs the current sent-hashprefix, which only exists once the provider context is built, so the checkpoint is held until
createResidentAttemptcan verify it.rehydrateBindingFromCheckpointrebuilds the binding from the current hashes andonly after
prefixDigest(hashes, sentCount)equals the recordedsentPrefixHash, and only whenaccount, model,
systemPromptHashandtoolsetHashall match. Every refusal falls through totoday's cold path, so the worst case is the behavior you already have.
Resume-first is preserved: this adds a way to reach reattach after a restart, and changes no
existing decision.
Design notes
sentPrefixHashis one sha256 over the prefix; storingthe full hash array would add tens of KB to the transcript on every turn.
identityDrift, which a restarted process cannotrun because the entry is gone.
BindingCheckpointtherefore gained optionalsystemPromptHashand
toolsetHash; absent means unknown, and unknown never rehydrates.claudeConfigDirbecame optional because nothing consumes it.verifyBindingAgainstTranscriptis deliberately left untouched and still unused. Supplyingits
transcriptExists/transcriptHasBoundaryUuidhonestly would mean hard-coding Claude Code'sprivate
projects/<mangled-cwd>/<id>.jsonllayout, and this extension deliberately treats resumefailure as that gate instead (
session-stream.tscatches it intoresume_initialization_failed). I did not want to pass it inventedtrues. Happy to wire it witha real probe if you'd prefer that direction — flagging it as the one open design call.
Verification
test/suite/regressions/808-claude-sdk-oauth-binding-persistence.test.ts— 11 tests: checkpointderivation, branch round-trip, successful rehydrate, six refusal cases (rewritten prefix,
truncated history, changed system prompt, changed toolset, different account, different model),
a pre-identity checkpoint, and single-use consumption. 11 passed.
prefixDigestguard fails exactlyrefuses to rehydrate on a rewritten prefix(1 failed | 10 passed); restoring it returns 11/11.npm run checkpasses (also enforced by the pre-commit hook on this commit).test/claude-sdk-oauth-binding.test.ts,test/claude-sdk-oauth-continuity.test.tsand all oftest/suite/regressions/— 325 passed, 7 failed. Those 7 (theme init, fswatch, models.json hotreload, two model-selector cases, cross-project resume) reproduce identically with these source
changes reverted, so they are pre-existing on
mainand unrelated.Draft because of the open design call above and because I have not run the
senpi-qaCLI channel —the restart path needs a real multi-turn OAuth session, which I can do if you want that evidence
before review.
Summary by cubic
Persist continuity bindings for
claude-sdk-oauthacross restarts so sessions reattach instead of re-sending full history on the first turn. Fixes #808.message_end, read it atsession_start, and rehydrate only if the current prefix digest and config identity (account, model,systemPromptHash,toolsetHash) match.prefixDigest; madeclaudeConfigDiroptional.Written for commit 0bdff32. Summary will update on new commits.