fix(claude-sdk-oauth): trusted restart continuity bindings - #959
Merged
Conversation
Persist only bounded, ledger-verified restart checkpoints and fail closed across malformed state, branch rewrites, compaction, forks, and tree navigation. Strengthen lifecycle coverage and the two-process restart probe so full-history replay cannot pass unnoticed.
Move persisted SDK lineage authority out of importable session JSONL into a strict mode-0600 sidecar. Anchor restoration to the current session, exact marker, committed assistant hash, and fail-closed lifecycle invalidation.
Require persisted identity equality and a matching top-level assistant in the local SDK transcript before resume. Fail closed for config-dir or unreadable transcripts and isolate turn execution into a focused module.
Report the hermetic sidecar identity and branch entry sequence on probe failure without exposing message content, making marker and suffix admission regressions directly actionable.
Document the private sidecar authority, marker and assistant anchoring, transcript verification, and fail-closed lifecycle policy introduced by the security review.
This was referenced Aug 18, 2026
Derive the persisted record from the registry entry and the hashes the branch actually carries instead of the process binding map, which holds the previous turn's state during message_end and only a prefix digest after a restart. Admit the full stop-hook and rule-activation metadata family after an anchor, key records by canonical session path, ignore non-clean commit outcomes, delete orphaned records, and drop pending-fork labels that lost their producers.
Document the branch-state anchoring, the widened metadata allowlist, canonical record keying, the untrusted-JSONL versus trusted-sidecar boundary, and the per-assistant marker growth surfaced by the security review.
Branch-derived and context-derived sent hashes now run the same isTransmittedMessage filter. The branch path previously kept content-less user messages that the context path skips, so a single such message shifted every later index and made a restart report a false divergence.
Replace the Reflect.get loader, its runtime type guard, and the locally redeclared boundary type with plain imports now that verifyRestoredTranscript is exported and SdkBoundary carries getSessionMessages.
sentMessageHashes now applies isTransmittedMessage itself, so a future caller cannot produce a hash list that disagrees with another caller's by omitting the filter. Idempotent for both current callers.
The branch walk is not compaction-aware while admission compares against the compaction-truncated context, so anchoring across a boundary inflated sentCount and flattened every later restart. Decline to anchor instead, and document that residual alongside the content-normalization and transcript-tip residuals from the security review.
The context path converts custom_message and branch_summary entries to user messages and hashes them, while the branch walk skips them. The branch under-counts rather than over-counts, so a restart flattens or re-sends as delta and the cost is a lost cache, not a wrong resume. Record it with the other residuals.
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.
Summary
Headless Claude SDK OAuth continuation (
senpi -p -c) now restores its SDK lineage across separate CLI processes through a trusted private sidecar instead of importable session-file data, so a restart sends only the new turn while tampered, stale, or inherited state fails closed. This is the security-corrected follow-up to #943; it supersedes #954 (whose implementation failed the five-lane review) and #809, and it carries the corrections from this PR's own review round.Changes
Lineage capability leaves the session file (
session-binding-store.ts,session-binding.ts){schemaVersion: 2, marker: true}entryRecords anchor at branch state, not the binding map (
session-registry-wiring.ts)message_end: it holds the previous turn's state while that handler runs, and only a prefix digest right after a restart — reading it anchored a stale sent-stream (duplicate resend after restart) or threw on the restored shape (orphaned marker, silent flatten)cleancommit outcomes never anchor a record; orphaned records are deleted rather than left on diskRestored state is verified before resume (
session-restored-admission.ts,session-reattach.ts,sdk-boundary.ts,session-continuity.ts)verifyRestoredTranscriptreads the local SDK transcript and requires the stored top-level assistant boundary; missing, mismatched, nested-only, throwing, orconfig-dirroots fail closedLifecycle boundaries invalidate durably (
session-registry-wiring.ts)SessionManagerand confirmed non-reproducingStructure (
session-turn-attempt.ts,session-stream.ts) — turn execution extracted; every touched file is under the 250 pure-LOC ceiling (session-stream.ts197).QA & Evidence
npm run check(biome, pinned-deps, ts-imports, shrinkwrap, install-lock, platform-lock,tsc --noEmit, browser-smoke): PASSfooter-data-providerreftable debounce test is timing-flaky under contention and passes 9/9 in isolation)deltaMessages: 1, no flatten, no full-history replay; the probe reports sidecar identity and branch entry sequence on failure[2 items]vs[1]with the filter removed) by me and independently by both reviewersRisks & Residuals
config-dirauth lane disables persisted restart resume (its transcript root cannot be addressed without a process-global env swap); same-lane continuation is unaffectedcontentdiverges between the two hash derivations; it fails closed (cold-seed) and falls in the untrusted-input class aboveverifyRestoredTranscriptrequires the stored assistant boundary to exist in the transcript, not to be its tipRelated Issues
-p -ccache-misses every turn (flatten/registry_miss) — ~2x cost vsclaude -p -coh-my-openagent#6981