Trace Format
Opt-in handle checks add run.resources enabled=true and, on failure,
resource.leak node=<id> kind=<file|directory|listener|connection> handle=<id>
path=<escaped-path> events. Socket paths are -; file paths reflect current
model metadata. Events follow process order and deterministic handle-table
order. These additive events consume no random decisions. A successful
explicit checkpoint emits no events.
The trace is Marionette's human-readable explanatory artifact and a byte-identical replay check. The typed decision tape controls exact choice replay; the trace verifies and explains the resulting execution, so its bytes still need a small spec.
Text Format
Traces use newline-delimited UTF-8 text.
The first line is a header:
marionette.trace format=text version=3
Version 1 (2026-06-11) added io.random events: every Io.random /
Io.randomSecure draw in simulation records its length and a fixed-seed
Wyhash digest of the produced bytes, so replay divergence is visible at the
draw site without inflating traces by buffer size.
Version 2 (2026-08-02) adds the mandatory disk.model event emitted when a
simulated disk is composed. This makes the portable disk semantic contract
and sector size explicit before any disk operation is traced.
Version 3 (0.7) adds seed cutover context, uses scientific notation for float attributes, records watchdog result capacity, and removes loss draws for disabled network loss. Random-byte choices retain their existing length/digest trace shape and store full bytes separately in the tape. Old trace bytes are not claimed to match the new model; capsules reject unsupported trace versions.
Every later line is one event:
event=<u64> <component>.<action> <key>=<value> ...
Example:
marionette.trace format=text version=3
event=0 world.init seed=12648430 start_ns=0 tick_ns=1
event=1 run.name value=smoke
event=2 run.tag value=scenario:smoke
event=3 run.attribute key=packet_loss_percent value=uint:20
event=4 world.tick now_ns=1
event=5 world.random_u64 value=10121301305976376037
event=6 buggify hook=drop_packet rate=20/100 roll=73 fired=false
event=7 request.accepted id=42
Rules:
- Lines end with
\n. - Event indexes start at zero and increase by one for every
World.recordcall and every traced simulator helper. - Event indexes are global within one
World. - Component and action names use lowercase words separated by
_, with one dot between the component and action. The currentbuggifyevent name is a short special case. - Keys use lowercase words separated by
_. - Values must be non-empty stable text for the same Marionette version, Zig version, target platform, user code, options, and seed.
World.recordreturnserror.InvalidTracePayloadif a formatted event payload is ambiguous: no leading, trailing, or repeated spaces; every field after the event name must be exactlykey=value; keys may contain only lowercase ASCII, digits, and_; values may not contain space,=, newline, carriage return, tab, or\.World.recordFieldswrites the same event shape from structured fields. Text values are percent-encoded byte-by-byte for ambiguous bytes: space,=,%,\, ASCII control bytes, and non-ASCII bytes become%HH. Existing unambiguous ASCII such asscenario:smokeremains readable.- Run attributes encode the Marionette scalar type in the value text:
string:<escaped-text>,int:<i64>,uint:<u64>,bool:<true|false>, orfloat:<f64>. - BUGGIFY events use
buggify hook=<comptime-tag> rate=<numerator>/<denominator> roll=<value> fired=<bool>. - Allocation authority events use
allocation.alloc,allocation.resize,allocation.remap, andallocation.freewithop, length,align,status,reason,roll,live_bytes, andsuccessful_allocationsfields. They never contain addresses. - Cooperative cancellation records
scheduler.cancel_request task=<u64>when a request is armed andscheduler.cancel_deliver task=<u64>whenerror.Canceledis delivered at a cancellation point. - A proven scheduler deadlock records
scheduler.wait_state waits=<task>:<key>[@<deadline>],...in task order, followed byscheduler.deadlock tasks=<u64> completed=<u64> blocked=<u64>. - Watchdog-enabled runs record
run.watchdogwith the configured stall, total-run, and trace-capacity bounds. If the host worker must be terminated, the preserved trace ends withwatchdog.non_yielding task=<u64-or-main>orwatchdog.livelock task=<u64>. Host timestamps are never recorded. These optional and failure-only vocabulary additions do not change the text envelope, so the trace format remains version 2. - A non-empty seed schedule records one
world.seed_cutover_configevent per ordered cutover during world initialization, withindex,at_ns,microstep, andseedfields. When a cutover takes effect,world.seed_cutoverrecords its requestedat_ns/microstep, the actualapplied_ns/applied_microstepnext random-call boundary, and the newseed. These optional events use the existing event grammar, so the text envelope remains version 2. - Time-evolved simulator faults are preceded by
fault_evolution.boundary now_ns=<u64>. Seeded scheduling draws and any network or process state transitions caused at that timestamp follow the boundary record. - Unstable network events use
network.send,network.drop, andnetwork.deliverwith stable packet ids and node ids. - Node-state changes use
network.node. - Link-filter changes use
network.link,network.partition, andnetwork.heal. Link-only healing usesnetwork.heal_links. - Path-clog changes use
network.clog,network.unclog, andnetwork.unclog_all. - The scheduler-backed stream adapter records
io.net.deliverfor delivered framed bytes andio.net.delivery_errorwhen a delivery-time topology fault is translated into a stream error. - Logical process lifecycle records use
process.kill node=<u64> reason=<literal>andprocess.restart node=<u64>. - Simulated disks begin with
disk.model contract=portable_v1 version=1 sector_size=<u64> torn_write=sector_prefix reorder=crash_global_reverse lifecycle=commit_pending. A disk crash publishes all fault choices, per-write and per-metadata classifications, its summary, and process-kill records transactionally. If any trace allocation fails, none of that crash sequence remains visible. - The one-shot liveness transition records
liveness.transition core_count=<u64>followed by the zeroed-rate events and, when a network is configured,network.liveness_restorewithcore_count,restored_links,cleared_clogs, andrevived_nodesfields.
What Goes In
Record data that explains simulator decisions and user-visible simulated behavior:
- Seed and simulation options.
- Time movement.
- Random choices that affect behavior.
- Scheduler decisions.
- Disk and network fault decisions.
- User service events that help explain a failure.
- Invariant failures and liveness failures.
What Stays Out
Do not record:
- Pointer addresses.
- Stack or heap addresses.
- OS thread ids.
- Wall-clock timestamps.
- Hash map iteration order unless sorted first.
- Raw unordered container dumps.
- Host file descriptors.
- Platform-specific error strings when a stable code is available.
Stability Policy
Trace bytes are guaranteed only within the full determinism contract: same Marionette version, Zig version, target platform, user code, simulation options, initial seed, and seed schedule.
If Marionette changes the trace layout, it must bump the trace format version. If Zig's formatter changes output for a value, that is outside the cross-version trace guarantee, but Marionette should avoid relying on ambiguous formatting in core simulator events.