Docs/Replay

One input boundary. Every mode.

Live, Record, Replay, and Scenario drive the same computation through canonical application-owned inputs. Replay is evidence, not a second implementation of the domain.

Canonical inputCheckpointsState hashes

Admission is the determinism boundary

The application owns the meaning and encoding of an observation. Once admitted, runD can give it stable identity, order, recording, and replay. Native wall-clock arrival and operating-system ready-list order remain outside that boundary.

host observation → canonical admission → deterministic runtime state

Mode behavior is exact

ModeInput originSource callsFallback
LiveApplication sourceExactly once per rowNot applicable
RecordApplication source + retained recordExactly once per rowNot applicable
ReplayRetained canonical recordZeroNone
ScenarioRetained Record + validated Choice patchesZeroNone
Fail closed Replay and Scenario do not call the live source, read the network, or synthesize a default value when canonical evidence is missing.

A checked Record → Replay path

replay.cpp · checked installed example
#include <rund/replay.hpp>

#include <array>
#include <cstddef>
#include <cstdint>

namespace {

int Replay(rund::Session &session) {
  std::byte produced = std::byte{7};
  std::uint32_t source_calls = 0u;
  bool simulation_ok = false;
  rund::replay::Binding replay{};
  auto source = [&](rund::replay::Writer &writer) -> std::uint64_t {
    ++source_calls;
    const std::array bytes{produced};
    (void)writer.append(bytes);
    return 1u;
  };
  const auto commands =
      replay.input(rund::replay::Input{.id = 1u, .schema = 1u}, source);
  auto simulate = [&](rund::replay::Context &context) {
    const auto value = commands.read(context);
    simulation_ok = value && value.sequence() == 1u && value.size() == 1u &&
                    value.bytes()[0] == std::byte{7};
  };

  const auto recorded = rund::replay::record(session, simulate);
  if (!recorded) {
    return recorded.exit_code();
  }
  produced = std::byte{255};
  simulation_ok = false;
  const auto replayed = rund::replay::run(session, recorded, simulate);
  if (!replayed) {
    return replayed.exit_code();
  }
  return simulation_ok && source_calls == 1u ? 0 : 2;
}

} // namespace

int main() {
  rund::Session session{};
  const auto opened = session.open(rund::SessionConfig{.workers = 1u});
  if (!opened) {
    return opened.exit_code();
  }

  const int operation = Replay(session);
  const auto closed = session.close();
  if (operation != 0 && operation != 2) {
    return operation;
  }
  return closed ? operation : closed.exit_code();
}

The source is called once while recording. After the source value changes, strict Replay still returns the recorded byte and makes zero additional source calls.

Scenario changes input, not the simulation path

A Choice belongs to the bound Channel, so id, schema, sequence, and replacement bytes cannot drift apart. Scenario validates and freezes all choices before restore or simulation.

Scenario choice · contextual fragment
std::array replacement{std::byte{0x2a}};
std::array choices{commands.choice(sequence, replacement)};

auto changed =
    rund::replay::scenario(session, baseline, choices, simulate);
if (!changed) return changed.exit_code();

Duplicate, missing, ambiguous, invalid, or over-capacity rows fail without invoking the callback. Inspect matches() only after a successful Scenario.

Checkpoints make divergence local

A stateful Binding owns one checkpoint schema and one borrowed restore codec. Checkpoint state is copied into immutable storage; continuation commits its start/state lineage so an unrelated Record cannot be spliced into the chain.

input identity
  → retained Record
  → checkpoint state hash + lineage
  → continued Record
  → next checkpoint
checkpoint and bounded History · contextual fragment
auto checkpoint = replay.checkpoint(baseline, state_bytes);
if (!checkpoint) return checkpoint.exit_code();

auto resume = replay.resume(checkpoint);
if (!resume) return resume.exit_code();
auto continued = resume.record(session, simulate);
if (!continued) return continued.exit_code();

rund::replay::History history{rund::replay::Retention{
    .max_segments = 64,
    .max_bytes = 16 * 1024 * 1024,
    .max_events = 4096,
}};
if (!history) return history.exit_code();
auto added = replay.append(history, std::move(continued), state_bytes);
if (!added) return added.exit_code();

History stores a bounded ring of immutable Record/Checkpoint segments. Segment count, retained bytes, and evidence rows are all explicit limits; eviction preserves a contiguous suffix and does not mutate copied Segment values.

Inspect the first divergent boundary

check performs strict equality. diff reports typed differences, and window exposes bounded context around the first observation, host-event, input, transcript, or trace mismatch. Save/load APIs enforce explicit limits before publishing immutable evidence.

The computation remains singular Replay supplies canonical input to the same application callback and explicit Compute target. It does not replace backend execution with a host approximation or fallback.

Continue to Runtime for Session shutdown and telemetry, or inspect the optional checked Scenario, checkpoint, and History examples ↗.

Next boundarySee which release tuples carry this claim