Replay¶
bernstein replay re-displays the events from a past orchestration run so you can debug, diff, and reproduce. It has a single command surface with several subcommands:
bernstein replay <RUN_ID>- the original replay, optionally with task-trace re-submission.bernstein replay diff RUN_A RUN_B- localise the first divergence between two recorded runs.bernstein replay export <AGENT_ID> -o RECEIPT- write a portable per-step receipt.bernstein replay publish <AGENT_ID> -o RECEIPT- write a redacted receipt for publishing.bernstein replay verify <RECEIPT>- offline verifier for an exported receipt.bernstein replay diff-journal A B- per-step divergence finder across two journals.
All subcommands read from the same underlying journal on disk. The base command additionally supports task-trace replay, which re-creates a new task from a stored task trace and (optionally) compares the replay's result_summary against the original via a colour diff.
What replay does (and doesn't do)¶
Replay is deterministic re-display of a past run's recorded events. Every event the orchestrator emitted - run_started, agent_spawned, task_claimed, task_completed, task_verification_failed, agent_reaped, run_completed - is replayed in order with its original timing offsets.
What replay does not do:
- It does not re-execute external HTTP calls. Any HTTP traffic the original agents performed (LLM API calls, GitHub API writes, webhook deliveries) is captured in the log but not re-issued.
- State mutations to remote services (a PR opened, a Slack message sent, a row inserted into your database) are not rolled back or repeated.
- It does not re-create branches or worktrees. The git state is whatever your repo currently is.
For full re-execution of the same task with a (potentially different) model, use the task-trace mode: bernstein replay <task_id> --model opus. This re-submits the original task description (plus any --extra-context you provide) as a new task on the running server and waits for it to finish, then renders a diff between the original and the new result_summary. (cli/commands/advanced_cmd.py.)
Where replay state lives¶
.sdd/
runs/
<run_id>/
journal.jsonl # canonical Merkle-chained event journal (one JSON event per line)
metadata.json # session metadata (started_at, git_branch, git_sha, config_hash)
divergence_report.json # written by `replay --verify` when a step diverges
traces/
<task_id>-<timestamp>.json # per-task traces (used by task-trace replay)
- The canonical run-event journal is
journal.jsonl, written by the always-onEventJournal(core/replay/journal.py). Each event chains asH(prev_hash, event_type, payload_hash, monotonic_index)and the head hash is the run identity. - Recording is on by default;
BERNSTEIN_REPLAY_RETENTION=Nbounds how many past run journals survive on disk (oldest run directories are pruned) instead of an on/off gate. - At run finalization the journal head is sealed into the run's lineage spine, so the replay identity and artifact provenance share one root.
- Session metadata is parsed by
read_session_replay_metadata()fromcore/runtime_state.py. - Task traces are loaded by
TraceStore(core/observability/traces.py).
Verifying and rebuilding from the journal¶
bernstein replay <RUN_ID> --verifyrecomputes the journal's Merkle chain and reports byte-identity, or the exact first divergent step index. On divergence it writesdivergence_report.json(step_index,expected_hash,actual_hash) and exits non-zero. An injected non-deterministic tool result surfaces as a precise hash mismatch rather than a silent drift.bernstein replay <RUN_ID> --from-step Nrebuilds a deterministic state projection by walking events[0, N). Two independent invocations produce byte-identical output, so the reconstruction is reproducible.
The fingerprint shown after a replay is the Merkle head over the journal's event chain; identical decision streams produce identical heads, which is how you verify two runs really are the same.
bernstein replay¶
Synopsis: bernstein replay RUN_ID_OR_TASK_ID [flags]
Flags: (source: cli/commands/advanced_cmd.py)
| Flag | Default | Meaning |
|---|---|---|
RUN_ID_OR_TASK_ID | required | Run ID, the literal latest, the literal list, or a task ID. |
--sdd-dir PATH | .sdd | Path to the .sdd state directory. |
--as-json | off | Emit raw JSONL (one event per line) instead of the Rich table. |
--limit N | none | Show only the first N events. |
--model NAME | none | Override model for task-trace replay (e.g. opus, sonnet, o3). |
--extra-context TEXT | none | Append extra hint text to the replayed task description. |
Resolution rules:
bernstein replay list- print every recorded run with timing, branch, SHA, event count, log size.bernstein replay latest- replay the most recent run.bernstein replay <run_id>- replay a specific run by directory name.bernstein replay <task_id>(no run with that ID exists) - falls through to task-trace replay: re-submit the task and diff result summaries.
The Rich table columns are TIME (offset from run_started), EVENT, AGENT, TASK, DETAIL. Common detail keys: model, role, cost_usd, fingerprint, tick, failed_signals. Events are colour-coded by type (run_started / task_completed are green; agent_reaped and task_verification_failed are red).
# What ran most recently?
bernstein replay latest
# Specific run, machine-readable
bernstein replay 20260415-143022 --as-json | jq '.events[] | select(.event=="task_completed")'
# Re-execute task T-abc123 on Opus instead of whatever it ran on originally
bernstein replay T-abc123 --model opus --extra-context "Make sure tests pass on Python 3.11."
Subcommands¶
Beyond the base run/task replay, bernstein replay exposes subcommands for diffing runs and for exporting and verifying portable receipts.
bernstein replay diff RUN_A RUN_B¶
Localises the first divergence between two recorded runs. Walks both event chains in lockstep and reports the first step whose hash differs, so a non-deterministic drift between two runs surfaces as a precise step index rather than a wall of diff.
bernstein replay diff-journal A B¶
Per-step divergence finder across two journals. Like diff but operates directly on two journal paths, reporting the first step index where the chains diverge.
bernstein replay export <AGENT_ID> -o RECEIPT¶
Writes a portable per-step receipt for an agent's journal to the path given by -o. The receipt carries the step chain and its head hash so it can be verified offline by another party.
bernstein replay publish <AGENT_ID> -o RECEIPT¶
Same as export, but produces a redacted receipt suitable for publishing. Sensitive fields are stripped while the head hash still anchors the visible steps.
bernstein replay verify <RECEIPT>¶
Offline verifier for an exported or published receipt. Recomputes the receipt's chain and reports byte-identity or the first divergent step. Exits non-zero on mismatch.
Common use cases¶
Reproduce a flaky failure. Run bernstein replay latest and read the EVENT column for task_verification_failed rows to see exactly which gate failed and on which agent. The detail column carries the failed signal names; cross-reference with .sdd/traces/ for the agent's full transcript. For a machine-readable pass, use bernstein replay latest --as-json | jq '.events[] | select(.event=="task_verification_failed")'.
Compare models on the same task. Find the run where the task succeeded:
then re-run with a different model:
The CLI prints a diff of the two result_summary strings.
Verify a fix. After fixing a bug, run the failing task again with bernstein replay <task_id> and compare. If the fingerprint changes only in the expected places, you have evidence the fix held.
Limits¶
- Replay does not re-issue HTTP calls. Mocking a remote dependency from the original log is not supported - agents in task-trace replay make fresh calls.
- Side effects to remote services (PRs, messages, webhooks, DB rows) from the original run are not undone by replay. There is no "rewind" mode.
- Run-event replay only re-renders what was recorded. If the
EventJournal(core/replay/journal.py) did not capture an event class, it will not appear. - Fingerprints depend on the exact recorder version. A replay log written by an older Bernstein may produce a different fingerprint when re-fingerprinted by a newer build.
- Task-trace replay submits a new task - it does not retroactively re-run the original task in place. The original task's record stays in the archive untouched.
For deeper integrity guarantees, see fingerprint (re-computes the run's SHA-256 and verifies it against a stored reference).