> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Time Travel

> Rewind a run to a previous frame, fork alternate branches from snapshot checkpoints, replay with edited inputs or code, and restore worktrees to any recorded filesystem state.

Hour four of an overnight run: the plan node produced a subtly wrong plan, six downstream agents faithfully built on it, and the review node finally caught it. Without time travel your options are bad — start the whole run over and re-pay every token, or hand-edit the workspace and hope the orchestrator's idea of state still matches the filesystem's. Neither works, because agent runs have **two** kinds of state that drift apart the moment you touch one: the orchestrator's database (which tasks ran, what they output) and the worktree (what the agents actually wrote to disk). Rolling back one without the other produces a run that is lying to itself.

Time travel in Smithers means rolling both back *together*, to a point that was actually recorded — and, when you'd rather not destroy history, branching a new run from that point instead. Every task attempt records a [jj](https://jj-vcs.github.io/jj/) commit pointer for its worktree; every render frame gets a snapshot of graph state. Those two ledgers are what make "go back to before the plan node and try again with a different prompt" a one-line command instead of an archaeology project.

One distinction up front, because the industry uses "replay" for two different things. **Deterministic replay** (Temporal, rr) re-executes code against a recorded log and guarantees you reconstruct *exactly* the same state — which requires the code to be deterministic. **State restoration + re-execution** (Smithers, LangGraph) restores a persisted checkpoint and runs *forward* from it, fresh — no determinism contract, so LLM calls are legal, but the re-executed portion may (often deliberately) diverge from what happened before. Smithers is squarely in the second family: you go back to a real recorded state, and everything after it is a new future, not a reconstruction of the old one.

## How the industry does it

| System                                                                                                                                                           | Mechanism                                                                                                                                                                                                                                                                | What "going back" means                                                          | Tradeoff                                                                                                                                                                                 |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Temporal reset](https://docs.temporal.io/cli/workflow)                                                                                                          | Copy the [event history](https://docs.temporal.io/encyclopedia/event-history) up to a `WorkflowTaskCompleted` reset point into a new execution; deterministic replay reconstructs state, then execution continues on current code — optionally re-applying later signals | Exact state reconstruction; used to carry bug fixes past a failure               | Workflow code must be [deterministic](https://docs.temporal.io/workflow-definition#deterministic-constraints); resets nothing outside Temporal (no filesystem, no external side effects) |
| [Azure Durable Functions rewind](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-instance-management#rewind-instances-preview) | `RewindAsync` puts a *failed* orchestration back into a running state by replaying from history                                                                                                                                                                          | Failure recovery only — you cannot rewind a healthy run to explore               | Preview-status API; same determinism contract as normal replay                                                                                                                           |
| [Event sourcing / CQRS](https://martinfowler.com/eaaDev/EventSourcing.html)                                                                                      | State is a fold over an append-only event log; rebuild any past state by replaying a prefix                                                                                                                                                                              | Temporal queries and full rebuilds for free                                      | You must design every state change as an event; external side effects during replay need gateways/stubs                                                                                  |
| [LangGraph time travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel)                                                                         | `get_state_history` lists per-super-step checkpoints; invoking with a past `checkpoint_id` replays to it, `update_state` forks a new branch with edited state                                                                                                            | Checkpoint restoration; forks are new branches, the original thread is untouched | Graph channel state only — no filesystem capture, so a coding agent's actual edits aren't part of the checkpoint                                                                         |
| [git worktrees](https://git-scm.com/docs/git-worktree) + [jj op log](https://jj-vcs.github.io/jj/latest/operation-log/)                                          | Every repo mutation is itself versioned; `jj op restore` / `jj undo` rolls the whole repo — working copy included — to any prior operation                                                                                                                               | A true filesystem time machine                                                   | Knows nothing about *why* states existed; no link to task/attempt semantics                                                                                                              |
| [rr](https://rr-project.org/) / [Replay.io](https://blog.replay.io/how-replay-works)                                                                             | Record nondeterministic inputs (syscalls, signals, shared-memory reads) once; re-execute deterministically any number of times                                                                                                                                           | Perfect instruction-level replay of one process                                  | Single-process debugging scope; heavy recording machinery, not an orchestration primitive                                                                                                |
| [Redux DevTools](https://github.com/reduxjs/redux-devtools)                                                                                                      | Actions are serializable and reducers pure, so the log can be sliced, reordered, and re-folded live                                                                                                                                                                      | UI state scrubbing during development                                            | Only works because reducers are pure — the property agent workloads lack                                                                                                                 |
| [OpenAI Agents SDK sessions](https://openai.github.io/openai-agents-python/sessions/)                                                                            | Sessions persist conversation history per `session_id` across runs                                                                                                                                                                                                       | Resume and continue; no rewind, fork, or branch operations                       | Memory persistence, not time travel                                                                                                                                                      |
| [Antithesis](https://antithesis.com/docs/introduction/how_antithesis_works/)                                                                                     | Run the whole system inside a deterministic simulation hypervisor; any moment can be revisited and *branched* to explore multiple futures                                                                                                                                | The strongest version of forking that exists                                     | Requires your entire system to run inside their hypervisor; a testing platform, not a runtime                                                                                            |

The jj operation log is the closest spiritual relative to what Smithers does for the filesystem — and not coincidentally, jj is the machinery Smithers uses under the hood. What no system above provides out of the box is the *join*: orchestration state and filesystem state rolled back as one unit, keyed by task attempt. That join is the whole point.

## How Smithers does it

Two ledgers get written during every run, without workflow-author effort:

* **Graph snapshots per frame.** Each render frame captures nodes, outputs, loop state, and input, content-hashed (sha256) and stored deduplicated across `_smithers_snapshots`, `_smithers_snapshot_contents`, and `_smithers_snapshot_payload_refs` (`packages/time-travel/src/snapshot/captureSnapshotEffect.js`). This is the source of truth for fork, replay, and timeline.
* **VCS pointers per attempt.** Every task attempt records the worktree's jj commit ID in `_smithers_attempts.jj_pointer` (plus `jj_cwd`), and the [durability layer](/capabilities/durability) captures intermediate Tier 1/Tier 2 checkpoints in `_smithers_workspace_checkpoints` while the agent works.

Five operations act on those ledgers. They differ on two axes — *destructive vs. branching* and *DB vs. filesystem* — and picking the wrong one is the most common mistake, so here is the whole map:

| Operation    | Destroys history?                 | Resets DB state                                                                                                                                             | Restores filesystem                                             | Use when                                                     |
| ------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
| `fork`       | No — new child run                | Copies snapshot into child; `--reset-node` marks nodes pending                                                                                              | No (child gets metadata only)                                   | Branch an experiment without touching the parent             |
| `replay`     | No — new child run                | Same as fork, then resumes execution                                                                                                                        | Optional (`--restore-vcs` restores the source frame's revision) | "Run it again from frame 5 with different input"             |
| `rewind`     | Yes — deletes frames after target | Deletes frames, snapshots, and vcs-tags past the frame                                                                                                      | Restores the frame's recorded jj pointer                        | Roll one run back to an earlier frame, in place              |
| `timetravel` | Yes — cancels later attempts      | Resets target node **and dependents** to `pending`, deletes their output rows, truncates frames/snapshots past the attempt, flips the run back to `running` | Yes — `revertToJjPointer` on the attempt's recorded commit      | "Redo from this task" on the same run                        |
| `revert`     | No DB reset at all                | Nothing                                                                                                                                                     | Yes — worktree back to the attempt's `jj_pointer`               | Inspect or salvage filesystem state without touching the run |

Separately, `smithers restore <runId> --node <id> [--seq N]` restores a worktree to a *durability* checkpoint — the finer-grained per-tool-call ledger — rather than a per-attempt pointer. Attempt pointers tell you where each task ended; durability checkpoints tell you every step of how it got there.

### Forks are real child runs

`forkRunEffect` copies the parent's snapshot at the chosen frame into a **new run** (fresh UUID, frame 0) and records the edge in `_smithers_branches`, all in one transaction. The child carries the parent's outputs as already-satisfied state; only the nodes you `--reset-node` (plus, via `expandResetSet`, what depends on them) re-execute. Input overrides overlay the snapshot's input, so a fork can ask "what if the input had been different." Replay is fork + auto-resume — and if you pass an *edited workflow file*, replay re-blesses the child's workflow hash so the resume guard accepts the very edit you're carrying forward (`replayFromCheckpointEffect.js`). `smithers timeline <runId> --tree` renders the whole family: parent frames, fork points, and every child branch, derived purely from snapshot + branch rows.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# What happened, and where can I go back to?
smithers timeline RUN_ID --tree
smithers snapshots RUN_ID                      # per-tool-call worktree checkpoints

# Branch two experiments off frame 5; parent untouched
smithers fork workflow.tsx -r RUN_ID -f 5 -n analyze -i '{"model":"opus"}' -l exp-a
smithers replay workflow.tsx -r RUN_ID -f 5 --restore-vcs        # fork + resume

# Redo the same run from a bad task (filesystem + DB together), then resume
smithers timetravel workflow.tsx -r RUN_ID -n plan --resume

# Filesystem only — put the worktree back, decide later
smithers revert workflow.tsx -r RUN_ID -n implement --attempt 1
```

### The side-effect boundary

Rolling back a database does not un-send an email. Every destructive operation (`rewind`, `timetravel`, `revert`) runs `guardEffectBoundary` before touching anything: it finds external effects recorded after the target point, executes registered `revert` compensation handlers for them, and **blocks** (`TIME_TRAVEL_SIDE_EFFECT_BLOCKED`) on any `succeeded`/`unknown` effect it cannot compensate — unless you pass `--force`, which records `SideEffectBoundaryCrossed` and marks the run needs-attention. Compensation is something you opt tools into:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { defineTool } from "smithers-orchestrator/tools";
import { z } from "zod";

const createTicket = defineTool({
  name: "jira.create",
  schema: z.object({ title: z.string() }),
  sideEffect: true,
  async execute(args, ctx) {
    return jira.createIssue({ ...args, idempotencyKey: ctx.idempotencyKey });
  },
  // Time travel across this effect deletes the ticket instead of blocking.
  async revert(_args, ctx) {
    const ticket = await jira.findIssueByKey(ctx.idempotencyKey);
    if (ticket) await jira.deleteIssue(ticket.id);
  },
});
```

Branch operations (`fork`, `replay`) never compensate the parent's effects — the parent's history still happened. They stop before an effect-bearing boundary only when they would *re-execute* across it (`replay`, `fork --run`); a plain `fork` just warns. Git commits, branch moves, worktree writes, and `git push` are exempt from the boundary; external API mutations (issues, comments, PR merges) are not.

### Safety machinery

Destructive operations are serialized and audited: a per-run **rewind lock** (lease-renewed at every step, checked again inside the DB transaction) prevents two concurrent time-travel operations; a rate limiter caps rewinds per window; every operation writes a row to a rewind **audit** table, with in-progress rows recovered at startup. A run that is `running` with a live owner or fresh heartbeat refuses time travel without `force: true`. All mutations run through the dialect-agnostic storage seam, so the same code path works on bun:sqlite, PGlite, and Postgres.

## Guarantees and limits

**Guaranteed:** filesystem and DB state roll back atomically to a recorded point — the jj pointer restore happens before the DB transaction, and the transaction truncates frames, snapshots, and vcs-tags together so no derived view (fork, replay, timeline) can read discarded state. Forks never mutate the parent. Uncompensated external effects block by default rather than being silently crossed.

**Not guaranteed:**

* **No deterministic re-execution.** Re-run nodes call real agents with real nondeterminism. If you need "the replayed run provably does what the original did," that is Temporal's contract, not ours — see [Smithers vs Temporal](/why/vs-temporal).
* **You can only travel to recorded points** — attempts with a `jj_pointer`, frames with a snapshot. An attempt without a recorded pointer returns `success: false`.
* **`--force` means you own the consequences.** Crossed effects are archived and the run is flagged, but the email is still sent.
* **Filesystem restore targets the worktree where the attempt ran** (`jj_cwd`). If you deleted that worktree, there is nothing to restore into.
* **`revert` alone desynchronizes deliberately** — worktree moves, DB does not. It exists for inspection; follow with `timetravel` if you want the run to agree.

## Operating it

Day-to-day, from the CLI: `timeline`, `snapshots`, `fork`, `replay`, `rewind RUN_ID FRAME_NO`, `timetravel`, `revert`, `restore`, plus `retry-task` for the common "just redo this one task and resume" case. Every one has an [MCP twin](/integrations/mcp-server) so an agent operating a run has the same surface: `get_timeline`, `list_snapshots`, `fork_run`, `replay_run`, `rewind_run`, `time_travel`, `revert_attempt`, `restore_checkpoint` — the destructive ones require `confirm: true`, and `rewind`/`timetravel`/`revert` accept `--no-revert` to skip compensation handlers (skipped effects become blockers).

## See also

* [Recipes: time travel, fork, replay, diff](/recipes#time-travel-fork-replay-diff) — the copy-paste quickstart
* [Time-travel commands compared](/cli/overview#time-travel-commands-compared) — the five commands side by side
* [Time-travel API](/reference/time-travel) — `timeTravel` / `revertToAttempt` programmatic surface
* [Revert](/runtime/revert) — compensation handler contract in full
* [Durability](/capabilities/durability) — the checkpoint ledgers time travel reads from
