Rewind — Roll Back to Any Historical Message#
Status: Implemented (2026-06) Reference: Claude Code
/rewindCode:agent/_rewind.py,webui/ws_actions/chat.py,web/components/chat/messages/message-actions.tsx
1. Behavior Definition#
After the user clicks the ↩ button on a user message (or enters /rewind N):
- File restore: file changes from that message's turn and all subsequent turns are restored from the checkpoint
- Message text refill: the text of that user message is placed back into the chat input box
- UI update: that message and all later conversation are removed from the UI
- DAG branch: the old conversation is kept in the DAG (not deleted); the current branch head moves to just before that message
- The user can edit the text in the input box and resend it → starting a new branch from that point
2. Comparison with Claude Code#
| Claude Code | OpenProgram | |
|---|---|---|
| Trigger | /rewind lists checkpoints, pick one |
↩ button + /rewind N |
| Rollback granularity | per-prompt (each user message) | same |
| File restore | restore from checkpoint snapshot | restore from checkpoint snapshot |
| Message refill | user message text placed back into input box | same |
| Conversation handling | fork conversation (new branch) | DAG branch (old conversation kept, not deleted) |
| bash blind spot | warns "does not affect manual/bash edits" | we trigger through a unified entry point, so bash is covered too |
3. Implementation#
3.1 Backend _rewind.py#
rewind_to(session_id, user_msg_id):
- Find the user node in the DAG corresponding to
user_msg_id - Extract that node's
output(i.e., the user message text) - Find that node and all subsequent assistant/llm nodes (sorted by seq)
- Call
revert_turnfor each assistant node to restore files - Mark
metadata.rewound = Trueon every rewound node - Move the store head to the last node before the target (
Nonewhen rewinding to the very start — head must never rest on a rewound node) - Return
{ user_text, turns_reverted, restored_paths, new_head_id, errors }
Key point: it accepts the user node ID directly, with no need to convert it into an assistant ID.
new_head_id is the head the rewind landed on. Callers that keep their
own head mirror must write it back — see 3.2.
3.2 Backend WS handler#
handle_rewind(ws, cmd):
- Receives
{ session_id, target_msg_id } - Refuses with
{ code: "run_active" }while a run is in flight, so a rewind cannot move HEAD out from under a streaming reply - Calls
rewind_to - Whenever
new_head_idis non-None, feeds it throughserver._set_active_headand re-estimates context stats. This is keyed on the head move, not onerrors: a file-restore failure is a partial failure of the rewind, but the store head has already moved, so the mirror must follow.errorsstill rides the result frame as the partial-failure warning - Returns
{ type: "rewind_result", data: { session_id, user_text, ... } }
The _set_active_head step is not cosmetic. rewind_to writes only
the store; the webui also keeps a per-session mirror in
_sessions[sid] (head_id + messages) that _save_session flushes
straight back into the store. A rewind that skips the mirror leaves it on
the pre-rewind head, and the next save silently reverts the rewind.
Every path that moves HEAD goes through _set_active_head for this
reason — it writes the store, re-reads the branch into the mirror, and
drops the message cache in one step.
handle_rewind_list(ws, cmd):
- Receives
{ session_id }— this is what/rewindwith no argument sends - Returns
{ type: "rewind_points", data: { session_id, points } }, newest first
Every frame of both types carries session_id, including the error
ones. Listeners match on it: rewinds in two conversations emit the same
frame type, and a listener keyed on type alone answers to whichever lands
first.
3.3 Frontend#
rewindToHere():
- Send WS action
{ action: "rewind", session_id, target_msg_id: msg.id } - After receiving a
rewind_resultwhosesession_idmatches:- Call
useSessionStore.getState().setComposerInput(data.user_text)to refill the input box - Call
wsSend({ action: "load_session", session_id })to refresh the message list (rewound messages no longer appear on the current branch) - Show a toast
- Call
- The listener also detaches on a 30s timeout. A rewind that fails without emitting a frame would otherwise leave the row's action buttons disabled until a page reload.
/rewind with no argument sends rewind_list; use-ws.ts renders the
returned points as a numbered system line in the transcript, so the
indices stay on screen while the user types /rewind N.