Concepts
Determinism & the sandbox
Replay rebuilds a function’s state correctly only if replaying the same events produces the same execution. That property is determinism, and it is the single most important invariant in the system.
Most orchestrators enforce determinism by convention: a list of rules
(“don’t call Date.now(), don’t use Math.random(), don’t spawn
threads”) backed by linters and code review. Break a rule and you get a
production panic deep into a replay, sometimes months later.
Hopskip enforces determinism at the sandbox boundary. Workflow code cannot perform non-deterministic operations, because the runtime does not offer them.
The Wasm tier
Workflow and activity code compiles to Wasm. The host simply does not provide non-deterministic imports to workflow modules:
Date.now()links against a host import that returns logical event time: the HLC timestamp of the resuming event, sourced only from Core.Math.random()links against a PRNG seeded from the workflow ID, so the sequence is reproducible on replay.- An attempted
fetchinside workflow code fails at instantiation with a link error naming the missing import, rather than failing at runtime, deep in a replay.
There are no linters, no workflow.sideEffect escape hatches, and no
runtime panics from a rule you forgot. Code that would break replay fails
to link.
The clock rule
Workflow-visible time is logical event time, never wall-clock time.
Every time-touching feature (timers, deadlines, sleep()) routes through
the same logical clock, whose value is the HLC of the resuming event and
comes only from Core. Two replays of the same history see identical
timestamps because they read the same events.
// Inside a workflow, this returns logical event time (deterministic on
// replay), not the wall clock of whichever worker is running it.
const now = Date.now();
await sleep(3 * 24 * 60 * 60 * 1000); // a 3-day durable timer, not a busy wait
The quiescence invariant
Snapshots depend on a normative ABI rule: suspension is defined as the
workflow export returning to the host. At every await, the guest
returns control to the host with its native stack empty, so a snapshot is
exactly linear memory plus exported mutable globals. No stack walking,
no code transformation.
Guest SDKs uphold this by keeping all pending-execution state in heap structures, which JavaScript, Python, and Rust get from their own async machinery. Haskell’s suspend/resume boundary is an explicit, hand-written continuation type rather than the RTS scheduler, so it upholds the same invariant. Runtimes that cannot return-to-host naturally use an Asyncify-style fallback behind the same contract.
Analysis operates on the compiled module
Because determinism is a property of the compiled artifact, Hopskip needs no per-language AST tooling. All structural analysis operates on Wasm IR and WIT metadata, regardless of source language:
- Compatibility gating:
hop deploydiffs the new control-flow graph (whose edges are host calls) against the deployed one. Removing anawaitthat in-flight workflows may be suspended at becomes a deploy-time error, not a production panic. See Compatibility & versioning. - Instrumentation injection: heartbeats are injected as host calls at loop boundaries; source maps keep stack traces pointing at your native code.
Why this matters
Determinism by construction is what makes the rest of Hopskip possible. Transparent memory snapshots, millisecond replay, time-travel debugging, and production forking all rest on the guarantee that replaying a history is faithful. Enforce it at the boundary once, and every downstream feature inherits the safety.
Determinism is the product. Replay divergence, snapshot/replay divergence, or fuzz nondeterminism is a release blocker with no override path.