Repository navigation
feat(agents): give Claude Code snapshot/v1's changelog - #296
Merged
Merged
Conversation
The shipped Claude Code agent served snapshot save and load but not the changelog, so a run couldn't be rewound to a tool call and resumed there. Where the host's tar is GNU tar, the agent now serves enable-changelog and apply-changelog: - enable writes a capture script and baselines a GNU incremental tar of the capture roots (the workspace, the CLI's state with its transcripts, home, /tmp/data, /usr/local and /etc), then installs the script as the CLI's PostToolUse hook. After each tool call the hook archives that call's writes and uploads them through the namespace grant as <position:06d>.tgz, using the protocol's NamespaceUploader; a reservation file keeps the grant's object and byte limits across hook processes. - apply downloads the increments in order, refuses one that isn't a readable archive, extracts each at /, drops a capture hook restored from the agent that captured, and with resume_conversation binds the target context to the restored transcript under a fresh session. It refuses while a turn runs and onto a context already bound, as a snapshot load does. - The changelog directory is one per port, outside every capture root, and readable by the agent's user alone. Where tar isn't GNU tar (an install into Alpine or busybox), the agent serves the snapshot without the changelog. The module is tar_changelog.py, so it doesn't share a name with the pi agent's changelog module when both load in one interpreter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aude Code's changelog - Capture after a failed tool call too. PostToolUse runs only after one that succeeded, so a failed call took no position and its writes went uncaptured. The capture is now registered for PostToolUseFailure as well, and a restored hook is removed from both events. - Don't promote the incremental-tar state before its archive is stored. A position now reserves and uploads while it holds its turn; when the grant's limits refuse it or the upload fails, the state stays at the last stored archive, so the next position's archive carries the writes. - Drop a capture hook a snapshot load restores, as an apply does. - Rebind a target context that's already bound, as a snapshot load and other agents do, instead of refusing it. - While a changelog is captured, run one turn at a time, so two contexts' tool calls can't interleave in one position sequence. - Refuse a changelog directory that's a link, not a directory, or owned by another user, before writing the grant's config into it or applying from it. - after_apply is required, directory is a Path, and the docstrings are shorter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…laude Code's changelog - Resume the conversation the rewind cut. Each capture records the session of the tool call it archives, which the CLI's post-tool hooks receive, and apply resumes the last applied increment's session. It falls back to the largest restored transcript only when an increment records none. - Before tar, each capture writes its position, that session and the roots removed since the last stored capture into a small metadata file in the CLI's state directory, so the increment carries it. Apply reads it after extracting each increment and replays the removals: only an absolute, normalized path that exists and isn't / or a top-level system directory. The roots present at the last stored capture (the baseline at enable) are promoted with the incremental state, so a removal survives a capture that wasn't stored. - Keep tar's stderr and check its status: exit 1 (a file changed as tar read it) is stored with a warning; 2 or more fails the capture, which stores nothing, keeps the state for the next position and still advances the turn. - Admit turns through one lock: a turn reads whether it's captured, and an uncaptured one is counted, under the lock enable holds while it refuses a running turn (409), enables and turns the capture on. No turn can start uncaptured while a capture begins. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e roots - Each capture's metadata (its position, its tool call's session and the roots removed since the last stored capture) now lives in its own 0700 directory beside the changelog's, outside every capture root, instead of in the CLI's state. The hook writes it there and hands it to tar as one more member, so removals and the session replay with custom roots too, and a tool deleting the CLI's state no longer stops the capture. A metadata write that fails is logged, and that capture goes on without it. - Apply finds the metadata in the listing it already makes, by its basename, so a target with another temp directory or port still reads it. It reads the member to stdout and leaves it out of the extraction, so nothing lands on the target. - Apply logs the number of increments it applied, and the first few tar exited non-zero on, instead of every increment. - The hook's and module's limits and intervals are named constants carrying their unit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e Code changelog metadata Apply excluded every archive member named claude-code-changelog-meta.json, so a user file of that name inside a capture root was skipped on replay. The metadata now lives in a directory named with a reserved suffix (.changelog-meta), and apply treats only that file in such a directory as the capture's metadata, wherever the capturing agent kept it, so any other file of that name is restored like the rest of the layer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
What and why
The shipped Claude Code agent served snapshot/v1's
saveandload, but not its changelog. So a run on it couldn't be rewound to a tool call and resumed there, which the wrapper this agent was ported from could do. This addsenable-changelogandapply-changelog.enable-changeloginstalls a capture hook for bothPostToolUseandPostToolUseFailurein the CLI's user settings.<position:06d>.tgz, the position being the call's zero-based ordinal. It uploads the archive through the namespace grant with the protocol'sNamespaceUploader.enable-changelogrefuses (409) while a turn runs. Once capture is on, turns run one at a time, so two contexts can't interleave in one sequence.apply-changelogdownloads the granted increments in order and refuses one that isn't a readable archive (422)./with--incremental, so deletions replay too, and leaves the capture's metadata out of the extraction.resume_conversation, it binds the target context to the transcript of the last applied tool call's session, under a fresh session id; a target already bound is rebound.loadalso drops restored capture hooks.The module is
tar_changelog.py, notchangelog.py, so it doesn't clash with the pi agent'schangelogmodule when both load in one interpreter.Known limitation: the hook runs before the CLI appends the tool call's result to the transcript. So increment k holds tool call k's writes but not its result, and a resumed conversation is missing the last applied call's result. The wrapper this agent was ported from behaves the same way. Fixing it means archiving at the next tool call, the turn's end or the CLI's exit, which is left for a follow-up.
How it was tested
tst/unit/agents/claude_code/test_changelog.pycovers:tst/unit/agentspasses on Linux with GNU tar 1.35: 293 passed, 0 skipped.ruff checkis clean.main(with fix(agents): open the 1M window for native-1M Claude models passed without [1m] #294) plus this branch, with the protocol pinned as a release pins it.A2A validator: every check matches the agent this replaces, including the changelog's capture, apply and marker-read round trip.
Rewind to a tool call onto a fresh agent:
The wrapper failed the last three: it brought back the third call's file, kept the removed root, and resumed the wrong conversation.
Chaos battery against the agent it replaces, identical faults:
🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; the previous finding is fixed and no new actionable issues were found.Summary
Adds Claude Code changelog capture and replay through
snapshot/v1.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Tool[Tool call succeeds or fails] --> Hook[Capture hook] Hook --> Tar[GNU incremental tar] Tar --> Grant[Upload through namespace grant] Grant --> Apply[Apply granted increments] Apply --> Files[Restore writes and deletions] Apply --> Resume[Optionally bind restored conversation]Reviews (4) · Last reviewed commit: "fix(agents): only treat the capture's re..." · Reviewed by Greptile