Repository navigation
feat: go back a turn — take back messages on any backend, optionally restoring the files (#355) - #362
Merged
Merged
Conversation
This was referenced Oct 10, 2026
…restoring the files Closes #355. Builds on the turn ids and workspace snapshots of #354. - session.rewind takes back a turn and every turn after it. Backend- agnostic: the cut happens in codeoid's canonical history and the backend is reset and re-seeded from what's kept (the backend-switch path), so every current and future provider forgets the removed turns, restart or not. A running turn, a send preparing one, and background agents are stopped first; rewinds serialize with sends. - restoreFiles puts the working directory back to that turn's starting snapshot (restore changed/deleted files, remove created ones; ignored and secret files untouched; every path checked to stay inside the workdir). An end-of-turn snapshot tells hand edits from the agent's: files edited by hand since the agent's last turn are reported as conflicts and only overwritten with force. dryRun previews everything and changes nothing. - The result lists what can't be undone (external MCP calls, shell commands with effects outside the workdir) and returns the taken-back prompt. - A durable marker in the transcript hides the taken-back rows on replay, restart, paging and forks alike; every attached client gets a fresh snapshot view. - Web: "go back to here" on your messages, an undo control, and a dialog that previews first; the prompt returns to the composer. TUI and `codeoid attach`: /undo [files [yes|force]]. CLI: codeoid undo. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d edits, the agent truly forgets Audit round 1 on #355: Files - Restore is one git operation: a two-tree `read-tree -m -u` over a throwaway index. Git refuses (changing nothing) if any file changed since the plan, never writes or unlinks through a symlinked parent, never overwrites an untracked file; nested repos are no longer fatal. - A file the snapshot didn't hold because it was ignored under that snapshot's own .gitignore is never deleted; a path now taken by an ignored file is left alone. - Hand edits are checked in every gap between the removed turns (one's end snapshot → the next's start) and after the last; a missing snapshot fails closed (every change counts, `unverified`). - The backend is torn down before files are restored; snapshot, preview and restore share one queue, so a preview can't make a snapshot skip. - planId binds a confirm to its preview: anything changed → refused. The agent forgets - Every warm provider clears its queued history seed on reset — a second undo could replay the turns just taken back. - Queued background reports and the wake timer are dropped, and nothing may wake the agent mid-rewind. - A restart before the next message re-seeds the kept history (backingFresh in the session meta) instead of resuming an empty backend. Safety and accuracy - Restoring files needs session:approve; stopping a busy session needs session:interrupt. waiting_approval counts as busy; sends queued after the rewind are no longer cancelled by it. - `codeoid attach` /undo pre-fills only a single sanitized line (readline submitted every embedded line); /undo files yes confirms the previewed plan; a refreshed view no longer reprints the whole session. - Shell commands are recognized under every backend's tool name; secret- looking values are redacted; only codeoid's own markers are honoured; refusals and failures are audited with force and counts. - Web: confirms the previewed plan, re-previews if it changed, puts the prompt above a half-typed draft. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d edits, survives restarts Audit round 2 on #355: - Interrupt rights are checked inside the serialized rewind (the session can get busy while the request queues), an unknown turn stops nothing, and an idle session is planned and refused BEFORE anything is torn down — a refusal really is "nothing changed". - The rewind claims its stop point synchronously: messages sent after it are no longer cancelled by its interrupt. - A path the restore would recreate never lands on an untracked file at a parent (a file where a deleted directory comes back); exact-name checks for case-insensitive filesystems; paths under a symlinked directory are left alone and reported as `skipped`. - After a restart the real end-of-turn snapshot is kept: restored turns count as ended, and an end snapshot is never overwritten. - A preview of a running session treats the live files as that turn's end, so confirming it no longer always fails the first time. - A preview needs attach/watch; the TUI pre-fill is stripped of control sequences; redaction covers Basic auth, cookies, URL credentials, -u/-p arguments, secret-named variables and bare JWTs (bounded input). - Restore reuses the shadow index's stat cache (no full re-hash); planning makes no per-file git calls; ignore files are read in one cat-file batch; end snapshots no longer halve how far back files can go. - A background turn restores an empty prompt, not the harness text; the CLI tells a chunked first replay from a refresh. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…doing it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
saucam
force-pushed
the
feat/rewind-355
branch
from
October 10, 2026 17:30
1257ca4 to
b66bd9e
Compare
KunalJavelin
approved these changes
Oct 10, 2026
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.
Closes #355 · Builds on #361 (#354, merged).
What this does
You can now take back a message, or go back to any earlier point in a session, on any backend.
The agent forgets what was taken back, and the message comes back to your input box to edit and resend.
Optionally, the files go back too, to exactly how they were when that message was sent.
codeoid attach:/undotakes back the last message;/undo filespreviews restoring the files;/undo files yesdoes it.codeoid undo <session> [files [yes|force]].git push,gh,curl, installs or deployments.How it was checked
Live, against a real daemon with the real Claude backend (scratch git repo):
hello.txt=one". Turn 2: "Correction: codename is now FALCON. Appendtwo, createnotes.md".hello.txt= one/two,notes.mdexists.hello.txt, removenotes.md, no conflicts. Nothing changed.hello.txt=one,notes.mdgone, the turn-2 prompt returned.hello.txtby hand, thencodeoid undo <s> filesandfiles yeshello.txt (edited by you since).files yesrefuses; the hand edit survives.codeoid undo <s>(conversation only)Repeated after both audit rounds. Two undos in a row, after taking everything back: "No, you haven't told me a codename". The agent forgets on a real backend.
Tests: typecheck, lint and build pass, plus 2,886 daemon tests and 585 web tests. Each guard added during the audits was mutation-checked: removing it makes a test fail.
rewind.test.ts(30) covers:terminal-rewind.test.ts(5) and CLI replay tests.RewindModal.test.tsx(6) and two MessageRow tests.Audits: two rounds, each with a correctness reviewer and a security reviewer. All findings were fixed here. The ones that shaped the design:
🤖 Implementation context (for agents / maintainers)
Protocol (
packages/protocol/src/turns.ts)session.rewind { sessionId, turnId, restoreFiles?, dryRun?, force? }returnssession.rewind.result { removedTurns, restoredPrompt, files?: { restore, remove, conflicts, late?, applied }, filesUnavailable?, irreversible, refused?, dryRun }. Scope:session:send+ ownership. Refused for conductor/role sessions.Daemon (
src/daemon/session.ts)rewind(): dry runs go straight to#planRewind. Real runs serialize on the send chain via#rewindInner.#rewindInnertakes these steps in order:force.restoreTree.#teardownProvider+accumulator.seed(kept).resetToNewSession(new backing id) +#seedProviderFromHistory.{event: "session.rewound", turnId, removedTurnIds, fromMessageId}.applyRewinds, rotate#resumeKey,#replaya snapshot to every client, and audit.<turnId>-end,#snapshotTurnEndon idle/error). Snapshots are serialized through#checkpointChainso a start snapshot waits on the previous end.irreversibleEffects()covers MCP tool calls andrun_shellcommands matching an outside-effect pattern.Checkpoints (
src/daemon/checkpoints.ts)currentTreestages the work tree, without a ref.diffTreesisdiff-tree -r -z --name-status.restoreTree:git checkout <sha> --pathspec-from-fileunderGIT_LITERAL_PATHSPECS.Transcript (
src/daemon/transcript.ts)applyRewindsruns insideloadTranscript. It cuts positionally fromfromMessageIdto the marker, and falls back to dropping rows stamped withremovedTurnIds. That covers resume, fork priming and paging.Clients
components/RewindModal.tsx. The taken-back prompt goes back to the draft via acodeoid:prompt-draft-changedevent thatPromptBoxlistens for.MessageRowgets the "go back to here" action andSessionControlstheUndoButton./undoinApp.tsx, plus wsturns/rewind./undoin attach (prefills viarl.write) andcodeoid undo. Shared grammar and wording insrc/terminal/rewind.ts.Known limits
🤖 Generated with Claude Code