octopus-replay
面向受治理事件系统的确定性测试框架,力求让每一次事故都可逐字节复现。
面向受治理事件系统的确定性测试框架,力求让每一次事故都可逐字节复现。
自述文件
镜像自该项目 GitHub 上的 README。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:
- Record once from a real incident (or a hand-built scenario) with
record, andserializeRecordingit to a*.jsonfixture committed to the repo. - Verify on every change — either
octopus-replay verify recording.json --handler ./handler.jsin CI, orverifyDeterminism(recording, handler)in anode --testfile that assertsresult.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:
| Member | On record | On replay |
|---|---|---|
ctx.now() | reads the live/injected clock, tapes the value | returns the next taped timestamp |
ctx.random() | reads the live/injected RNG, tapes the value | returns the next taped value |
ctx.respond(key, produce) | runs produce(), tapes its result under key | returns the taped result — produce is not called |
ctx.seq | 0-based index of the current event | same |
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 atstartand advances bystepms per call.sequenceClock(times)— returns each timestamp intimesin 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/.ndjsonlog, a.jsonarray of events, or a scenario object.--seeddrivesctx.random()deterministically;--clock-start/--clock-stepdrivectx.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
- Harness —
record(scenario, handler, opts?),replay(recording, handler),toScenario(recording). - Verify —
verifyDeterminism(recording, handler)→{ ok, expectedHash, actualHash, divergence? },diffTranscripts,diffRecordings,formatDivergence. - Sources —
fromJsonl,fromEvents,fromWebhookFixtures. - Determinism —
seededRandom,steppingClock,sequenceClock. - Hashing —
stableStringify,canonicalHash,canonicalEqual,cloneJson. - Format —
serializeRecording,parseRecording,assertRecording,RECORDING_VERSION. - Errors —
DeterminismError,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.
主题
- determinism
- event-sourcing
- reproducibility
- governance
相关研究方向
- 证据、审计与回放
- 模型评估
