How replay works
TypeScript durable runs use deterministic runtime policy plus recorded host-call results. Given the same inputs, compatible source hashes, and the same recorded results for host calls, agent control flow is expected to produce the same outputs.
- Original run: every
prompt(),tool(),fetch()call is recorded — sequence number plus result — in the journal (the run's call log), the append-onlyrecords.jsonlfile in the run directory.chidori/runs/<run_id>/. - Persist: the journal is written as the run executes, so whatever already happened is on disk when the run pauses, crashes, or completes.
- Replay: re-run the agent with the journal loaded. Each host call checks the journal at its sequence number — a hit returns the recorded result instantly, a miss executes live.
Replay is guarded, not best-effort:
- Source verification: every resume surface (the server's resume/approve
routes and
chidori resume) verifies the agent's entry + module source fingerprints against the run's snapshot manifest before replaying, and refuses on mismatch — recorded results are never paired with changed code silently. (Runs persisted before manifests existed skip with a warning.) - Edit-and-resume is an explicit opt-in: pass
--allow-source-changetochidori resume(or"allow_source_change": trueon the server's resume/signal/approve routes) to replay a recorded run against edited code. The divergence checks below still guard the journaled prefix — an edit that changes an already-recorded call fails loudly, an edit past the pause point resumes cleanly. ABI/policy mismatches are environment drift, not edits, and always refuse. Every accepted edit is also recorded as a commit in the run's git-like source history, so the version that produced the journaled prefix and the version taking over both stay recoverable — inspect and diff them withchidori history <run-id>(Source History). - Divergence checks compare arguments, not just names: a replayed call
must match the recorded call's function and arguments (the derived
request_digestfield is ignored). A completed async host call whose recorded arguments differ from the re-executed call's is a hard divergence error instead of a silent live re-execution of the side effect. - Escape hatch:
CHIDORI_REPLAY_LAX=1downgrades argument-level divergence to a warning (serving the recorded result / re-executing live, the historical behavior). Function-name mismatches are always fatal.
chidori resume carries the run's own configuration so recovery needs no
flag archaeology:
- The model travels with the run. The run's resolved default model is
recorded in its manifest;
resume(andbranch-resume/branch-rerun, and the server's resume/replay/approve routes) default to it. A barechidori resume agent.ts <run-id>replays a--model-started run byte-for-byte; an explicit--model/CHIDORI_MODELstill overrides — and a divergence error that stems from a model mismatch says so, naming both models, instead of blaming "changed code". - Trust mirrors
run.resumeaccepts--trusted/--untrustedso live continuation past the replay frontier (crash recovery) executes under the same posture the originalchidori run --trustedhad. Without--trusted, gated effects re-ask at the terminal exactly likerun. - Continuation is journaled. Live records past the frontier persist
into the same run directory, so a resume that itself crashes resumes from
the new frontier — and the run's lease (
lease.json) refuses a second concurrent driver of the same run dir.
This means you can:
- Debug without spending money: save a failing session, replay locally with breakpoints.
- Iterate for free:
chidori dev agent.tsis the edit-and-replay loop as a command — it watches the file and re-runs on every save, replaying recorded calls from the journal so edits cost zero tokens. - Run deterministic tests: a recorded run is a $0 CI test — see Replay as test below.
- Resume after crashes: the journal persists after each host call; on restart, replay picks up where it left off.
- Pause for human approval:
input()suspends execution; when the human responds, the agent replays to that point and continues.
Replay as test
A recorded run is a complete, deterministic specification of your agent's behavior — commit one and assert against it in CI.
Don't commit the raw run directory: it is heavy (the runtime snapshot blob
alone can run to tens of MB). Instead,
chidori export <run_id> --fixture tests/fixtures copies just the four
artifacts verify reads (records.jsonl, runtime.snapshot.json,
output.json, input.json) into tests/fixtures/<run_id>/ — typically a
few KB. Export refuses runs whose journal isn't a complete verifiable
record (still leased by a live process, paused at a pending host call, or
never completed).
chidori export <run_id> --fixture tests/fixtures # once, after recording
git add tests/fixtures
chidori verify agent.ts <run_id> --runs-dir tests/fixtures # in CIchidori verify replays the run in the strictest posture: no providers
configured, no tools registered, the untrusted policy profile, and no
--allow-source-change escape. A fixture without its runtime.snapshot.json
manifest is refused outright (a verification that cannot check source
fingerprints is not a verification), replay runs with
CHIDORI_REPLAY_STRICT=1 — a journal miss is a hard error instead of a
silent live fallthrough — and CHIDORI_REPLAY_LAX is ignored. It exits 0
on pass and 1 on any failure, with a distinct message for each cause:
source drift, an unclean replay, a journal the run cannot fully consume
(the replayed run must journal exactly as many records as the fixture
holds — a shorter path is a divergence even though nothing executed live),
a run that pauses instead of completing, unexpected live calls, or output
that isn't byte-identical.
One caveat: workspace state is real disk, not journal-served, so top-level journaled workspace writes do re-materialize their recorded artifacts during verify — the same bytes, with a fresh mtime. Everything else replays without touching the world, and nothing is written to the run directory itself.
When you need a machine-readable result rather than pass/fail,
chidori resume <agent.ts> <run_id> --ci replays and emits a JSON report,
with distinct exit codes: 0 on match, 3 on divergence, 1 on error. Flags
and exit codes for both commands:
CLI reference.
Replaying from an SDK
Both SDKs talk to a running chidori serve instance over HTTP — no native
bindings, no install. The Python SDK is pure stdlib:
import sys
sys.path.insert(0, "sdk/python")
from chidori import AgentClient, Checkpoint
client = AgentClient("http://localhost:8080")
# Create a session (runs the agent with live LLM calls)
session = client.run({"document": "Rust is a systems language."})
print(session.output)
# {"summary": "...", "action_items": "..."}
# Save a checkpoint to disk
checkpoint = session.checkpoint()
checkpoint.save("/tmp/session.json")Later, replay the session from disk — zero LLM calls:
from chidori import AgentClient, Checkpoint
client = AgentClient("http://localhost:8080")
cp = Checkpoint.load("/tmp/session.json")
# Replay: re-executes the agent but returns recorded host-call results
replayed = client.replay(cp)
assert replayed.output == session.output # identical outputSee sdk/python/README.md and
sdk/typescript/README.md for the full SDK
surface.
FAQ
Common questions: Python support, Node, providers, how Chidori compares, data locality, securing the server, and what to do when things go wrong.
Running Modes
One-shot CLI runs, the HTTP session server and its endpoint reference, event-driven handlers and the serve status-code contract, and approval postures.