Skip to content

fix(claude-code): capture Stop transcripts as validated snapshots - #2099

Open
MuskanPaliwal wants to merge 9 commits into
entireio:mainfrom
MuskanPaliwal:fix-claude-transcript-readiness
Open

fix(claude-code): capture Stop transcripts as validated snapshots#2099
MuskanPaliwal wants to merge 9 commits into
entireio:mainfrom
MuskanPaliwal:fix-claude-transcript-readiness

Conversation

@MuskanPaliwal

@MuskanPaliwal MuskanPaliwal commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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 provide the final assistant response, so Entire can validate a transcript immediately with no artificial wait. Older payloads retain the 500 ms quiet-window fallback. The lifecycle also keeps checkpoints refreshable across Claude's repeatable Stop events and derives file and token metadata from one transcript parse.

The wait did not own the transcript

TranscriptPreparer returns 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.

Owned, validated Stop snapshots

The built-in Claude Code adapter now has a Stop-specific capture operation. It:

  1. fingerprints the producer-owned transcript by file identity, size, and modification time;
  2. reads exactly the observed byte range;
  3. verifies both the opened handle and current path still identify the observed file version;
  4. validates the JSONL and derives the position from those exact bytes; and
  5. returns an Entire-owned snapshot used by lifecycle, metadata analysis, position advancement, and checkpoint finalization.

None of those consumers reopens Claude's live transcript for the same Stop. If the file changes during capture, the bytes are discarded and capture retries against the new version.

The portable fingerprint catches ordinary appends, truncation, replacement, and rewrites whose modification time changes. It cannot prove that a same-size rewrite did not happen when the filesystem also reports the same timestamp. Claude appends transcript records, so that is an acceptable limit here; the tests do not claim stronger detection.

Modern readiness is evidence-based and immediate

For modern payloads, non-empty last_assistant_message supplies current-turn evidence. Capture requires a transcript position successfully measured at TurnStart, reconstructs the latest assistant text only after that boundary, and requires it to match Claude's supplied final response.

The first readiness check now runs immediately. The previous design waited for the first 50 ms ticker event even when the transcript was already complete. After the immediate attempt, incomplete captures return to the normal 50 ms polling cadence.

A rejected file fingerprint is remembered. Entire does not reread and revalidate the same unchanged 70 MiB snapshot every 50 ms; it waits until file identity, size, or modification time provides new evidence.

Missing, stale, incomplete, continuously changing, mismatched, timed-out, and canceled captures return no snapshot. The maximum wait remains three seconds.

Legacy payloads keep the quiet-window fallback

When last_assistant_message is missing, null, or empty, Entire has no producer evidence that the current final response reached the transcript. It therefore retains the 500 ms quiet window used by older Claude Code versions.

The split is intentional:

Payload Readiness evidence Timing behavior
Modern Stable owned snapshot, valid current-turn JSONL, final response match Immediate first check; 50 ms polling only when not ready
Legacy Stable owned snapshot, structurally valid ending, 500 ms without file changes Always pays the quiet window

Repeatable Claude Stops remain refreshable

A blocking Claude Stop hook can cause the same assistant turn to continue and later emit another Stop without a new UserPromptSubmit event. A valid last_assistant_message proves readiness for one Stop; it does not prove that Claude will never continue afterward.

Claude therefore advertises a repeatable turn-end capability. After each Stop, Entire:

  • finalizes checkpoints using the validated snapshot available at that Stop;
  • retains the original turn boundary and checkpoint IDs;
  • tracks commits made during a continuation; and
  • refreshes the same checkpoints if another Stop arrives.

The next user prompt or SessionEnd seals the turn. stop_hook_active is preserved as protocol metadata but is not treated as proof that a Stop is permanently final.

If the latest checkpoint refresh fails, Entire keeps the last successfully stored transcript and preserves the checkpoint IDs as recovery bookkeeping. Successful session-end refreshes release those IDs; failed ones keep the supporting shadow-branch state protected from cleanup.

File and token analysis share one parse

Claude file extraction and token calculation previously implemented separate capabilities, each of which parsed the same main transcript and subagent transcripts independently.

The optional TranscriptTurnAnalyzer capability now parses the current main turn once and derives both modified files and token usage from those records. Each subagent transcript is likewise parsed once for both results. Other agents retain the existing independent capability fallback.

Capture validation remains a separate, lightweight safety pass. Its question is whether the bytes are safe and complete; turn analysis answers what metadata those validated bytes contain.

Capture and analysis timing

These are medians from three runs on an Apple M3. They measure transcript capture, validation, and transcript-backed metadata analysis, not complete end-to-end Stop-hook latency.

Modern Claude Stop hook

Transcript case Capture + validation File/token analysis Total transcript latency
64 KiB normal turn 0.170 ms 0.249 ms 0.419 ms
4 MiB large turn 8.848 ms 15.966 ms 24.814 ms
70 MiB session, small current turn at tail 13.134 ms 19.308 ms 32.442 ms
70 MiB entirely belonging to current turn 152.057 ms 281.803 ms 433.860 ms

The realistic large-history case is the third row: the transcript file is 70 MiB, but most records belong to earlier turns. The 433.860 ms case deliberately makes all 70 MiB one enormous current turn.

Legacy Claude Stop hook

Transcript case Capture including quiet window Analysis Total
64 KiB 501.098 ms 0.249 ms 501.347 ms
4 MiB 501.653 ms 15.966 ms 517.619 ms
70 MiB session, small tail turn 514.228 ms 19.308 ms 533.536 ms
70 MiB full current turn 514.228 ms 281.803 ms 796.031 ms

Other timing cases:

  • A modern transcript that is already ready has no artificial wait.
  • If writing finishes after Stop begins, latency is the remaining write delay plus approximately 50–100 ms stability detection and processing time.
  • A transcript that never becomes valid, or whose final response never matches, times out after three seconds.
  • Missing or already stale transcripts fail immediately.

The common modern case moves from the previous PR design's unavoidable 50 ms first poll to actual processing time: under 1 ms for a small transcript and about 32 ms for a realistic 70 MiB historical session.

Test coverage

Focused tests cover:

  • immediate modern capture independent of the poll interval;
  • legacy quiet-window behavior and mid-write pauses;
  • repeated assistant text before the current-turn boundary;
  • partial final JSON and complete JSON without a trailing newline;
  • continued growth, truncation, replacement, and same-size rewrites with changed modification time;
  • remembered rejection of an unchanged snapshot;
  • missing, stale, timed-out, and canceled capture;
  • source mutation after capture and finalization ownership;
  • measured-zero versus unmeasured-zero turn positions;
  • repeated Stops reusing one turn boundary;
  • commits and checkpoint refreshes during Stop continuations; and
  • successful versus failed session-end refresh cleanup.

The benchmarks live in cmd/entire/cli/agent/claudecode/transcript_capture_test.go.

Verification

Fresh verification for commit 40fbeb0:

  • Required mise run check passed.
  • Race-enabled unit and integration suite passed.
  • Vogon canary passed 56/56.
  • Roger-Roger canary passed 4/4.
  • Post-commit mise run lint passed with 0 issues before push.
  • git diff --check passed.

The separately reported test totals were:

  • Unit: 9,740 tests, 5 skipped.
  • Integration: 531 tests, 3 skipped.
  • Lint: 0 issues.

Kept out of this PR

This PR does not introduce a repository-wide TranscriptPreparer replacement, change external-agent readiness contracts, enforce a minimum Claude Code version, or claim end-to-end Stop-hook latency from the transcript-only benchmarks.

@MuskanPaliwal
MuskanPaliwal marked this pull request as ready for review August 21, 2026 17:58
@MuskanPaliwal
MuskanPaliwal requested a review from a team as a code owner August 21, 2026 17:58
@MuskanPaliwal

Copy link
Copy Markdown
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?

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant