Determinism guarantees
Worlds' pitch is trustworthy replay: the same request script against the same seed produces byte-identical world state, every time. This page says exactly what is guaranteed, how it's achieved, and where the boundaries are. Everything here is enforced by tests (packages/server/test/determinism.test.ts) and a source-level lint (pnpm check:determinism-sources).
The guarantee
Replaying the same sequence of twin + admin operations against two worlds created from the same seed yields:
- byte-identical dumps —
GET /admin/worlds/:id/dumpis a canonical-JSON export (sorted keys, stable ordering) of all world-visible state: objects, events, idempotency records, counters, marks, webhook delivery ledger, clock, and counters; - identical event logs — same events, same
evt_ids, same order, same payloads; - identical
before-images in a diff —GET /admin/worlds/:id/diff?from=<mark>carries, for every updated or deleted object, the object as it stood at the mark, and two replays produce the same images.
This holds across processes, machines, and reruns. world reset restores a world to exactly its fresh-create state — the ids it will mint next, the request log's numbering, and each API key's use counter (so a scenario that expires a key after N requests fires at the same request after a reset). That equivalence is a guarantee, not a precondition: a reset waits (up to 5 s) for twin requests already in flight to finish, admits new ones only once it is done — they then run entirely in the reset world — and fences off a webhook delivery that was mid-flight, which stops when its send returns instead of marking the reset world's identical row delivered. The reset world shows no trace of pre-reset work. The one request that can outlive the wait is one still in flight when it expires — the whole time in flight counts: in practice a latency_ms rule near or above the bound, or a request queued behind a long clock advance; it is refused with a 500 api_error that names the reset, is not executed, and is not logged. Keys, admin tokens and the webhook endpoint survive a reset; nothing else does.
How
IDs. Every world-visible id (cus_…, in_…, re_…, evt_…) is prefix_ + base62 of sha256(seed_hash + ":" + world_counter). No RNG state, no clock: the nth id minted in any world with that seed is always the same string.
Logical clock. Integer unix seconds, starting at the seed's epoch. It advances only via the admin API (clock advance) or, in clock_mode: "auto", by a fixed tick per twin request. The auto tick is the first thing inside the world lock, so a request the pipeline turns away before the lock — an authentication failure, an injected scenario response, an unmatched route, a parameter the refused-parameter table rejects (scope, "Refused parameters") — does not tick, while a request a handler's own validation rejects does: two kinds of validation 400, one tick between them, an asymmetry recorded rather than changed (DECISIONS, Determinism). A deterministic script sees the same ticks on every replay either way. The tick is one transaction, like an admin advance: a renewal the twin cannot bill — a negative invoice total whose credit would land in a currency other than the customer's balance (scope, semantics simplifications) — rolls the tick back, clock included, and the request answers a logged api_error 500 instead of reaching its handler; the admin route answers 409. No request builds such a renewal (the doors, the pin sites and a non-negative unit_amount on prices, scope); should one meet state written outside the API, in auto mode every twin request then answers that 500 until the world is reset — the tick comes before any handler — while in manual mode a positive line in the invoice's currency or cancel_at_period_end=true on the subscription frees the next advance. Date.now() never leaks into world-visible state — a CI grep (scripts/check-determinism-sources.sh) forbids wall-clock and ambient-randomness APIs everywhere in the server except the one module that mints harness identity.
Single logical writer. All handlers for a world run serialized (a per-world lock around synchronous SQLite transactions), so there is exactly one order of state changes. Different worlds share nothing but the process: each has its own file, lock and counters, so their requests interleave freely on the server's one Node event loop — isolation, not CPU parallelism (one process, one JavaScript thread, SQLite work synchronous inside it). The bench (pnpm bench) proves 100 identically- and concurrently-driven worlds converge on one dump hash; its timing figures are per-operation times on a quiet machine — a sequential p95 and a batch's wall time divided by its size — not a per-request p95 under concurrent load, which nobody has measured (build log).
Events & webhooks. Events are numbered by a world sequence; webhook deliveries dispatch in event-creation order. Retry backoff is 60s × 2^(attempt−1) in logical time — a failed delivery retries when the world clock passes that point, not when a wall-clock timer fires. Scenario chaos (drop/reorder) is driven by counters in world state. The event payload's created is the logical clock whichever signature_time the endpoint uses; only the Stripe-Signature header's t= differs (see Boundaries). Pass the tolerance to your verifier: stripe-node's constructEvent(payload, header, secret, tolerance) fourth argument, stripe-python's Webhook.construct_event(..., tolerance=...). With auto_deliver on, delivery runs outside the world lock and after the response — a fire-and-forget drain after each twin request and clock advance — so when a delivery lands relative to the agent's next request is a wall-clock race. A run that grades on the delivery ledger or on the listener's side effects either sets auto_deliver: false and flushes explicitly (POST …/webhooks/flush), or waits for quiescence — no request in flight and a flush that returns an empty batch — before it marks or diffs; a qualifying run (program plan O1) does one of the two. Delivery is at-least-once across a server restart: a drain that is mid-send when the process exits (SIGTERM, server.close()) never writes its row, the row stays pending with its attempts unchanged, and the first drain after the restart sends it again — a listener that must not act twice deduplicates on the evt_ id, as with Stripe. A dropped delivery (drop_every_nth) counts its attempt like a sent one: the ledger row and the flush result both say attempt 1.
Time travel. Advancing the clock N days generates every elapsed subscription renewal as it would have happened: invoices, charges, and events timestamped at each period boundary, in subscription creation order — at most 1000 periods per subscription per advance (MAX_RENEWALS_PER_ADVANCE). The response's renewals_pending counts the subscriptions still due after that cap, and the next advance (or, in clock_mode: "auto", the next request's tick) continues exactly where this one stopped; a subscription whose current_period_end is still behind the clock is the visible sign of a backlog. The cap is a bound on one transaction, not a limit on time travel: two advances bill the same history as one.
Scenarios. Failure rules count matched requests in world-state counters — the 4th request hits the storm in every replay.
Idempotency. Keyed responses (status + exact body) are stored in the world DB and replayed verbatim: every outcome the endpoint produced — errors and 500s included — is replayed. Validation failures (a missing, malformed or out-of-range parameter) never reach the endpoint and store nothing.
Before-images. Every update or delete archives the row it replaces (object_versions: the data, and the sequence range it was current for), so a diff can hand back each changed object's image at the mark. The archive is a function of the request sequence, not of anything the harness snapshots; because it is derivable from that sequence it is not part of the dump — a determinism test compares the before blocks of two replays instead. The file grows by about one copy of each replaced object per update and nothing compacts it: worlds are disposable, and a reset empties the archive with the rest of the history.
Boundaries (deliberate)
- World ids, API keys, and admin tokens are random and excluded from dumps: they are harness-level identity, minted in
core/nondeterministic.ts, and never appear inside Stripe objects. Two worlds from one seed differ only there. - Wall-clock latency (
latency_msrules, real webhook HTTP round-trips) affects when responses arrive, never what state results. - The
Stripe-Signaturetimestamp is delivery time by default (signature_time: "wall"on the webhook endpoint), so stock verifiers — stripe-node'sconstructEvent, stripe-python'sconstruct_event— accept a Worlds delivery with their default 5-minute tolerance. It is the transport envelope, not world state: minted incore/nondeterministic.ts, never stored, not in the dump.signature_time: "logical"signs with the world clock instead, for harnesses that compare headers across replays; those pass a large tolerance to their verifier. The event inside carries logical time either way. - Webhook delivery outcomes depend on your listener: if it responds identically across replays (the normal case in tests), the delivery ledger is identical too.
- Background vs. flushed delivery: with
auto_deliveron, which internal flush performs an attempt can vary, but attempt order, payloads, and final statuses do not. Harnesses that assert on flush batches setauto_deliver: falseand drive delivery viaPOST .../webhooks/flush— or wait for quiescence before marking; and across a restart, delivery is at-least-once (above). - Your request script must itself be deterministic — if you send
Date.now()in a param, that's on you (it will faithfully differ).
Checking it yourself
pnpm test # includes the byte-identical replay test
pnpm bench # 100 parallel worlds → 1 dump hash
pnpm check:determinism-sources # no Date.now()/Math.random() in the server