reuse transcript blobs across turn-end checkpoints#984
Conversation
Chunk + zlib-compress the session transcript once per stop hook instead of N times (one per mid-turn checkpoint, doubled on v1+v2 dual-write). Adds a content-hash short-circuit so UpdateCommitted skips all blob work when the stored transcript already matches. Motivated by a 30 s stop-hook report with an 18 MB image-heavy transcript: pure-Go zlib of identical content across 3 checkpoints dominated wall time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 6fd1208c5c6d
Mirrors the v1 UpdateCommitted tests against V2GitStore so the /full/current transcript fast paths are exercised directly instead of only transitively via broader checkpoint tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: b4cdc67eb804
There was a problem hiding this comment.
Pull request overview
This PR improves stop-hook performance by avoiding repeated transcript chunking/zlib work across multiple turn-end checkpoint finalizations, and by short-circuiting transcript rewrites when content is unchanged. It fits into the checkpoint “finalize” flow by reducing redundant blob creation and reuse across both v1 (entire/checkpoints/v1) and v2 (refs/entire/*) storage paths.
Changes:
- Precompute transcript chunk blob hashes + content-hash blob once per turn and reuse across all
UpdateCommittedcalls infinalizeAllTurnCheckpoints. - Add v1/v2 short-circuiting based on stored content-hash files to skip chunking/blob creation when transcript bytes are identical.
- Add tests for precomputed blob reuse and no-op updates (roundtrip + stability checks).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/strategy/manual_commit_hooks.go | Precomputes transcript blobs once and threads them into per-checkpoint UpdateCommittedOptions. |
| cmd/entire/cli/checkpoint/checkpoint.go | Extends UpdateCommittedOptions with PrecomputedBlobs and defines PrecomputedTranscriptBlobs. |
| cmd/entire/cli/checkpoint/committed.go | Adds v1 short-circuiting + uses precomputed chunk/content-hash blobs; introduces PrecomputeTranscriptBlobs. |
| cmd/entire/cli/checkpoint/v2_committed.go | Adds v2 short-circuiting and reuses precomputed chunk/content-hash blobs. |
| cmd/entire/cli/checkpoint/committed_update_test.go | Adds v1 tests around precompute roundtrip and “short-circuit” scenarios. |
| cmd/entire/cli/checkpoint/v2_precompute_test.go | Adds v2 tests around precompute roundtrip and “short-circuit” scenarios. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit da55218. Configure here.
- v2 short-circuit now returns nil instead of splicing the tree and advancing the /full/current ref; prevents a no-op commit per identical re-finalize (matches v1 behavior). - finalize skips PrecomputeTranscriptBlobs entirely when the redacted transcript is empty, avoiding a wasted empty-chunk blob. - Short-circuit tests now count calls through a chunkTranscript hook so they actually prove chunking is skipped; previously they only asserted content-addressed blob hashes are equal, which is trivially true. - Added isUsable() invariant check so a malformed PrecomputedTranscriptBlobs falls through to the fresh path instead of writing a zero-hash entry. - Extracted precomputeTranscriptBlobsForFinalize helper to keep finalizeAllTurnCheckpoints under the maintainability threshold. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: e4aeff0a5613

This should address some of the issues reported with codex hitting hook timeouts, for example #957
Summary
Stop hooks were taking ~30 s on 18 MB image-heavy transcripts (reported against Codex, but the code path is agent-agnostic). Two root causes inside
finalizeAllTurnCheckpoints:replaceTranscript/updateCommittedFullTranscriptunconditionally rewrote all transcript blobs even when the stored content was identical.Changes
PrecomputedTranscriptBlobsnew type onUpdateCommittedOptions: chunk blob hashes + content-hash blob, computed once. Content-addressed hashes are identical for v1 (full.jsonl) and v2 (raw_transcript) paths, so one precompute serves both stores.PrecomputeTranscriptBlobs(ctx, repo, transcript, agentType)constructor incheckpoint/committed.go.finalizeAllTurnCheckpointsnow callsPrecomputeTranscriptBlobsonce above the per-checkpoint loop and threads the result into everyUpdateCommittedOptions. On precompute failure the loop falls back to the old per-checkpoint path (best-effort, logged).replaceTranscript(committed.go): accepts precomputed blobs; short-circuits when existingcontent_hash.txtmatches the new sha256.updateCommittedFullTranscript(v2_committed.go): same short-circuit againstraw_transcript_hash.txt;writeTranscriptBlobs+ newwriteContentHashFromPrecomputeaccept precomputed blobs.Expected effect on a 30 s, 18 MB, 3-checkpoint finalize
compactTranscriptForV2is genuinely per-checkpoint (per-checkpointstartLine), so it stays in the loop — not addressed here.Risk notes
PrecomputeTranscriptBlobsfails, every checkpoint re-chunks and re-blobs as before.ReadSessionContentetc.) — verified byTestUpdateCommitted_PreservesMetadata+ roundtrip tests.Potential next steps
In decreasing ratio order, not shipped in this PR:
lifecycle.gohandleLifecycleTurnEnd runsExtractPrompts+ExtractAllModifiedFiles+CalculateTokenUsage+parseTranscriptForCheckpointUUID— each parses the same bytes independently. Async.Once-guarded shared[]TranscriptLinewould collapse 3–4 parses into 1 without touching public agent interfaces. Estimated ~3–5 s/turn on image-heavy transcripts. Every agent, every turn — not just stop.compact.Compactif we can amortize per-startLine.compactTranscriptForV2is currently per-checkpoint because ofstartLine. Worth a follow-up to check whether the expensive part (the JSONL walk) can run once with all chunks shared, and only the startLine slice differs.ExtractPrompts/parseTranscriptForCheckpointUUID. Both currently re-read the same transcript file from disk. ~0.5–1 s/turn. Subsumed by (1) if it lands..entire/metadata/<sid>/full.jsonlwriters/readers.lifecycle.go:357writes ~18 MB to disk every turn, but finalize re-reads the original agent transcript, not this copy. Some resume/explain flows may still depend on it — scope before deleting.json.RawMessagefor imagecontentblocks. Biggest win for 18 MB base64-heavy transcripts but largest diff; diminishing returns if (1) lands first.Test plan
mise run checkto confirm fmt/lint/test:ci pass on this branch.git stop, confirmentire/checkpoints/v1tree and/full/currenttree both contain the finalized transcript and content hash.Want me to push the branch and open the PR with this as the body?
Note
Medium Risk
Touches checkpoint persistence logic for both v1 and v2, adding new short-circuit paths based on stored content hashes; mistakes could leave transcripts stale or mismatched across refs. Changes are localized and covered by new roundtrip/short-circuit tests plus a fallback-to-old-behavior path if precompute fails.
Overview
Speeds up turn-end checkpoint finalization by precomputing transcript chunk blobs once and reusing their content-addressed hashes across all
UpdateCommittedcalls (and across v1+v2 dual-writes).Adds a
PrecomputedTranscriptBlobsoption andPrecomputeTranscriptBlobshelper, updates v1replaceTranscriptand v2/full/currentupdates to skip re-chunking/re-compressing when the transcript content is unchanged (via existing content-hash files), and threads the precomputed blobs throughfinalizeAllTurnCheckpoints. Includes new tests covering precompute reuse and the content-hash short-circuit behavior for both v1 and v2.Reviewed by Cursor Bugbot for commit da55218. Configure here.