Randomness
Canwu draws random numbers from named, versioned random streams. Each stream belongs to the mechanic that declared it, and every draw is recorded. The same run inputs therefore always produce the same results, and a reload cannot reroll an outcome. Read this page before you add randomness to a plugin.
A draw, from request to replay
Section titled “A draw, from request to replay”View diagram source
flowchart LR
System["Boundary system<br/>declares the stream"] --> Draw["random_range or<br/>random_sample_for_operation"]
Draw --> Record["RandomDrawRecord<br/>stream, address, bound,<br/>value, purpose"]
Record --> Commit{"Boundary<br/>commits?"}
Commit -- "yes" --> Saved["Snapshot and<br/>boundary evidence"]
Commit -- "no" --> Undo["Draws and stream<br/>positions roll back"]
Saved --> Replay["Replay recomputes<br/>each draw and compares"]
Stream identity
Section titled “Stream identity”A RandomStreamKey has a namespace, a name, and a version, for example:
canwu.core / knowledge-report-delay / 1The kernel derives each stream’s seed from the run’s root seed and the stream key. Each mechanic draws from its own stream, so adding a draw to one system leaves every other system’s numbers unchanged. The stream state stores the algorithm, the derived seed, the position, and the generator state.
New streams use SplitMix64V2, which reduces a raw value to the requested range with unbiased rejection sampling. Streams that recorded SplitMix64V1 keep its modulo reduction, so old journals replay with their original values. The algorithm is part of the saved format: to change how a stream draws, publish a new stream version.
Declare before drawing
Section titled “Declare before drawing”A boundary system may draw only from streams it lists in BoundarySystemContract::random_streams. When the system runs, the kernel gives it a random session holding copies of those streams. A draw from any other stream fails with UndeclaredRandomStream.
Every draw needs:
- an upper bound greater than zero;
- a
purposestring that is non-empty and has no leading or trailing whitespace; - for a sequential draw, room to advance the stream position.
The system receives a bounded integer; the generator state stays inside the kernel.
Operation-keyed random draws
Section titled “Operation-keyed random draws”A sequential draw, SimulationView::random_range, takes the next value from the stream. That is enough when a system draws in a fixed order. A mechanic that may retry, roll back, or continue across phases should use an operation-keyed draw instead: random_sample_for_operation, random_range_for_operation, which returns only the value, or random_draw_for_operation, which also returns the ID of the draw’s evidence.
An operation-keyed draw is addressed by the operation it serves. Its address combines the producing plugin, an operation kind, the application’s operation ID, a target, and a draw slot. The target is a RandomOperationTarget: an entity, an exact decision ticket version, an exact domain record version, a knowledge holder, or a canonical key. The draw also cites the evidence that caused it. The kernel computes the value from the root seed, the stream, the address, the bound, and the purpose, so the result is the same however many draws came before. Asking again with the same address, evidence, bound, and purpose returns the same value. Reusing an address with different evidence, bound, or purpose fails with RandomOperationConflict.
Random decision policy uses this path. RandomOperationTarget::DecisionTicket binds the draw to an exact ticket version, DecisionOptionWeight maps the drawn value to one of the ticket’s existing options, and the ResolveDecisionRandomly directive makes the kernel generate an ordinary decision for the next boundary. A guarded utility policy’s random tie-break uses the same path and draws only among the near-equal top options.
A controller’s policy kind (Utility, Rule, Random, Human, External, or Llm) is fixed when the controller is registered; registering the same controller ID again fails. Random decision policy only chooses among a ticket’s options. Weather, breakdowns, epidemics, and similar world events belong in domain systems that draw from their own streams.
Cite the draw you computed from
Section titled “Cite the draw you computed from”SimulationView::random_draw_for_operation takes the same arguments as random_sample_for_operation and returns a RandomOperationDraw { draw_id, sample }. The draw_id is the RandomDrawId of the draw’s evidence, so a record or event your system writes can cite the exact draw it was computed from.
- For an address drawn at an earlier boundary,
draw_idis the ID of that draw, whether its record is still retained or has been sealed. - For a fresh draw,
draw_idis the ID the draw’s record gets when the boundary commits. A boundary that fails commits neither the draw nor anything your system proposed, so an ID is only ever saved together with the draw it names. Exact replay hands out the same IDs.
A draw made in the current boundary becomes retained evidence only when that boundary commits. In the same boundary you can write its ID into a record payload or an event, and name it among a record’s identity-evidence dependencies. Until the next boundary, though, evidence_exists is false for it, the positions the engine checks as evidence (knowledge origins, evaluation-trace terms, and person provenance) refuse it, and it cannot be the operation evidence of another operation-keyed draw.
Retries after evidence is sealed
Section titled “Retries after evidence is sealed”An exact retry, with the same address, evidence reference, bound, and purpose, returns the recorded draw whatever has become of the evidence it cites. A run that sealed its evidence with seal_evidence therefore answers a retry exactly as its unsealed replay does. A request that differs from the recorded draw fails with RandomOperationConflict.
Only a fresh draw needs its operation evidence to exist at the reader’s cut: retained evidence, or evidence produced earlier in the same boundary. If your system may draw an address for the first time after a seal, cite evidence that survives one, such as the initial-scenario version of a record or evidence newer than the seal. When a fresh draw is refused with InvalidRandomOperationEvidence, return the error so that the boundary fails. A handler that swallows the error settles a boundary that the run’s own exact replay rejects, because the replay still holds the evidence and makes the draw.
What a keyed draw costs later
Section titled “What a keyed draw costs later”The engine answers operation-keyed requests from the keyed-draw ledger, the record of every address the run has drawn and not yet retired, retained or sealed. It keeps the ledger in memory as a persistent map, drops what retires at or before a boundary’s time when the boundary starts, extends it when the boundary commits, restores it when a boundary fails, shares it with a fork and a seal, and rebuilds it once on restore; a continuation checkpoint carries the draws not retired at its time. The ledger is never hashed, and keyed draws are no longer evidence dependencies, so the work of settling a boundary, taking a checkpoint or a paged checkpoint, sealing, and forking does not grow with the draws the run has already made.
One cost remains. The ledger holds one entry for each address the run has drawn (about 522 bytes each in the recorded measurement, and more with longer stream names, operation IDs, and targets) until the draw retires, and for as long as the run lives when its stream declares no keyed-draw retirement.
The boundary system that owns a stream declares one in BoundarySystemContract::keyed_draw_retirement, a KeyedDrawRetirement { stream, after }. It is part of the plugin descriptor, so a run is restored and replayed only with the same declaration. A draw of that stream retires at its boundary time plus after, and the first boundary at or after that instant drops it before any system runs. A later request with the same address is a fresh draw under a new draw ID: its evidence is checked as a fresh draw’s, its value is the same for the same bound and purpose, and a conflict across the retirement is no longer detected. Draw records stay in the draw journal as evidence. A restore from the draw journal applies the same rule draw by draw, so a journal may draw one address again after it retired, and drawing it again before is still refused; a continuation checkpoint lists only the draws not retired at its time, and a restore refuses one that lists a retired draw. Choose after no shorter than the time anything retries the address or cites the draw.
A fresh draw must cite seal-stable operation evidence: evidence produced earlier in the same boundary, an event, command, attempt, ingress record, boundary, or draw that the journal counters prove exists (sealed or not), or a record’s committed current version or initial-scenario version. A retained version that is no longer current is refused with InvalidRandomOperationEvidence in every run, so a sealed run and its exact replay accept the same first draws.
Draw evidence
Section titled “Draw evidence”Every draw records a RandomDrawRecord:
| Field | Meaning |
|---|---|
stream and address |
The versioned stream, plus a sequential position or an operation address |
upper_exclusive and value |
The requested bound and the returned value |
purpose |
Why the mechanic drew |
producer |
The boundary system or core system that drew |
outcome |
What the draw decided, for example a decision selection or a report delivery |
cause and correlation_id |
The causal source and the processing chain |
Draw records and stream state are saved in snapshots and boundary evidence, and are included in hashes. When a boundary fails, its stream positions and draw records roll back with it, so a failed attempt consumes no randomness.
Reloading, alternative realities, and repeated trials
Section titled “Reloading, alternative realities, and repeated trials”Deterministic replay removes the easy reroll. The same authoritative state, command or decision input, plugin semantic environment, and simulation time produce the same draw. Reloading a snapshot from before an event and replaying the same inputs therefore gives the event the same outcome.
To try a different command from the same snapshot, fork() the run and submit it there. The result is an alternative reality: a new causal history, distinct from an exact replay of the original run. The host application’s save policy decides whether players get manual saves, single-writer saves, or research forks.
Randomness in first-party extensions
Section titled “Randomness in first-party extensions”Where first-party crates use randomness, and where they deliberately don’t:
canwu-movementmakes no draws. An application system decides that a leg failed, a route closed, or someone seized custody, usually with its own operation-keyed draw. It reports the result through themovement_incident_v1ingress, citing an exact evidence record.canwu-correspondencedraws on its own stream,canwu-correspondence / operation-resolution / 1. For an incident request such as an interception, a disaster, or a carrier seizure, the application supplies aprobability_per_mille, and the plugin makes one operation-keyed draw to decide whether the incident happens. The same stream selects communication opportunities and their recipients.- Whether a forgery is detected, behind an authenticity finding, is decided by the application;
canwu-informationrecords the finding. - Capacity booking allocation follows a fixed order: priority, window start, tie-break key, admission sequence, and booking identity.
- Tie-breaks in unit-block law stages are rules. When as many blocks vote
ForasAgainst,status-quoadds no block, andcasting-seat:<seat>adds oneForblock if that seat itself votedFor.
Guidance for plugin authors
Section titled “Guidance for plugin authors”- Give each independent mechanic its own stable, named stream.
- Write the reason for the draw in
purpose, so debug and replay output can explain the result. - Review stream declarations together with the plugin version and semantic hash.
- Do not use wall time, thread order, or container iteration order as a seed or a tie-break.
- Model unknown facts, actor choices, discrete incidents, and aggregate rates as separate mechanisms; do not fill in missing facts with random draws.
Replay contract
Section titled “Replay contract”Exact replay requires the stream identities, algorithm versions, root seed, sequential positions, operation addresses, every draw, and every final outcome to match. When loading a snapshot, the kernel checks each sequential stream’s state against its seed advanced by the recorded number of draws, and recomputes every operation-keyed value. Boundary replay also compares each draw’s producer, purpose, cause, and correlation.