Course / Lesson 28  ·  PT-BR
Lesson 28 · Advanced · deepens invariant 3

Determinism & replay: the same input, the same run

Invariant 3 (Lesson 16) says a run is content-addressed and replayable. This lesson is the mechanism: a run's id is a hash of its spec, so the same spec always lands in the same directory; that directory is an append-only event log plus a checkpoint, so a crashed run resumes; and the plan VM bans the three functions that would break all of this — Date.now(), new Date(), and Math.random(). Where real time and real randomness are genuinely needed, they enter through an injected seam — a Clock and an id factory — never a global. That single discipline is what makes "replay this exact run" possible.

Content-addressed run IDs

A run's identity is not a timestamp or a UUID — it's the content hash of its spec. runIdFor(spec) hashes the run's specification, so changing any spec field yields a new id and therefore a new directory:

// packages/swarm/src/orchestrator.ts:168 — the run id is its spec's hash
const runId = runIdFor(spec);   // SHA-256-style content hash of the spec
// change any field of `spec` ⇒ a different runId ⇒ a different run directory
// (packages/swarm/src/types.ts:247-257)

The payoff: identity is content. Two runs of the same goal+plan+contract share a directory and can resume each other; a tweaked spec is unambiguously a different run. There's no clock-derived id that would make "the same run" un-findable tomorrow.

The deterministic run directory

Each run lives at a fixed, predictable path with an append-only log and a checkpoint — the substrate for resume and replay (Lesson 16):

// packages/etl/src/run-directory.ts:52-59 — the layout
<baseDir>/runs/<runId>/
  ├── events.jsonl     // append-only; every event in order
  ├── checkpoint.json  // latest resumable state
  └── meta.json        // goal/plan/contract fingerprint (validated on --resume)

And the content-addressed stores reinforce it: results are written by SHA-256 over canonical (key-sorted) JSON, so "re-appending identical content is a no-op and re-runs converge" (etl/stores.ts:79-97). Idempotence is structural — running twice can't duplicate, and a resumed run picks up exactly where the log left off.

specgoal+plan+contract runIdFor(spec)SHA-256 hash runs/<runId>/ events.jsonl (append-only) checkpoint.json replay / resumere-read the log

The ban: no Date.now(), new Date(), Math.random() in a plan

A plan module (alembic.plan.ts) is the deterministic description of what to run. If a plan could read the wall clock or roll dice, two evaluations of the same plan would diverge — and replay would be a lie. So the plan VM rejects all three at evaluation (complete-map §7.8; CLAUDE.md "Determinism"):

// alembic.plan.ts — these throw at plan-VM evaluation:
const id = Date.now();        // ✗ rejected — wall clock
const t  = new Date();         // ✗ rejected — wall clock
const r  = Math.random();      // ✗ rejected — nondeterminism

The error is named in the troubleshooting guide: "Non-determinism error — remove Date.now(), new Date(), Math.random() from the plan module." The VM doesn't trust you to remember; it enforces it. (Note: this ban is on the plan module, not on application code — a CLI command may read the clock freely; the plan describing a run may not.)

The escape hatch: inject the clock and the id factory

Real systems do need real time (a curator deciding "stale after 30 days") and unique ids (a clarify question needs a handle). The answer is not to forbid them — it's to make them an injected seam, so production passes the real one and a test passes a fake. You've already seen both:

The principle: nondeterminism is a dependency, so inject it

Time and randomness are side effects, exactly like the filesystem. The engine's second invariant — pure kernel, injected side-effects — applies to them too. Banning the globals in the plan VM and threading a Clock/id factory everywhere else is one idea wearing two hats: the only nondeterminism in a run enters through a seam you control. Control the seam and you control replay. The curator and clarify subsystems store thresholds in milliseconds precisely so the injected Clock is the single source of time.

Why this is the bedrock of replay

Put it together. Content-addressed ids mean "the same run" is findable. The append-only log + checkpoint mean a run can be re-read and resumed. The plan-VM ban means re-evaluating the plan yields the identical structure. The injected Clock/id factory mean the residual nondeterminism is captured and replayable. Remove any one and replay breaks: a clock-derived id would un-find the run; a random branch in the plan would re-run differently; a global Date.now() in the curator would make "stale" depend on when you replayed. The discipline is holistic — which is why the VM enforces the easy-to-forget part automatically.

1. Why is a run's id derived from a hash of its spec rather than a timestamp or UUID?
Correct: c. runIdFor(spec) content-addresses the run. A timestamp id would make "the same run" un-findable tomorrow and would change on every invocation; a content hash makes re-runs converge to the same directory and makes a tweaked spec a distinct run.
2. The plan VM rejects Date.now(), new Date(), and Math.random() in an alembic.plan.ts. The core reason is:
Correct: b. A plan is the deterministic description of a run. Wall-clock or random values would yield different structures on re-evaluation, breaking content-addressed replay. The VM enforces what a human would otherwise forget — "remove them from the plan module" is the named error.
3. The curator genuinely needs "now" to decide staleness. How does it get time without breaking determinism?
Correct: d. Time is a side effect, so it's injected like any other. The curator stores thresholds in milliseconds and reads "now" from the Clock port — never a global — so the same telemetry + the same clock always yields the same lifecycle decision (Lesson 9).

Common confusions

"The ban means Alembic can never use the current time." It bans the globals in the plan module, where nondeterminism would break replay. Everywhere else, time enters through an injected Clock — fully usable, just controllable. A CLI command can read the clock; a plan describing a run cannot.
"Content-addressed ids are just for dedup." Dedup is one benefit (re-appending identical content is a no-op), but the deeper purpose is replay: the id is the spec's fingerprint, so resume/replay can find and re-run exactly the same run. Identity and idempotence fall out of the same hash.