Skip to main content

What the journal records

A run’s journal is the ordered list of everything that crossed the sandbox’s walls: Each entry carries its callsite, the line of the script that caused it, which is how the dashboard puts each call beside its line. Values (arguments, results, signals, the input and output) are identified by the SHA-256 of their canonical JSON; values above 64 KiB are stored apart, in S3.
The journal is the only source of truth. The dashboard, the OpenTelemetry traces, the metrics and the webhooks are projections of it. No feature has a side channel.

Replay

What runs inside the sandbox is computed again; what came from outside is read from the journal. To rebuild a run, a worker starts the script from the beginning with the run’s input and seed. Each await that has a recorded outcome returns it immediately. When the script reaches the end of the journal, it continues live. This is how a run survives a crash, a deploy, or ten days of sleep: nothing but the journal is needed. Workers keep recently active runs in memory, so most activations need no replay at all; replay is what makes losing that memory harmless. It also means that any run can be re-executed, anywhere, with the same result:
  • rewind replay <run> re-executes a run locally and checks that it matches its journal.
  • rewind replay <run> --script fixed.ts tells whether a corrected script is compatible with that history, and if not, where it diverges.

Forks: fix a run without repeating its effects

A fork is a new run started from an existing one with a corrected script. Rewind replays the new script against the parent’s history: while its calls match the parent’s, the recorded results are reused and no tool is called again. At the first difference, or at the end of the parent’s history, the fork continues live. An agent fixes step 7 of a failed run without redoing the side effects of steps 1 to 6. The model does it with the agent tool retry_run; your code with runs.fork.
A fork keeps its parent’s seed, input, toolset and namespace, and has its own id and TTL.

Cassettes: tests from real runs

A run’s calls and signals, exported as a cassette, become the mocks of a test. A test against a cassette answers each tool call with what the real tool answered, in virtual time, without calling anything. See Testing.

Logical time

Inside a script, Date.now() returns the time of the last event delivered to the run: its start, a tool’s result, a timer, a signal. Time only moves when the outside world speaks, and it moves identically on replay. After await sleep("10d"), Date.now() is at least ten days after the start. In tests (mock mode), time is virtual: the clock jumps straight to the next timer, so a ten-day workflow runs in milliseconds.

Determinism epochs

A replay only works if the script computes exactly what it computed the first time. The script cannot be non-deterministic: the sandbox controls time, randomness and every input. What can change is Rewind itself: a new V8, a new version of the rewind library, a new sandbox global. These components are versioned together by one number, the determinism epoch, recorded in each run. A build of Rewind keeps the versions of every epoch that may still have live runs, and CI replays a corpus of recorded journals for each one. If a replay ever diverges from its journal, that is a fault of the platform, never of the script. The run is not failed: it is quarantined, and an operator releases it after a fix, or gives up on it. Quarantined runs appear on the Overview of the dashboard.

How a run ends

A run never ends while a tool call is in flight: a forgotten await still lets the call finish and records its outcome. So when a run ends by itself, every call it made has a recorded outcome, which is what lets a fork know which effects happened.