Skip to content
Octopus Research Institute
EcosystemApache-2.0TypeScript

octopus-replay

A determinism harness for governed event systems, aiming to make every incident byte-for-byte reproducible.

A determinism harness for governed event systems, aiming to make every incident byte-for-byte reproducible.

Our research relates to this project; it is ecosystem engineering, not an Institute research output unless marked otherwise.

Readme

Mirrored from the project's README on GitHub.

English | 简体中文

Replay

Make every incident byte-for-byte reproducible. A determinism harness for governed event streams — it freezes the clock, randomness, and external IO so a handler replays to a byte-identical transcript.

Part of Octopus Core — the open infrastructure stack for governed AI. One job per repo, along the agent lifecycle: Scout · Observe · Experience · Blackboard · Workstate · Runtime · Replay — with Inspect governing every stage. The whole stack rides one root primitive, Evidence — the canonical, tamper-evident atom that is the root category every stage is expressed in.

This repo — Replay · Verify: Reproduce every agent incident byte-for-byte.

Scenario (logs / fixtures) → record → Recording ── replay ──▶ Transcript
                                          └────── verify ─────▶ pass / divergence

Capture a golden recording from a real incident — the events plus every value the handler read from the clock, the RNG, and the network. Re-run it in CI and any change that alters governed behavior fails with an exact pointer to the first output that differs. Same recording, same handler → the same transcript hash, on any machine, forever.

Boundaries

Replay is a determinism harness, not a JSONL reader. The log adapter is a convenience; the substance is that it freezes every source of nondeterminism. A handler that reads the clock, randomness, and external IO only through the ReplayContext replays exactly. A handler that reaches for Date.now(), Math.random(), or a live network call directly escapes the harness and will diverge — that discipline is the whole point.

Replay does not patch globals, intercept fetch, or sandbox your code. It does not judge whether an output is correct — only whether it is reproducible. It hosts a handler; it is not that handler's dependency.

It validates the governed cores (Observe / Runtime / Blackboard) by replaying incidents through a handler that wraps them — but it has zero dependency on any of them. The boundary is the Handler signature and the JSON event shape, not any package SDK. Replay is testing and proof infrastructure.

It has zero third-party dependencies: its only runtime dependency is the first-party octopus-evidence primitive (itself zero-dependency), which provides the canonical hashing the whole stack shares. The repo is otherwise fully usable on its own.

Install & build

npm install
npm run typecheck   # tsc --noEmit
npm test            # node --test
npm run build       # emit dist/
npm run example     # capture a recording and verify it (see below)

Requires Node ≥ 22.

Quickstart

A handler is a deterministic function of an event and its context:

import type { Handler } from "octopus-replay";

// A tiny governed handler: stamp a time, make a policy roll, do one IO call —
// each source of nondeterminism read only through `ctx`.
const handler: Handler = async (event, ctx) => {
  const receivedAt = ctx.now();               // frozen clock
  const sampled = ctx.random() < 0.2;         // frozen RNG (20% sampled)
  const balance = await ctx.respond(          // frozen IO: taped on record,
    "account.balance",                         // replayed on replay
    () => 1000 - (event as { amount: number }).amount,
  );
  return { receivedAt, sampled, approved: balance >= 0, balance };
};

Build a scenario, capture a golden recording, then verify:

import {
  fromEvents,
  record,
  seededRandom,
  steppingClock,
  verifyDeterminism,
} from "octopus-replay";

const scenario = fromEvents([{ amount: 300 }, { amount: 1000 }, { amount: 50 }], {
  id: "incident-4211",
  note: "three charges captured from production",
});

// Capture a golden recording under deterministic sources.
const recording = await record(scenario, handler, {
  sources: { clock: steppingClock(Date.parse("2026-07-03T00:00:00Z"), 1000), random: seededRandom(99) },
});

// Same handler → byte-for-byte reproducible.
const good = await verifyDeterminism(recording, handler);
good.ok; // true — actualHash === expectedHash

When the behavior drifts, the same call catches it with a pointer to the first divergence:

// A regression: someone flips the approval boundary to `> 0`.
const changed: Handler = async (event, ctx) => {
  const receivedAt = ctx.now();
  const sampled = ctx.random() < 0.2;
  const balance = await ctx.respond("account.balance", () => 1000 - (event as { amount: number }).amount);
  return { receivedAt, sampled, approved: balance > 0, balance }; // >= 0 became > 0
};

const bad = await verifyDeterminism(recording, changed);
bad.ok;                    // false
bad.divergence?.seq;       // 1 — the { amount: 1000 } charge, balance 0
bad.divergence?.expected;  // { …, approved: true,  … }
bad.divergence?.actual;    // { …, approved: false, … }

This is examples/replay-incident.ts end to end; run it with npm run example.

Use it in CI

Commit a recording as a fixture and let CI catch behavior drift:

  1. Record once from a real incident (or a hand-built scenario) with record, and serializeRecording it to a *.json fixture committed to the repo.
  2. Verify on every change — either octopus-replay verify recording.json --handler ./handler.js in CI, or verifyDeterminism(recording, handler) in a node --test file that asserts result.ok.

Any change that alters a governed output makes verify fail with the exact event and the expected/actual values — a regression test you got for free the moment you captured the incident.

Bare vs. frozen scenarios

A bare scenario is events only — the source adapters (fromEvents, fromJsonl, fromWebhookFixtures) return these. record runs it against live sources (Date.now / Math.random by default, or the sources you inject) and tapes every value the handler consumes, producing a fully-frozen recording.

A frozen scenario already carries its clock, random, and responses tapes (e.g. toScenario(recording)). record re-materializes it from its own tapes — no live source is touched. replay always runs frozen: it is pure, with no live clock, RNG, or network.

The ReplayContext contract

Everything nondeterministic a handler needs must come through the context:

MemberOn recordOn replay
ctx.now()reads the live/injected clock, tapes the valuereturns the next taped timestamp
ctx.random()reads the live/injected RNG, tapes the valuereturns the next taped value
ctx.respond(key, produce)runs produce(), tapes its result under keyreturns the taped result — produce is not called
ctx.seq0-based index of the current eventsame

On replay, produce never runs — so an IO call that fails, mutates, or is slow in production is a no-op during replay. If a handler over-reads a tape (calls now()/random() more often than recorded, or respond with an exhausted key), the frozen context throws a DeterminismError, which verifyDeterminism surfaces as a divergence rather than throwing. That is the harness telling you behavior drifted in a way the recording can't reproduce.

Source adapters

Turn captured production data into a bare scenario:

import { fromJsonl, fromEvents, fromWebhookFixtures } from "octopus-replay";

// NDJSON / JSONL event log (one event per line):
const a = fromJsonl(logText, { select: (line) => (line as { event: unknown }).event });

// A plain array of events:
const b = fromEvents([{ amount: 300 }, { amount: 50 }]);

// Captured webhook deliveries — ordered deterministically by `receivedAt`:
const c = fromWebhookFixtures(fixtures, { wrap: true }); // wrap → { id?, headers?, payload }

Determinism helpers

For driving record reproducibly (in tests, examples, and the CLI):

  • seededRandom(seed) — a mulberry32 PRNG; the same seed yields the same sequence, independent of the platform RNG.
  • steppingClock(start, step = 1000) — a clock that starts at start and advances by step ms per call.
  • sequenceClock(times) — returns each timestamp in times in turn, and throws when exhausted so a test that over-reads fails loudly.

Canonical hashing

"Byte-for-byte" is defined against a canonical form, so key order, whitespace, and number formatting never affect equality:

  • stableStringify(value) — deterministic JSON with object keys sorted at every depth. Rejects what JSON can't faithfully round-trip: non-finite numbers, undefined, functions, and cycles — a "successful" hash never hides silent data loss.
  • canonicalHash(value) — the SHA-256 of that encoding (the transcript hash).
  • canonicalEqual(a, b) — deep equality via the same encoding, so a diff can never disagree with a hash mismatch.
  • cloneJson(value) — a deep clone that also asserts the value is canonicalizable JSON.

Recording format

A recording serializes to pretty-printed JSON (RECORDING_VERSION = "1"), fit to commit as a fixture:

{
  "meta": { "recordingVersion": "1", "id": "incident-4211", "createdAt": "2026-07-03T00:00:00.000Z", "note": "…" },
  "events": [ { "seq": 0, "event": { "amount": 300 } }, … ],
  "clock":  [ 1751500800000, 1751500801000, 1751500802000 ],   // ctx.now()  values, in call order
  "random": [ 0.63…, 0.11…, 0.94… ],                            // ctx.random() values, in call order
  "responses": { "account.balance": [ 700, 0, 950 ] },          // ctx.respond() results, per key, in order
  "transcript": {
    "entries": [ { "seq": 0, "output": { "approved": true, … } }, … ],
    "hash": "a1b2c3…"                                            // SHA-256 over the canonical entries
  }
}

serializeRecording / parseRecording / assertRecording write, read, and validate this shape; a malformed file (bad version, wrong field types) throws a RecordingFormatError.

CLI

octopus-replay record <scenario> --handler <mod> [-o out.json] [--seed N] [--clock-start MS] [--clock-step MS]
octopus-replay verify <recording.json> --handler <mod>
octopus-replay run    <recording.json> --handler <mod>
octopus-replay diff   <a.json> <b.json>
  • record — run the handler over a scenario and print (or -o) a golden recording. Scenario input is a .jsonl/.ndjson log, a .json array of events, or a scenario object. --seed drives ctx.random() deterministically; --clock-start/--clock-step drive ctx.now().
  • verify — replay a recording and check it still matches; prints or the first divergence.
  • run — replay a recording and print the resulting transcript JSON.
  • diff — compare two recordings' transcripts.

The --handler <mod> module default-exports the (event, ctx) => output function (a handler named export also works). Relative/absolute paths resolve from the cwd; bare specifiers resolve as node modules.

Exit codes: 0 ok · 1 divergence / verify failed · 2 usage or IO error.

API surface

  • Harnessrecord(scenario, handler, opts?), replay(recording, handler), toScenario(recording).
  • VerifyverifyDeterminism(recording, handler){ ok, expectedHash, actualHash, divergence? }, diffTranscripts, diffRecordings, formatDivergence.
  • SourcesfromJsonl, fromEvents, fromWebhookFixtures.
  • DeterminismseededRandom, steppingClock, sequenceClock.
  • HashingstableStringify, canonicalHash, canonicalEqual, cloneJson.
  • FormatserializeRecording, parseRecording, assertRecording, RECORDING_VERSION.
  • ErrorsDeterminismError, RecordingFormatError.

Design

The authoritative architecture and contracts live in docs/DESIGN.md — the determinism-harness thesis, the Scenario → record → Recording → replay/verify pipeline, the ReplayContext discipline, and the recording format. Read it before making changes; code is written against that spec.

License

Apache-2.0 © Octoryn.

Topics

  • determinism
  • event-sourcing
  • reproducibility
  • governance

Related research areas

  • Evidence, Audit and Replay
  • Model Evaluation
All open source