fix(claude-code): capture Stop transcripts as validated snapshots - #2099
Open
MuskanPaliwal wants to merge 3 commits into
Open
fix(claude-code): capture Stop transcripts as validated snapshots#2099MuskanPaliwal wants to merge 3 commits into
MuskanPaliwal wants to merge 3 commits into
Conversation
MuskanPaliwal
marked this pull request as ready for review
August 21, 2026 17:58
Contributor
Author
|
Hi @Soph, this pr is ready for an early review. Could you take a look and let me know if the overall approach makes sense, especially how I'm handling Claude transcript readiness? |
…pt-readiness # Conflicts: # cmd/entire/cli/lifecycle.go
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.
Refs #2091, item 3 only.
Before this change, Claude Code's Stop hook waited for a transcript sentinel that no longer appears, then fell back to a 500 ms quiet window. The wait only validated a mutable path. Lifecycle copied that path later, and provisional-checkpoint finalization opened it once more. Entire could therefore store bytes other than the ones it had treated as ready.
This PR replaces that sequence with one owned, validated snapshot for Claude Stop. Modern Claude payloads usually finish in one polling interval. Older payloads keep the existing quiet-window protection.
The wait did not own the transcript
TranscriptPreparerreturns only an error. It gives the caller no bytes, open handle, identity, size, or file version. A successful wait says that path state A looked ready, but lifecycle can then copy state B and checkpoint finalization can read state C.Shortening the wait alone would reduce latency while leaving that race intact. Valid final JSON would not be enough either. A complete older prefix can be syntactically valid even when the current turn has not reached the file.
Proposed solution
The built-in Claude Code adapter now has a Stop-specific capture operation. It fingerprints the transcript, reads exactly the observed byte range, validates the JSONL, and calculates the transcript position from those bytes. The lifecycle copy, Claude transcript analysis, token calculation, position advancement, and checkpoint finalization all use that snapshot. None of them reopens Claude's live transcript for the same Stop.
For modern payloads, non-empty
last_assistant_messagesupplies current-turn evidence. Capture requires a transcript position successfully measured at TurnStart, searches only after that boundary, reconstructs the latest assistant text, and fails closed if it does not match. A measured position of zero is valid on the first turn. A missing transcript or analyzer failure may also produce zero, but without the measured-position bit it cannot qualify.When the final-response field is missing, null, or empty, capture keeps the 500 ms quiet window used by older Claude Code versions. Missing, stale, incomplete, continuously changing, timed-out, and canceled captures return no snapshot.
Capture timing
A 20-run focused test using production capture timings measured:
The modern path reduces median local capture time by 450 ms, or 90%. These numbers measure deterministic transcript capture, not end-to-end Claude Stop latency.
Why the snapshot belongs in the Claude adapter
TranscriptPreparerreturn bytes everywhereFixtures now model a real first turn
The integration harness has an explicit helper for modern Stop payloads. Every integration call site that sends
last_assistant_messagecreates the transcript before TurnStart and records a real boundary.TestHookRunner_SimulateStopnow writes the empty first-turn transcript before simulating UserPromptSubmit, then sends the modern Stop payload. This keeps the production rule honest instead of exempting synthetic fixtures.Focused tests cover repeated assistant text before the turn boundary, partial final JSON, continued growth, truncation, replacement, complete JSON without a trailing newline, timeout, cancellation, source mutation after capture, finalization ownership, and measured-zero versus unmeasured-zero state. A same-size rewrite is covered when its modification time changes.
What the fingerprint cannot prove
The portable fingerprint uses file identity, size, and modification time. It catches ordinary appends, truncation, replacement, and rewrites, but it cannot prove that a same-size rewrite did not happen when the filesystem also reports the same timestamp. Claude writes transcripts by appending, so this is an acceptable limit here. The tests do not claim stronger detection.
Modern readiness also depends on Claude's
last_assistant_messagematching the assistant text reconstructed after the measured boundary. A mismatch fails closed instead of falling back to timing. Legacy payloads still pay the 500 ms wait and remain a timing heuristic.Verification
Fresh on
e6b57ada44ffb13973eda18b720c1c459731a309:mise run lintpassed with 0 issues.mise run testpassed 9,615 tests with 5 skipped in 78.835s.go test -race -count=1 -tags=integration ./cmd/entire/cli/integration_test -run 'Test(ClaudeStop_|HookRunner_SimulateStop$)'passed in 11.194s.git diff --check upstream/main...HEADpassed.At the current PR head,
go test -json -count=20 -run 'TestCaptureTranscript_(ReturnsOwnedValidatedBytes|LegacyStableUsesQuietWindow)$' ./cmd/entire/cli/agent/claudecodepassed all 40 test executions. The modern path had a 50 ms median and the legacy path had a 500 ms median.Before the commit,
mise run checkpassed in 296.93s. Formatting and lint passed, race-enabled unit and integration tests passed, Vogon passed 56/56, and Roger-Roger passed 4/4. A separate unrestrictedmise run lintimmediately before the existing push also passed with 0 issues.Kept out of this PR
This PR does not include condensation batching, session pruning, full-redaction reuse, subagent transcript atomicity, a repository-wide
TranscriptPreparerreplacement, external-agent capability changes, or a minimum Claude Code version policy.