Skip to main content

Time Machine — replay, fork, diff

Time Machine is the run-history toolkit: replay an old run without live IO, fork from a recorded decision point, or diff two executions. It works because flux records accepted plans and leaf-operation results as durable artifacts.

Two recordings make a run travelable:

  • Every accepted plan is persisted as canonical, re-parseable Flux-Lang text — so re-executing a past run needs no model call.
  • Every leaf-op dispatch (a file read, a shell command, an HTTP call) records a redacted cassette cell — op, input hashes, output — on the session's event stream, so op outputs are durable and re-execution needs no live IO either.

Three verbs operate on any recorded run:

CommandWhat it does
flux replay <session>re-execute a past run exactly — offline, model-free, side effects never re-fire
flux fork <session> --at Nbranch a run at a decision point and continue differently
flux diff <A> <B>align two runs and show where the plan, or the world, diverged

Use flux sessions to list recent session ids (s_42-style); every verb also accepts last.

flux replay — hermetic re-execution

flux run "summarize Cargo.toml" # a normal run records plans + cassette automatically
flux replay last # re-execute it: no API call, no live IO
flux replay s_42 --turn 2 # replay only the second turn's plans (1-based)
flux replay s_42 --sub-agents # also replay spawned sub-agent child streams, in spawn order
flux replay s_42 --json # machine-readable report

Plans re-parse from the stored plan text; op outputs are served from the cassette. No model client is ever constructed, no side effect re-fires, and confirm gates auto-allow — nothing can actually execute from tape. The transcript renders like the original, minus the latency, ending with a summary:

replayed 3 plan(s) · 7/7 recorded cell(s) served

Replay reproduces the recorded loop's dispositions (skipped identical plans, halted prefixes, revision fast-forwards) and tolerates the nondeterministic interleaving of parallel branches. Any mismatch between the re-executed run and the recording is a loud divergence error with exit code 1 — never a silent continuation. With --turn, a statement that references a symbol bound in an earlier turn fails honestly rather than fabricating a value.

flux fork — branch at a decision point

# Branch s_42 at top-level statement 2 (0-based, of its final executed plan):

# Mode A: inject a different value as that statement's result, run the rest live
flux fork s_42 --at 2 --inject '{"status": "degraded"}'

# Mode B (default): let the model re-plan the tail from the forked state
flux fork s_42 --at 2 --replan --prompt "try the staging environment instead"

# Mode C: continue with a corrected plan file (.flux text or DraftAst JSON);
# unchanged leading statements fast-forward, edits run live
flux fork s_42 --at 2 --edit fixed-plan.flux

The fork creates a new session, correlated to its source. The prefix before --at replays hermetically from the cassette — no side effects — then execution crosses the cassette-vs-live boundary: the tail runs live, through the real approval envelope, against today's world. The three modes are mutually exclusive; --prompt refines --replan (default: continue the recorded task). Agent flags (e.g. -m for the re-planning model) apply as on flux run.

The forked session records its own cassette and plan text, so a fork is itself replayable — and diffable against its parent:

flux fork last --at 1 --replan
flux diff s_42 s_43

flux diff — where did two runs diverge?

flux diff s_42 s_43 # e.g. a run and its fork
flux diff s_42 last --json

flux diff aligns the two runs' executed top-level statements positionally and classifies each row:

  • = — identical statement, identical recorded outputs.
  • ~ plan diverges — the statement itself differs (the model, or an edit, chose differently).
  • ≠ same statement, different world — the same plan step got a different recorded op output (a file changed, a command returned something else).

Exit code is 1 when the runs diverge, diff-style, so it composes in scripts. Positional alignment is designed for run-vs-fork pairs; unrelated runs will mostly report plan divergence.

Cassettes

Capture is on by default for every run, including flux flow run. Each recorded cell costs roughly 442 bytes on representative ops — about 0.01% of a heavily-used event log — so the default is cheap.

  • Redaction — cell contents pass through the same redactor that scrubs stored plan text before anything hits disk, so recorded op outputs carry the same secret-scrubbing posture as the rest of the durable event log.
  • Per-op capFLUX_CASSETTE_MAX_BYTES (default 1 MiB). An over-cap output keeps a truncated head and is marked as such; replay refuses truncated cells loudly rather than serving a partial world.
  • Kill switchFLUX_CASSETTE=0 disables capture entirely. Runs recorded this way (and runs from before the cassette existed) cannot be replayed or forked; the commands say so explicitly.

Resumable stored flows

Authored flows get a related capability on the flux flow run path — halt, fix, and resume without repeating completed work:

flux flow run deploy.flux --resumable
# … a failed statement (or a paused `await`) prints a structured halt report:
# ✓/✗/· marked statement tree, a machine-readable failure summary, and the session id,
# then exits non-zero instead of erroring the whole run.

# Fix the flow file, then resume that session:
flux flow run deploy.flux --resume s_57
flux flow run deploy.flux --resume last

--resume (which implies --resumable) re-parses the — possibly corrected — file, folds the halted session's statement ledger, fast-forwards the completed prefix (bound values are rehydrated, side effects are not repeated), and executes from the first changed statement. --resume last needs the flow to declare a name (flow <name> -> …) so its most recent halted session can be found unambiguously; unnamed flows need the explicit session id. For the in-language durability markers that pair with this (await, checkpoint, once), see Durability.

Limits

  • Only recorded runs travel. Sessions captured with FLUX_CASSETTE=0, or predating cassette capture, are not replayable or forkable.
  • The model is not replayed. The cassette records leaf-op dispatches; the outer agent loop (the model calls themselves) is never cassetted. Replay re-parses the accepted plans — it reproduces what the run did, not the token stream that produced it.
  • A fork's tail is live. Past the divergence point, ops act on today's world. That is the point — and it is gated by the same approval envelope as any live run — but it means fork output depends on current state, and --replan is nondeterministic by design (it is a real model turn).
  • Divergence is fatal, by design. If the world leaked into a run in a way the recording cannot explain, replay stops with an error instead of improvising.
  • Agent Lab — the SDK product built on these recordings: golden scenario tests, counterfactual what-ifs, and crash resurrection.
  • CLI — the full command surface, including flux sessions and flux flow run.
  • Durabilityawait, checkpoint, once, saga.
  • Execution model — why flux runs are deterministic.
  • Concepts — plans, symbols, and the safety envelope.