Decision Tapes And Replay Capsules
Seeds and seed schedules explore executions. A decision tape records the actual choices. A replay capsule persists that tape together with the information needed to verify an execution later.
Compatibility Contract
Seed schedules reset the PRNG at (sim_time_ns, microstep) positions. They are
same-build controls: adding an earlier draw can move later positions. Making a
seed schedule durable would require recording the choices it produced, which
is the tape's job.
Capsules are durable for a pinned build and workload, not promises that an old execution survives arbitrary application or simulator changes. They retain build/SUT identities, Zig version, target, optimization mode, disk semantic version, initial seed and cutovers, runner options, simulation options, trace, error/check identity, and exact decisions. Replay rejects incompatible identities, unsupported versions, malformed traces/decisions, and incomplete tapes. It then checks every choice boundary and the complete trace and outcome.
The caller must supply meaningful build and sut identities, retain the
matching executable/source/dependencies, and provide the matching initializer,
scenario, checks, and application input. Include the input/workload identity in
sut; capsules do not serialize arbitrary application state or closures.
Changing code requires a new identity and exploration, not relabeling the old
artifact. Cross-version migration and reduction are future work.
Entry Contract
Each Decision contains a semantic site_id, simulated logical_time_ns, a
zero-based microstep at that timestamp, the preceding trace event, typed
alternatives, and a selected value. Alternatives are any u64, boolean,
bounded unsigned integer, or an exact byte count. Byte decisions own the
complete byte buffer; their scalar selection is a deterministic digest.
Site IDs use lowercase ASCII words, digits, and _, separated by single .
characters. The decision contract and capsule envelope are independently
versioned at version 1. The execution payload is shared with watchdog transport.
Scheduler, network, disk, allocation, automatic process events, named world
choices, legacy traced World.random* calls, and std.Io application random
bytes participate. Workload/BUGGIFY choices participate when they use those
controlled authorities. Ambient host randomness is outside the contract.
Runner And Ownership
runSimCase records the first execution, exact-replays its tape on the second,
and compares traces and failure identities. The first mismatch produces
RunFailureKind.replay_diverged with an owned replay_divergence, identifying
the tape index and expected/actual boundaries. Mismatches survive rollback and
remain fatal even if application code catches the immediate error.
RunResult and RunFailure own the first decision_tape; takeDecisionTape
transfers it. Otherwise call RunReport.deinit to release all owned storage.
Completed watchdog workers publish the same owned execution payload and use
exact replay. Killed or prematurely exited workers retain only a trace prefix,
set tape_complete = false, and use same-seed replay. Timeouts require matching
failure identities and compatible trace prefixes; worker crashes require exact
traces. Incomplete reports cannot become capsules.
Save And Replay
const identity: mar.ReplayIdentity = .{
.build = "simulator-and-application-build-digest",
.sut = "sut-revision-and-workload-input-digest",
};
const simulate: mar.World.SimulateOptions = .{};
var report = try mar.runSimCase(.{
.allocator = allocator,
.simulate = simulate,
.init = App.init,
.scenario = App.scenario,
});
defer report.deinit();
const bytes = try mar.ReplayCapsule.encode(allocator, &report, identity);
defer allocator.free(bytes);
// Persist bytes using the harness's caller-provided host std.Io.
// The report retains the simulation options actually used.
var capsule = try mar.ReplayCapsule.decode(allocator, bytes);
defer capsule.deinit();
var replayed = try mar.replaySimCase(.{
.allocator = allocator,
.init = App.init,
.scenario = App.scenario,
// Supply the original checks too, when present.
}, &capsule, identity);
defer replayed.deinit();
Encoding supports passing runs and reproducible failures. It rejects divergent
reports (UnreproducibleRun) and incomplete tapes (IncompleteDecisionTape).
Nonfinite float metadata is encoded as tagged JSON strings (inf, -inf,
nan) and round-trips alongside finite values. The codec never performs host I/O. decode owns its parsed storage, while the
returned replay report owns its own storage and outlives the capsule.
Direct World Use
var recording = try mar.World.init(allocator, .{ .seed = 1234 });
defer recording.deinit();
_ = try recording.chooseIntLessThan("scheduler.select", usize, runnable_count);
var tape = try recording.cloneDecisionTape(allocator);
defer tape.deinit();
var replaying = try mar.World.init(allocator, .{
.seed = 9999,
.decisions = .{ .replay = tape.entries },
});
defer replaying.deinit();
_ = try replaying.chooseIntLessThan("scheduler.select", usize, runnable_count);
try replaying.finishDecisionReplay();
World choices stage PRNG, cutover, trace, and tape changes transactionally. An allocation failure retries the same value and microstep. Fatal replay diagnostics are deliberately outside that rollback boundary. Direct tape replay can override a different generated seed value; full capsule replay also verifies configuration and trace, and therefore retains the original seed and schedule.