Rewind the conversation on /undo, staged so a redo can lift it (part 3 of #944) - #1345
Open
KazenDev wants to merge 3 commits into
Open
Rewind the conversation on /undo, staged so a redo can lift it (part 3 of #944)#1345KazenDev wants to merge 3 commits into
KazenDev wants to merge 3 commits into
Conversation
Snapshot capture, restore, and the per-chat journal. No UI yet: the picker, the command registry entry, and the send-message hook come in the follow-up. Three data-loss paths closed, each with a test that fails without it: - a failed `git add` no longer hands back the tree of a stale index - every entry is anchored (`refs/freebuff/undo/<chat>/<hash>`) so the cleanup job's prune cannot collect a snapshot the journal still lists - reverting checks the content, not just the tree: `git checkout` deletes a file whose blob it cannot read, and `ls-tree` still lists it The journal is the root set: refs are released when the entry is dropped, and `sweepAnchors` collects what a deleted chat left behind. Verified: 20 tests pass, the CLI typecheck has no new errors, and the full suite matches its baseline (+20 pass, same 61 pre-existing failures).
…odebuffAI#944) The UI half: the picker, the commands, and the send-path hook. The snapshot engine (part 1) is CodebuffAI#1343 and is not touched here. Two review points from CodebuffAI#944, each with a test that fails without its fix: - The picker warned about the cascade only in a code comment. The files panel now says how many newer turns go with the selected one, and lists the union of the files they touch rather than the selected turn's alone. - recordUndoEntry ran in the finally even when a newer turn had started (an interrupt releases the chain lock first), so its diff picked up that turn's files. A turn now stamps itself when it starts and records only while its stamp is still the newest for that chat. Verified: 16 new tests, the CLI suite at 3069 pass with the same 74 pre-existing failures as the port alone, and a typecheck with no undo errors.
…3 of CodebuffAI#944) /undo reverted files while the model kept its memory of the turn, so the agent would look at the disk, not find the file it believed it had just written, and write it again - the undo undid itself. A turn now records where it started in both memories, and an undo stages a cut at that point: nothing is deleted, the model's history is slice-copied on its way into createRunConfig (the one place a run is built), and /redo lifts the mark. A turn that runs afterwards absorbs the cut and clears it. The picker half - hiding the reverted tail on screen, prefilling the composer, and choosing code-only vs code-and-conversation - is the next PR.
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.
Stacked on #1344 (itself stacked on #1343). The four engine files and the picker in the diff below come from those two and are unchanged here between them; this PR's own change is 6 files, +466/-5.
/undoreverts files, but the model keeps the memory of the turn. So the agent looks at the disk, does not find the file it believes it just wrote, and writes it again — the undo undoes itself. Observed live in the CLI:This PR is the conversation half: an undo can now take the turn out of the model's memory as well as off the disk.
The two memories, and the one door
useChatStore.messagesrunState.sessionState.mainAgentState.messageHistorypreviousRunStatecreateRunConfigis called in exactly one place (cli/src/hooks/use-send-message.ts, thepreviousRunStateargument), so cutting the model's history is onesliceat one call site. The SDK never reads the storedoutputof a previous run (onlysessionState,traceSessionIdandinference), so nothing else needs to move.The cut is staged, not applied
Nothing is deleted when you undo. The journal records a boundary and the history is slice-copied on the way into the run:
/undoputs the files back and setsrewindin the chat'sundo.json—{recordId, historyLength, transcriptIndex}.messageHistory.slice(0, historyLength)./redodeletes that mark, so the conversation comes back for free.That is why a redo cannot lose a message here: there was never a copy to get wrong. It also matches what the tools everyone compares us to do — see the prior art below.
Each turn records where it started, in both memories, captured as its run begins (next to the stamp that already guards the entry against a newer turn):
{historyLength, transcriptIndex}. An entry written before this existed has no anchor, and an undo of it still reverts its files but stages no cut — a guess would be worse than a file-only undo.What each file does
cli/src/state/undo-rewind.ts(new)cli/src/state/undo-store.tsanchoron the record,RewindBoundaryon the journal,getRewindBoundary/setRewindBoundary/clearRewindBoundary,undoToRecord(..., { conversation })stages the cut,redoToRecordlifts itcli/src/hooks/use-send-message.tsrecordUndoEntry, apply the staged cut at the run config, clear it once a run has absorbed itcli/src/app.tsxundo-rewind.test.ts(new, 10) and 6 more inundo-store.test.tsPrior art (this is the shape the field settled on)
/rewindoffers Restore code and conversation / Restore conversation / Restore code, and "the original prompt from the selected message is restored into the input field so you can re-send or edit it." It also keeps checkpoints with the conversation so/rewindstill works after a resume. https://code.claude.com/docs/en/checkpointing/redoto cancel it." https://opencode.ai/v2/docs/snapshots/redoonly ever reverses the cut it directly follows." feat: /rewind to an earlier turn and a reversible /undo gi-dellav/zerostack#151Not in this PR, on purpose
creditsUsedis not rewound. The conversation goes back; the bill does not.Verification
bun test cli/src, with the baseline taken by stashing exactly this PR's files (not from memory):diffempty)+16 tests = exactly the new ones.
bun run typecheckincli/: 10 errors, the exact baseline, 0 of them in these files.Red before green, measured: with the staging switched off (
false && …), 2 of the 6 journal tests fail — an undo takes the turn out of the model conversation too and a redo lifts the staged cut. The other 4 assert absence (no cut when it is not asked for, no cut without an anchor, a newer turn does not lift a staged cut, lifting twice is safe), so they pass either way — the pair is what pins the flag.What these tests do not claim: the user-visible bug is a model behaviour (it prefers to rewrite a file it believes is missing), so no unit test reproduces it. The reproduction is the CLI transcript above; the tests pin the mechanism that removes the model's reason to rewrite.
How to try it