Skip to content

Determinism contract for workflow code (SES Compartment policy) #756

Description

@kriszyp

Part of #752 (Durable Execution v0.1 epic).

Problem

Replay safety requires the deterministic portion of a workflow function to produce the same control flow on every replay. If a workflow calls `Date.now()` directly, or reads `Math.random()`, or makes an unfettered `fetch`, replay produces different control flow and the engine cannot reason about which steps to skip.

Engines in this category (Temporal, DBOS, Restate) settle on mechanical enforcement of determinism rather than convention + warnings — the failure mode of accidental non-determinism is too easy to hit and too hard to debug after the fact.

Proposed shape

Workflow `run` functions execute inside an SES Compartment with non-deterministic globals shadowed or blocked:

Capability Behavior in workflow context
`Date.now`, `Date()` Returns the deterministic clock — the timestamp recorded in the engine's replay log, not wall clock
`Math.random` Returns from a deterministic PRNG seeded per workflow / step
`setTimeout`, `setInterval` Replaced by `ctx.sleep` — direct use throws at runtime
Free `fetch` / `http` / `net` / `fs` Blocked — must go through `ctx.step`
`crypto.randomUUID`, `crypto.getRandomValues` Shadowed with a deterministic source seeded per step
Globals (`globalThis.*`) Frozen per Compartment policy

The only way to get external data or side effects is through `ctx.step` (or `ctx.atomic`), which intercepts the call, checkpoints the result, and returns the cached value on replay.

Existing substrate

SES Compartment support is already wired up in `security/jsLoader.ts`. This issue is the policy layer on top of it — the specific endowments, the shimmed clock/PRNG, the runtime interception of forbidden calls, and the developer-error surface.

Developer error surface

Mechanical enforcement is brittle if the error message is opaque. Concrete asks:

  • Calling `Date.now()` should not silently return the deterministic clock — it should use it, but the runtime should also log a one-time warning to a developer-visible diagnostic stream so accidental non-determinism is caught in development.
  • Calling `fetch` directly should throw a typed `WorkflowDeterminismError` with a message pointing at the surrounding step and the suggested wrap ("wrap in `ctx.step('fetch-foo', () => fetch(...))`").
  • Replay-time mismatch (a step's recorded input hash != current input hash) should produce a distinct `WorkflowReplayMismatchError` with the diff.

Where it runs

Most of this is developable against the existing SES integration without core changes. The exception is the step-interception fast path: at hot-loop scale, the `ctx.step` runtime check (replay vs. execute, idempotency-key generation, checkpoint commit) benefits from being in the workflow runtime directly rather than in userland JS. Calling that out as the one core-adjacent piece of this issue.

Open questions

  • Mechanical enforcement or convention-plus-warnings? Proposal recommends mechanical; this issue assumes that — but it's worth a real conversation with whoever ends up owning the developer experience.
  • Should the deterministic clock be the engine's logical clock (monotonic per workflow) or the wall-clock-at-replay-source-time (the original timestamp)? Different engines pick differently; Temporal uses replay-source wall time.
  • Per-step or per-workflow PRNG seeding? Per-step is safer but slightly noisier.

🤖 Filed by Claude on behalf of @kriszyp

Activity

  1. kriszyp commented on May 23, 2026

    @kriszyp
    MemberAuthor

    I am inclined to believe that we actually want to do more convention-based determinism, rather than this ticket, but will keep for now.

  2. added theissue type on Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    Priority

    P2

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions