State on Disk
State lives on disk, not in chat.
This is the load-bearing idea behind loop engineering. An agent’s memory of what it did is an echo. The context window rots: earlier turns get summarized, evicted, or contradicted, and cost grows roughly quadratically with conversation length. A loop that carries its progress in the window inherits all of that decay.
By contract, each loopexec iteration runs stateless: it rebuilds a narrow context slice from disk, executes the agent, runs the check once (O6, single capture), records the outcome, and discards the window. Progress is durable because it lives in git and in two state files - never in the model’s recollection. Read state from disk and the CLI; never assume chat memory.
The real iterating run loop ships as of v0.2.0 (SPEC section 11): each iteration runs your exec then your check, computes its halt reason, and writes a typed JSONL receipt plus durable state. This page describes that durable-state contract - the behavior a run records today, not a future plan.
Two levels of state
Section titled “Two levels of state”Durable state splits into two files with two audiences. One is for the human at 3am. One is for the runtime.
| File | Audience | Contents | Authority |
|---|---|---|---|
STATUS.md | human | Plain-language narration: what the loop is doing, where it stands, what it is blocked on | advisory |
.loop_state.json | machine | Typed, schema-versioned state the runtime reads to resume and to compute the next halt reason | normative |
The machine file is the source of truth. STATUS.md is a rendering for people. They must agree, but only .loop_state.json is read back into a decision.
The machine state file
Section titled “The machine state file”The contract (SPEC section 8) requires .loop_state.json to carry at least the following fields. These are the anchors that make a loop resumable and a halt explainable.
{ "schema_version": 1, "phase": "iterating", "iteration": 7, "last_green_commit": "a1b2c3d", "open_failures": ["pkg/auth: TestExpiry", "pkg/auth: TestRefresh"], "cumulative_usd": 4.12, "baseline": { "model_calls": 1, "tokens": 8400, "top_error_type": "assertion" }, "determinism_probe": { "runs": 17900, "stable": true, "confidence": 0.99, "checked_at": "2026-06-25T11:02:00Z" }, "metric_integrity": { "check_cmd_sha": "9f2...", "test_tree_sha_at_last_green": "c4d..." }, "model_pin": "anthropic/claude-opus-4@build-...", "escalation": { "state": "none", "channel": null, "ref": null, "acked_by": null }}open_failures is the failing-test set F_i - the convergence signal, not a grepped line. last_green_commit is the point a regression reverts to. cumulative_usd is the run-total spend ledger. baseline is the Step-1 measurement an explain-halt reads to tell “still improving, raise the limit” apart from “stalled, never retry.”
What a receipt pins
Section titled “What a receipt pins”A receipt is the per-run audit record. The contract is blunt: a receipt MUST pin everything that determines the output, or fail closed with workspace_invalid. A receipt you cannot reconstruct the inputs from is not a receipt.
What must be pinned:
- Model-identity tuple -
{provider, model_id, version/build, quantization, runtime, endpoint}. A registry label like"qwen"is not an identity; a silent server-side model swap must be visible. - Sampling params -
{temperature, top_p, seed, max_tokens}, recorded as values.temperature=0assumed but unrecorded is not pinned. - Context manifest - ordered
[{path, content_sha256}], enough to reconstruct the exact prompt surface. - Cost actuals -
{prompt_tokens, completion_tokens, usd}, parsed from provider usage, not estimated. - Per-iteration check + commit - the check command, its exit code, and the commit it ran against. Per Invariant T2, the receipt must answer which oracle, at which commit, under which pinned model, having spent how much, with which guards intact.
- Computed
halt_reason- the halt reason is computed from observed state, never a flag. - Achieved determinism confidence - the lower bound from the confidence bound probe, not a pass/fail.
{ "run_id": "2026-06-25T10-00Z-fix-auth", "model_identity": { "provider": "anthropic", "model_id": "claude-opus-4", "version": "build-20260601", "quantization": null, "runtime": "api", "endpoint": "https://api.anthropic.com" }, "sampling": { "temperature": 0, "top_p": 1, "seed": 0, "max_tokens": 4096 }, "context_manifest": [{ "path": "pkg/auth/token.go", "content_sha256": "7e1..." }], "cost_actuals": { "prompt_tokens": 6120, "completion_tokens": 880, "usd": 0.41 }, "iterations": [ { "i": 7, "check_cmd": "go test ./pkg/auth", "exit_code": 0, "commit": "a1b2c3d", "guards_intact": true } ], "halt_reason": "success_condition_met", "determinism_confidence": 0.99}Replay verifies a verdict. Re-execute re-runs the loop.
Section titled “Replay verifies a verdict. Re-execute re-runs the loop.”These two verbs must never blur. Conflating them is the single most common overclaim in this space.
replay | reexecute | |
|---|---|---|
| Question answered | ”Does this receipt’s verdict still hold?" | "Is this outcome reproducible in distribution?” |
| What it runs | the deterministic check against the recorded end-state | the live agent loop, again |
| Agent | none | yes |
| Budget | free | burns budget; --confirm-gated |
| Determinism | deterministic | non-deterministic |
| Result | fingerprint match against the receipt | a statistical match, never byte identity |
| Verb | VERIFY | RE-RUN |
replay re-runs only the external check against the recorded commit and compares the fingerprint {exit_code, stdout_normalized, stderr_normalized} to the one in the receipt. No agent. No spend. It proves the verdict, offline.
reexecute runs the whole live loop again. The LLM samples, so the trajectory differs every time. It can report a distributional match, nothing more.
Resume revalidates before the first agent call
Section titled “Resume revalidates before the first agent call”Because progress lives on disk, a loop is resumable: the contract’s resume path can pick it back up without losing place. But on-disk state can go stale - the model behind an endpoint can change, and the test-determining surface can drift between runs.
So on resume, the contract requires the runtime to revalidate determinism_probe and metric_integrity before the first agent call, and to halt on drift rather than build on a stale anchor:
- The recorded
model_pinis re-resolved against the live model-identity tuple. A mismatch ismodel_drift_detected(class 19, liveness/drift). - The metric-integrity gate snapshot is re-checked against the immutable
t0baseline. A change in the collected-test set, assertion count, or protected-manifest hash halts before any new work lands. - Anything that cannot be resolved fails closed with
workspace_invalid(class 30), not a silent best-effort resume.
An unacked escalation state is treated as indistinguishable from a crash: resume blocks until a signed ack clears it, so a paged human is never bypassed.
Capability status
Section titled “Capability status”| Capability | Status |
|---|---|
init / status / check / step commands, stable exit codes | Shipped |
Durable schema-versioned .loop_state.json (SPEC section 8) | Shipped |
| Typed JSONL receipt with full pinning | Shipped (core) - live cost metering Planned |
probe-check confidence bound feeding determinism_probe | Shipped (core) - perturbation + in-loop monitor Planned |
replay (verify) / reexecute (re-run, --confirm-gated) | Shipped |
attest / ack signed receipts | Shipped |
| Metric-integrity gate revalidated on resume | Shipped (core) - assertion-count / manifest-hash / coverage-floor Planned |
State on disk is what makes the rest of the discipline possible. Pair it with determinism for a trustworthy check and with halt reasons for a computed stop.