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.
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.
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.
Date.now(), new Date(), Math.random() in a planA 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.)
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:
Clock — the curator takes time as an injected Clock (epoch ms), "never Date.now()/new Date() — the engine's plan VM forbids them and they break deterministic replay" (curator/types.ts:26-29). A test passes a fixed clock and the active→stale→archived transitions become deterministic (Lesson 9).monotonicIdFactory rather than calling a global. A test passes a counter; production passes the monotonic one. Same shape, deterministic in test (Lesson 10).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.
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.
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.Date.now(), new Date(), and Math.random() in an alembic.plan.ts. The core reason is:Clock port — never a global — so the same telemetry + the same clock always yields the same lifecycle decision (Lesson 9).Clock — fully usable, just controllable. A CLI command can read the clock; a plan describing a run cannot.