Claude-Code-style time-travel for Emacs agent-shell: snapshot every agent turn, then rewind or fork a session's conversation and code together back to any earlier turn.
Two layers, joined by a tiny index:
- Code — each turn becomes a jj commit; rewind restores the tree.
- Conversation — the agent SDK's own session log is truncated to turn N and resumed, so the
model faithfully continues as of that turn — no regeneration, and no protocol extension
required. (It's essentially what Claude Code's own
/rewinddoes, driven from Emacs.)
Everything is non-destructive (jj undo + the original session file recover the pre-rewind
branch), and the resumed shell renders natively via session/load replay. DESIGN.md has the
full design + the experiments that validated it.
Status: working end-to-end — conversation + code rewind/fork validated on real sessions.
- Emacs + agent-shell (tested v0.57.3) +
jjon PATH. - The session's project must be a jj repo (
jj git init --colocatein a git repo, orM-x agent-shell-rewind-init, or setagent-shell-rewind-auto-initto t).
(add-to-list 'load-path "~/Lab_notebook/agent-shell-rewind")
(require 'agent-shell-rewind)
(agent-shell-rewind-mode 1) ; snapshot every agent turn, globallyThen in a jj-backed project, each agent turn adds a checkpoint labeled with the prompt that drove it (from the transcript) and anchored to both the jj change (code) and the SDK session leaf-uuid (conversation).
goto(agent-shell-rewind-goto-turn) — reset the code to turn N, in place. Conversation untouched. ("undo the files, keep talking")rewind(agent-shell-rewind-rewind-turn) — unwind THIS shell to turn N in place (same buffer/window) — code and conversation — then continue. Non-destructive: the pre-rewind session stays on disk (recoverable viaagent-shell-resume-session) and code isjj undo-able. ("take me back and go from there")fork(agent-shell-rewind-fork-turn) — like rewind but into a NEW shell, leaving the original open too. ("explore an alternative without losing this one")
Both rewind and fork restore code (jj) + truncate the SDK session JSONL to turn N + resume
with native session/load replay, so the shell looks like a normal agent-shell session.
agent-shell-rewind-timeline— the table UI. Keys:g/RETgoto ·wrewind (in place) ·ffork (new shell) ·ddiff ·rrefresh ·qquit.agent-shell-rewind-list— completing-read a turn; jumps the code there.agent-shell-rewind-diff—jj diffworking copy vs turn N.agent-shell-rewind-init— set up jj for the current project.
agent-shell-rewind-command-map is a prefix map; bind it, e.g.:
(with-eval-after-load 'agent-shell
(define-key agent-shell-mode-map (kbd "C-c r") agent-shell-rewind-command-map))
;; C-c r t timeline · l list · g goto(code) · w rewind(in place) · f fork(new shell) · d diff · i initNote: the key maps are defvars, so on in-place reload use M-x eval-buffer (or restart
Emacs) to pick up new bindings.
Everything is a jj operation → undoable with jj undo / jj op restore, and
non-destructive (old turns stay in the DAG). Checkpoints live in
$XDG_STATE_HOME/agent-shell/rewind/<project-hash>/checkpoints.jsonl.
turn-complete event → jj describe @ (labels the turn's tree) → capture change-id →
jj new (fresh working copy for next turn) → append {turn, jj_change, jj_op, session_id, leaf_uuid, ...}. goto-turn = jj new <change>.
- Code → jj (content-addressed, compact,
jj undo-able)..gitignorekeeps churn out. - Conversation → the SDK's own
~/.claude/projects/<enc-cwd>/<session-id>.jsonl(a parentUuid-chained DAG — the same store Claude Code's/rewindtruncates). We don't copy it into jj; we truncate it to turn N +resume(proven faithful — the model reconstructs context from the truncated file, no regeneration, no ACP extension). checkpoints.jsonl= the join index (turn ↔ jj_change ↔ leaf_uuid). Rewind/fork = restore code (jj) + truncate session to leaf_uuid + resume. Storage is a few MB/session (append-only logs diff cheaply); no per-checkpoint dirs.
The resumed shell renders the prior conversation with agent-shell's own rendering, via
session/load full-replay — user turns at the Claude> prompt, agent responses below,
exactly like a live session. The trick: agent-shell-session-restore-verbosity is read
during the async load, so we setq it to full (a let-binding unwinds before the load
fires — that was the earlier "flaky") and restore the old value on the new shell's
prompt-ready (with a timer safety-net). agent-shell-rewind-restore-verbosity (default
full) controls it. Validated: turns 1..N replay natively and verbosity auto-restores.
- Diffstat column in the timeline, checkpoint retention/pruning, named bookmarks.
- Optional btrfs "nuke the whole workspace" layer for messy untracked-state projects.
- A
session/load-less fallback (inject the saved transcript atprompt-ready) for agents that don't advertise native replay.
- Snapshot-per-turn + checkpoint store +
goto/restore (both directions) — validated. - Turn summaries pulled from the transcript's last
## Usersection. - Timeline UI (
agent-shell-rewind-timeline) + prefix keymap. - Live end-to-end dogfood on real
turn-completeevents — real turns snapshotted a jj repo (summaries "go for it" / "check" from the live transcript);goto-turnreset the code back to turn 1 (files reverted, new file vanished) and forward to turn 2. The full event → handler → jj chain confirmed on real data, not mocks. - Tier-2 conversation rewind — VALIDATED. Truncated a real session JSONL at turn 2 and
resumed it: the model recalled the early turns and correctly had no memory of the
later turns we cut. Faithful, no regeneration, no ACP change.
fork-turnwires this to jj code-restore for full conversation+code rewind.
This package was largely vibecoded in collaboration with Claude Fable 5.
MIT — see LICENSE.