Skip to content

Save, replay, and fork

Use this page to save and load a run, keep compact history for a long-running simulation, replay a run exactly, or fork from a saved point to try different inputs.

A Canwu save holds more than world data. It also carries the commands, events, boundary records, random draws, plugin semantic environment, and state commitments that let a load verify the state. The plugin semantic environment is the name, version, and semantic hash of every plugin in the run; loading and replay need a plugin set that matches it.

Persistence paths: a live run can be saved as a snapshot or checkpoint journal and restored with the same plugins, replayed exactly from its replay journal, or forked into an alternative reality. View diagram source.
View diagram source
flowchart LR
  Run["Live run"] -->|"snapshot_json()"| Snap["Snapshot"]
  Run -->|"checkpoint_journal_json()"| CJ["Checkpoint + later journal"]
  Run -->|"replay_journal()"| Journal["ReplayJournal"]
  Run -->|"continuation_checkpoint()"| Cont["Continuation checkpoint"]
  Run -->|"fork()"| Fork["Fork: new inputs, alternative reality"]
  Snap -->|"from_snapshot_json_with_plugins()"| Restored["Restored run"]
  CJ -->|"from_checkpoint_journal_json_with_plugins()"| Restored
  Cont -->|"CompactedCanwu::from_continuation_checkpoint_verified()"| Continued["Continued run (compact)"]
  Journal -->|"replay_from_journal()"| Replayed["Replayed run"]
  Plugins["Matching plugin set"] -.-> Restored
  Plugins -.-> Continued
  Plugins -.-> Replayed

A restored, continued, or replayed run has the same checkpoint_hash() as the original. A fork starts from the same state and then diverges.

Goal Save with Load with Your application’s job
Normal save or service restart snapshot_json() Canwu::from_snapshot_json_with_plugins() Store the whole snapshot; pass the same plugin set when loading
Compact storage for a long run checkpoint_journal_json() Canwu::from_checkpoint_journal_json_with_plugins() Store the checkpoint and the unbroken journal after it
A save that does not grow with the campaign, with no replay continuation_checkpoint() CompactedCanwu::from_continuation_checkpoint_verified() Store the checkpoint and its continuation_hash; see continuation checkpoints
Incremental saves that store only changed state prepare_paged_checkpoint() Canwu::from_paged_checkpoint_and_journal_with_plugins() Keep every state page and the unbroken evidence prefix; see paged checkpoints
Audit or exact reconstruction replay_journal() Canwu::replay_from_journal() or replay_from_journal_json() Store the whole journal and keep a matching plugin set
Send effects to outside systems outbox_entries() — Deliver at least once, deduplicate by delivery_id, and store acknowledgement state
Try different inputs from one point fork() — Treat the fork as a new alternative reality
use canwu_api::Canwu;
use canwu_reference_world::{ReferenceWorldPlugin, demo_scenario};
let (scenario, _) = demo_scenario()?;
let plugin = ReferenceWorldPlugin;
let canwu = Canwu::new_with_plugins(35, scenario, &[&plugin])?;
let json = canwu.snapshot_json()?;
let restored = Canwu::from_snapshot_json_with_plugins(&json, &[&plugin])?;
assert_eq!(restored.checkpoint_hash(), canwu.checkpoint_hash());

Loading validates the snapshot, including its format, engine version, and plugin descriptors, and fails if anything does not match. Once a save contains plugin domain records, pass the same plugins to load it.

The starter example runs this together with forking and exact replay:

终端窗口
cargo run -p canwu-reference-world --example starter

A checkpoint journal (CheckpointJournal) bundles a validated checkpoint with the unbroken evidence that follows it. It suits long runs because it starts from the checkpoint and carries only the evidence recorded after it.

Exact replay rebuilds the same run from the journal’s initial scenario, inputs, and recorded evidence:

let checkpoint_journal = canwu.checkpoint_journal_json()?;
let from_checkpoint =
Canwu::from_checkpoint_journal_json_with_plugins(&checkpoint_journal, &[&plugin])?;
assert_eq!(from_checkpoint.checkpoint_hash(), canwu.checkpoint_hash());
let journal = canwu.replay_journal();
let replayed = Canwu::replay_from_journal(&[&plugin], &journal)?;
assert_eq!(replayed.checkpoint_hash(), canwu.checkpoint_hash());

Rules for exact replay:

  • The scenario comes from the journal; replay_from_journal() takes only the plugins and the journal.
  • Run identity, engine version, plugin semantic environment, and recorded evidence must all match.
  • Replay reuses recorded outcomes. Decisions, random draws, and external answers are read from the journal, and side effects are not sent again.

fork() copies the run at its current point into an independent simulation. Use it to compare another command sequence, policy, or research assumption. The fork advances through the normal command and time APIs, and once its inputs differ it is a new alternative reality, separate from exact replay of the original.

A fork is also how you save without pausing the game. Canwu, CompactedCanwu, and every fork are Send (Sync is not promised), and a fork shares the run’s archive provider, so take fork() on the simulation thread, move the fork to a worker, and write the checkpoint journal, continuation checkpoint, or sealed segments there while the simulation thread keeps settling the original; the worker writes the same bytes the original thread would. Four rules keep this safe:

  • A save-only fork never loads from your archive provider. checkpoint_journal_json, continuation_checkpoint, and seal_evidence read only the run’s own state and evidence. A fork you also advance, restore, or validate on the worker does load, at the same time as the original, so attach a provider that can serve both.
  • One answer per object. Your provider or store must give every run the same answer for the same (namespace, object_id), whatever the order or concurrency of the calls, and block while it is busy (for example while another thread writes) instead of returning None or an error, which a boundary would take for a missing or unreadable object. A store a run holds is passed as Arc<dyn … + Send + Sync>; keep its maps behind a Mutex or RwLock.
  • Keep what a live fork reads. Deleting the lookup pages an archive commit superseded gives up every earlier point of the run that reads them, including a fork whose head precedes the commit; see archiving terminal records. While a fork is live on a worker, do not delete pages or package archive objects it still reads.
  • A fork’s seal is the fork’s. Sealing a fork leaves the original’s evidence and seal history alone, and the segments the fork returns belong to the fork’s save, not to the original’s segment chain.

fork() itself is constant-time: it copies no state and no journal. The first append on either side while both still hold the shared journals copies them (about 53 ms at 65,536 records on a full-history run), so a save fork that is still alive at the original’s next boundary moves that copy onto the original’s boundary, where the fork paid it in earlier releases; finish or drop the save fork before the next boundary when that matters, or seal, since a sealed run copies only the tail since its last seal.

Boundary emissions meant for outside systems appear in outbox_entries(). Each OutboxEntry has a stable delivery_id derived from the run, boundary, event, and emission index.

  1. Store each delivery_id with its delivery and acknowledgement state in your own storage.
  2. Deliver at least once, so a timeout or restart can retry safely.
  3. Deduplicate by delivery_id on the receiving side; a network call may happen more than once.
  4. Exact replay regenerates the same delivery_id values and leaves delivery to you.

In compact mode (CompactedCanwu), seal_evidence() returns the sealed EvidenceJournalSegment. Keep that segment with your delivery and acknowledgement state. Sealing or compacting evidence does not mark an effect as delivered or acknowledged. A seal no longer waits for an empty ingress queue: it moves events, commands, and attempts up to the admission cursors and the other journals to their end, and keeps in memory the evidence that work still in flight may read (pending ingress and their causes, payload-required dependencies, and the unadmitted tail), so a busy run seals as often as it likes. After a seal ingress_log holds only the retained tail, but pending_ingress() still lists every queued record. A seal fails with ArchiveNotReady, and changes nothing, only when a plugin declared a dependency on evidence the run no longer holds.

A continuation checkpoint (ContinuationCheckpoint) is a save from which a run continues without its evidence history. It holds the current state, the evidence that work in flight may still read, the derived indexes a restore cannot rebuild without history (the draws of the keyed-draw ledger not retired at its time, the request index that keeps every tracked request idempotent, the time index of sealed evidence, and the boundary ledger), and resumable forms of the journal commitments. It holds no boundary, event, command, ingress, or draw records that nothing in flight can read, so its size follows the current state and the number of tracked requests and keyed draws, not the length of the campaign: in the recorded continuation-profile run, 2.2 to 5.7 MB at month 14 against a 150 to 160 MB full save, written in 4 to 9 percent of the time and restored in 1 to 3 percent.

let (checkpoint, continuation_hash) = canwu.continuation_checkpoint()?;
// store both; the hash is the trust anchor
let continued = CompactedCanwu::from_continuation_checkpoint_verified(
checkpoint,
&continuation_hash,
&[&plugin],
)?;
assert_eq!(continued.checkpoint_hash(), canwu.checkpoint_hash());
  • Write. Canwu::continuation_checkpoint() and CompactedCanwu::continuation_checkpoint() return the checkpoint and its continuation_hash. Writing changes nothing in the run. The checkpoint is byte-identical at the same cut whether the run never sealed, sealed at any cuts, or was itself restored from a continuation checkpoint. It fails with ArchiveNotReady exactly when a seal would.
  • Restore. CompactedCanwu::from_continuation_checkpoint_verified(checkpoint, expected_hash, plugins) compares the recomputed hash with the one you stored, then checks the formats, integrity, shape, state, keyed draws (their values, and that none has retired), identity (every state root and the checkpoint hash), and that the carried evidence is exactly what the restored state needs, before it rehydrates the plugins. The _with_plugins and JSON forms skip the stored-hash comparison and detect corruption only; use the verified form for anything you did not write yourself in the same process. The result is a CompactedCanwu, never a Canwu.
  • What a continued run can do. Settle boundaries, admit and cancel ingress, process commands and decisions, seal, fork, write further continuation checkpoints, and answer every host read that does not need history, with the same results as the uninterrupted run. It can also list the ingress still queued, including what was queued before the save, with pending_ingress (trusted host only: select your own items, for example those with no cause, before showing anything to a player). CompactedCanwu::continuation_base() reports the base cut.
  • What it cannot do. Produce a full snapshot, a replay journal, a checkpoint journal, or a paged checkpoint (ErrorCode::HistoryUnavailable); read archived segments before its base; or see boundaries, events, and traces before its base through the retained-evidence reads. A host that needs exact replay or audit keeps a full save as well.
  • Trust. The checkpoint holds ground truth, like a snapshot; never give it to player, agent, observer, or remote clients. Its own continuation_hash field detects corruption, not tampering: the carried evidence, the pending set, and the settled cut are covered only by the hash you stored.
  • Host helpers. Every extension helper that enqueues or reads for the host takes any CanwuRun, the trait Canwu and CompactedCanwu implement, so an archive-backed or continued run is driven through the same calls as a full-history run. Module restore wrappers that replay history (technology, production, society, historical research) have no continuation form. Technology state can still be checked on a continued run: call validate_technology_runtime_on, which takes either run handle (see technology).

Plugins that keep a rule’s evidence readable across a seal declare it payload-required in a current record; a plugin must not declare an initial-scenario record version that way (it stays readable without a declaration, and the engine refuses the declaration at the seal).

Paged checkpoints, StatePageProvider, decision-history archives, and plugin archive participants matter only once snapshot size or hot-history growth becomes a real problem. At that point your application must:

  • store each state page under its content identifier and return the same bytes; a missing page must fail with an error, never read as empty;
  • keep the unbroken journal prefix and the archive-retention roots that each checkpoint needs;
  • keep the lookup pages of the resource, production, fiscal, and reference package archives for every archive head that a restore, an exact replay, or a fork can pass through (see archiving terminal records);
  • restore the same archive contracts and semantic hashes for every plugin; and
  • check reachability from committed roots before deleting cold data.

A paged checkpoint is the content-addressed form of the current-state checkpoint. It is stored as state pages in a StatePageProvider, and each new checkpoint adds only the pages that changed. Since 0.13.2, you can write and restore paged checkpoints through the public API:

  • Write. Canwu::prepare_paged_checkpoint(source, provider) captures current state as a PreparedPagedSimulationCheckpoint. Its delta leaves out every page the provider already holds; source is the earlier checkpoint, if there is one. store_and_verify stores the delta. Preparing changes nothing in the run: the state revision, the checkpoint hash, and every journal stay the same.
  • Restore. Canwu::from_paged_checkpoint_and_journal(checkpoint, provider, segments, budget) and from_paged_checkpoint_and_journal_with_plugins rebuild the run from the provider and the evidence prefix: the unbroken journal segments from the start of the run up to the checkpoint. Collect them as the run goes with journal_segment_since. Each segment must contain at least one new record, so skip any segment whose start equals its end. A restore runs the same validation as loading a snapshot. Only the pages the restore reads must be in the provider, and a missing one fails with StatePageUnavailable instead of reading as empty. Of the archived decision history, the restore reads only the pages that hot decision state depends on; other archived history reports Unresolved until decision_history_location_with_provider looks it up through a DecisionArchiveProvider that serves the same pages.
  • Compact mode. CompactedCanwu offers the same calls. Its evidence prefix is the segments you have sealed, in order, followed by what journal_segment_since returns from the end of the last one. Skip that final segment when nothing was recorded after the seal. After a compact restore the prefix changes; see Restart a compact run.

The checkpoint carries no evidence records. Its size follows current state rather than the length of the run, apart from compact-mode continuation data such as one archived-segment header per seal, and each delta holds only the pages that changed. Preparing can still read retained evidence, and a restore still validates the whole evidence prefix. Domain-record pages are binary (raw-canonical-v2): a record longer than 1 MiB is paged in content-addressed 1 MiB record-value chunks, so an unchanged chunk is never stored twice, and a record of up to 512 MiB (MAX_PAGED_RECORD_BYTES) can be paged; a longer one makes prepare_paged_checkpoint fail with ValueOutOfRange, which is not a corrupt page. The knowledge ledger is paged the same way, as two content-addressed maps (bodies by ID, holder ledgers by holder, one leaf each, so an unchanged body or holder keeps its page), and the compact checkpoint body and the hot decision state are 1 MiB chunk pages, so no part of a paged checkpoint is limited to one page: a world with 4,000 report holders and a 9.8 MB ledger saves as about 16,000 pages, prepared in about 90 ms and restored in about 250 ms, and a day’s delta is about 2,300 pages.

Every paged restore takes a restore budget as its last argument. PagedRestoreBudget { max_assembled_bytes } bounds the bytes the restore assembles from pages: each record value, knowledge body, holder ledger, the compact body, and the hot decision state is charged before it is allocated, and a value past the budget fails with RestoreBudgetExceeded before any chunk of it is read, restoring nothing; a descriptor that states more than the budget is refused before any page is read. The descriptor records the exact charge of an honest restore in PagedSimulationCheckpoint::assembled_bytes, and a restore refuses a descriptor whose count differs from what its pages assemble, so for a checkpoint you wrote pass PagedRestoreBudget::new(checkpoint.assembled_bytes). PagedRestoreBudget::DEFAULT (2 GiB) is a ceiling for your own larger checkpoints. The budget bounds assembled bytes, not heap: a restore holds the assembled values, the state decoded from them, and validation copies, and decoded JSON can take many times its text, so hostile pages inside a 2 GiB budget can still peak at tens of GiB. For pages from a source you do not trust, pass a budget near the size you expect (the assembled_bytes of the checkpoints you write, with headroom) and authenticate the root page ID against one you recorded; DEFAULT is not that bound. Any entry point of your own that wraps a paged restore, such as a page-store adapter, takes and passes on the budget.

Keep these caveats in mind:

  • Keep the earlier checkpoint’s pages in the provider. When a run restored from a paged checkpoint prepares a new one, its delta also leaves out the archived decision pages the run never loaded, so its provider must already hold every page of the checkpoint it was restored from. store_and_verify checks only the new pages and the root; use StatePageRetentionLedger::prepare and verify to check a checkpoint’s complete page set.
  • A compact restore needs the _with_plugins form whenever the checkpoint names a plugin.
  • A paged root is not a state identity, because it also commits to which evidence is sealed. Any restore from sealed segments, through Canwu or CompactedCanwu, keeps the original checkpoint hash but gets a different paged root, and a later seal does not bring the old root back. A run that has never sealed evidence restores to the same paged root. Compare checkpoint hashes, not paged roots.

A compact restore, whether from a paged checkpoint or through CompactedCanwu::from_checkpoint_and_journal and its _with_plugins form, keeps the whole prefix you supplied as retained evidence. That changes which segments later restores need:

  1. Until the restored run seals again, a checkpoint of it uses the prefix you restored it from, followed by what journal_segment_since returns from that prefix’s end.
  2. Its next seal_evidence() returns one segment that starts at the beginning of the run and replaces every segment sealed before it.
  3. From then on, the prefix starts with that new segment, followed by what journal_segment_since returns from its end.
  4. Keep the earlier segments only for checkpoints taken before that seal.

A prefix that still lists the earlier segments in front of the new one is not contiguous, so restoring from it fails.

The repository’s versioning and persistence contract lists format numbers, paged-storage contracts, and archive invariants.

Canwu 0.23.0 is a minor release that changes no engine format number. The technology and fiscal plugins change their semantic hashes, so a run that registers either one could not load a 0.22.1 save even if the engine versions matched. Each technology record now also lists the bodies that the versions it cites rely on, so the same operations write larger records, and a record near the 16 KiB technology payload limit can now exceed it and be rejected. A new operation whose exact citation names a superseded version that no current technology record relies on is now also rejected. With these changes, a run restored from a continuation checkpoint, or a sealed run, decides later technology operations exactly as the uninterrupted run does; check such a run with validate_technology_runtime_on (see technology). The fiscal plugin, now version 0.6.0-experimental, gains a command that admits several offices’ batches at one boundary and settles them all at the next (batch sets), and one evidence record can now serve several scopes (aggregated execution evidence). A run can now hand its seat to an heir while it continues: the binding in force is saved in every save form and covered by the checkpoint hash, but not by the authoritative state hash, so restore a continuation checkpoint with from_continuation_checkpoint_verified and the continuation hash you stored. A run that never rebinds its seat writes none of the new seat-rebinding fields, so its saves and hashes are what they would be without this feature. Code that builds FiscalExecutionEvidence, BoundaryRecord, BoundaryReceipt, or SimulationSnapshot with a struct literal adds the new field, and an exhaustive match on IngressPayload or BoundaryIngressKind adds the SeatRebinding arm. Every change for saves and for code is in the 0.23.0 entry of docs/versioning.md.

Canwu 0.22.1, the release before that, was a patch release that changed no engine format, no public API, and no plugin’s semantic hash. A knowledge page now costs the page it returns rather than the holder’s whole history, and it returns exactly what 0.22.0 returned: the same records, cursors, and read cuts, so the read cuts that correspondence and law records store keep their values. Only the engine version changed: as with any version change, every checkpoint hash records it, so a 0.22.1 run does not load 0.22.0 saves. Every change is in the 0.22.1 entry of docs/versioning.md.

Canwu 0.22.0, the release before 0.22.1, was a minor release that changed no engine format: snapshot 9, commitment 8, checkpoint/evidence journal 6, paged checkpoint 7, continuation checkpoint 4, and the state-revision, admission-cursor, and exact-replay formats 3 stayed as they were, and no plugin’s semantic hash changed. It added trusted-host reads on the run handles: the current knowledge records of any holder, and a reader of the run’s current resource root. A type of your own that implements CanwuRun must add the four new methods (knowledge_heads, knowledge_current_records, domain_record_shared, domain_records_of_kind). A kept resource root is host memory and is never saved, so after a restore the host reads everything again. As with any version change, every checkpoint hash records the new engine version, so a 0.22.0 run does not load 0.21.0 saves. Every change for saves and for code is in the 0.22.0 entry of docs/versioning.md.

Canwu 0.21.0, the minor release before 0.22.0, changed one engine format. Commitment format 8 commits decision state through canwu.commitment.decisions.v4, in which a committed ticket’s options carry their ticket option bias (DecisionOption::bias, omitted when zero), so every checkpoint hash and state commitment of a run with decision state differs from 0.20.0; the decision archive blob and receipt format is 2. Snapshot 9, checkpoint/evidence journal 6, paged checkpoint 7, continuation checkpoint 4, and the state-revision, admission-cursor, and exact-replay formats 3 are unchanged. The release also changes the semantic hashes of the movement plugin (kept bookings) and the military plugin (payload schema version 2); the fiscal archive changes no format, but a continuation restore no longer counts as a seal (see fiscal). Every change for saves and for code is in the 0.21.0 entry of docs/versioning.md.

Canwu 0.20.0, the release before 0.21.0, changed two engine formats. Commitment format 7 commits the decision admission batch (the tickets the latest boundary opened) in the decision root through canwu.commitment.decisions.v3, so every checkpoint hash and state commitment of a run with decision state differs from 0.19.0; continuation checkpoint format 4 carries only the keyed draws not retired at its time and a restore refuses a checkpoint that lists a retired one (see keyed-draw retirement). Snapshot 9, checkpoint/evidence journal 6, paged checkpoint 7, and the state-revision, admission-cursor, and exact-replay formats 3 are unchanged. The release also changes the semantic hashes of the correspondence, movement, resource, production, technology, and military plugins, the resource state format (6), and both resource bucket formats (2). Every change for saves and for code is in the 0.20.0 entry of docs/versioning.md.

Canwu 0.19.0, the release before 0.20.0, is a minor release that kept every engine persistence format (snapshot 9, commitment 6, checkpoint/evidence journal 6, paged checkpoint 7, continuation checkpoint 3; the state-revision, admission-cursor, and exact-replay formats stay at 3), adds the trusted-host pending_ingress read, and changes the movement and resource plugins and the resource state format (5). What changes for saves and for code:

  • List your pending ingress after a load. pending_ingress() on Canwu, CompactedCanwu, and CanwuRun lists every queued ingress record in queue order, including the records a seal or a continuation restore keeps outside the retained journal, where ingress_log holds only the retained tail. It is a trusted-host read: it holds every source’s payloads unfiltered (host commands, decision ingress, other plugins’ internal packets, boundary-generated work), so select your own items, for example those with no cause or with your own request IDs, before showing anything to a player. It changes no format, hash, or replay input.
  • Hold a leg. DelayLeg holds the current leg, its arrival if departed or its departure if not, until an absolute time no earlier than its scheduled due; see leg timing.
  • Movement evidence must be current. An incident’s evidence and an order’s delivery_attempt must be the current version of their record when they settle, so a sealed run and its replay decide alike. An order whose delivery-attempt record is revised before it settles is refused; resubmit it with the new version.
  • Checks on entry. A movement runtime root, from MovementState::into_initial_record or a save, is refused at load when a departed leg’s arrival due is not its departure plus its planned duration and no hold explains it.
  • Resource state format 5. A transfer carries its link history (link_history, every leg and handoff it was advanced through, kept into the archive), and an open transfer keeps its transport evidence there only: its exact_evidence is empty until its terminal disposition. A lease’s eligibility envelope (EligibilityEnvelopeV1) may carry an operation_binding. A transfer with no link and an unbound envelope encode as before, and the archive blob format stays 1. A restore refuses a link history the transfer’s advances could not have recorded.
  • Resource bookkeeping. Acceptance credits a demand with what is accepted, not with the allocation leg, so a partial loss reopens that much of the demand; an allocation pass counts escrow in flight as drawn; an ended demand’s records stay in the live root until the transfer drawn on them ends; and Lose adds the escrow left to lost.
  • Code to update.
    • Movement. MovementOperation gains DelayLeg; MovementOrderRecordV1 and MovementReportDetailV1 gain hold (MovementLegHoldV1, omitted when absent, so a runtime or report without a hold encodes as before).
    • Run handles. CanwuRun gains the required method pending_ingress; a type of your own that implements the trait adds it.
    • Resource. ResourceOperationRequestV1 gains RecordTransferLoss and RescopeAccount, ResourceOperationKind gains RescopeAccount, ResourceTransfer gains link_history, EligibilityEnvelopeV1 gains operation_binding, and ResourceCreditSourceV1::ExternalInflow holds a record version (DomainRecordVersionRef). A tracked inflow Credit needs a single-step lease bound to exactly that credit (EligibilityEnvelopeV1::bound_to), and a place scope changes only through enqueue_resource_account_rescope; see completion leases.
  • Plugin identities. The movement plugin’s semantic hash and its movement_report schema hash change, and so does the resource plugin’s semantic hash; every other plugin keeps its own.

The engine version in every checkpoint hash changes, as with any version change. First-party plugins that take their plugin version from the crate version report a new one. The exact engine-version check rejects 0.18.0 saves; keep the 0.18.0 engine to read them.

Canwu 0.18.0, the previous minor release, kept the flat-save formats (snapshot 9, commitment 6, checkpoint/evidence journal 6; the state-revision, admission-cursor, and exact-replay formats stayed at 3) and changed the paged checkpoint (format 7), the continuation checkpoint (format 3), and several extension formats. The figures below come from the checked-in benchmarks, recorded on one developer machine (see docs/versioning.md). What changes for saves and for code:

  • Request outcomes are kept for a window. A tracked command request’s exact outcome and queued receipt are kept for the run’s request outcome retention (RunConfiguration::with_request_outcome_retention, default 30 simulated days, up to 366, fixed when the run is created): an exact retry inside the window gets its original outcome, and after it the request is retired, its ID stays used for the run, and any request with that ID, an exact retry or not, is refused with RequestOutcomeRetired without an attempt, a revision, or an event. Every host branch that treats IdempotencyConflict as “this ID is taken, take another” accepts RequestOutcomeRetired the same way. The rule is simulated time alone, so a wall-clock pause retires nothing, a fast simulation speed retires sooner in wall time, and an unsealed run, a sealed run, a continuation-restored run, and an exact replay answer alike and write byte-identical continuation checkpoints.
  • Choosing the retention and allocating IDs. Choose the retention from your highest speed and longest client retry delay; a client that may be away longer is answered from the outcomes the host received. Allocate request IDs consecutively (a counter the run’s state restores, or the revision a batch is built against plus an offset): retired IDs then collapse into a few ranges, where IDs hashed from keys cost about 59 bytes each for the life of the run. After a restore, the next free ID is one more than the largest ID in the checkpoint’s retired_commands, commands, queued_commands, decision_commands, unadmitted attempts, and pending ingress. At world scale the continuation checkpoint, which grew about 3.8 MB a simulated month through the request index, now grows 0.85 MB in all from month 5 to month 24.
  • Continuation checkpoint format 3. RequestIndex gains retired_commands, canonical ranges of retired request IDs; commands holds only outcomes inside the retention and queued_commands no receipt of a retired request. The restore refuses non-canonical ranges, a kept outcome or receipt inside a range, an outcome outside the retention, and a pending command request whose receipt is missing or whose ID is retired.
  • Checks on entry. A scenario or save whose initial records declare evidence no seal can accept (a payload-required or identity dependency naming an initial-scenario version, the record’s own version, or a malformed reference) is refused when the plugin registers (InvalidDomainRecord) and when the save restores (InvalidSnapshot), instead of starting a run that can never seal or write a continuation checkpoint. enqueue_command refuses request ID 0. A plugin descriptor may carry a publication reservation; a run restores and replays only with a plugin that declares the same one.
  • Code to update.
    • Fiscal. prepare_fiscal_archive(&run, book_limit, &store) replaces FiscalState::prepare_fiscal_archive, enqueue_fiscal_archive loses its sealed_cut argument, and the sealed cut comes from the run (sealed_evidence_cursor, a new required method of CanwuRun); a sealing host seals at each save. The fiscal_report schema is version 6 (a compact part payload) and the plugin is 0.5.0-experimental.
    • Resource. RESOURCE_STATE_FORMAT_VERSION is 4: accounts, access grants, definitions, unit revisions, and a custodian index persist in live buckets (resource:live:{bucket}) beside the hot terminal buckets, both of the record kind canwu.resource bucket (ResourceBucketRecord replaces ResourceHotTerminalBucketRecord), and a hot bucket holds about two records. ResourceState::into_record refuses a root with opening accounts; use into_records. max_mutations_per_boundary is max_batch_items_per_boundary and is enforced; ResourceConsumption gains fulfillment; a host Expire is refused at enqueue; a finished lease leaves the root.
    • Routing. TraversalModel gains Recurring, RoutingError gains InvalidConditions, RoutePlan gains edge_conditions_digest, and ROUTING_ALGORITHM_VERSION is canwu-routing.v2, so stored snapshot and plan digests differ from 0.17.0’s.
    • Society. SocietyState gains relation_vocabulary; the plugin descriptor loses three settlement systems and gains the relation intake, so every society semantic hash changes; a settlement journals one “Settled society” record change.
    • Law and information. LegalDefinition gains caseload; LEGAL_SHARD_PAYLOAD_FORMAT_VERSION is 2; LegalRuntime serializes without plan; max_provider_packets defaults to 256. The information delegated_authority record is one versioned ledger per command grant, and delegated_authority_reference takes only the grant.
    • Reference integrations. ForceSupplyStateV1 and EconomyReferenceStateV1 are format 2 with an archive receipt chain; prepare_economy_archive takes the run’s force-supply root; the finalize helpers return Option<IngressReceipt>.
    • Production. ProductionState gains scope, limits, and incident_history; a host with more than one root installs one per scope under canwu.production:runtime:scope:{scope}; ProductionLimitsV1 replaces max_mutations_per_boundary with max_batch_items and max_operations_per_boundary and drops max_incident_receipts; PRODUCTION_ARCHIVE_BLOB_FORMAT_VERSION is 3 (residue records, three new archive key kinds, decision_receipts on archived executions, duplicate_settlements on receipts, scope on batches, commits, and receipts, the canwu.production.archive-source-root.v2 domain); every packet carries scope; a scope root’s lease and output keys begin with {scope}/ and the primary root’s hold no /.
  • Plugin identities. The semantic hashes of the society, law, information, correspondence, fiscal (0.5.0-experimental), resource, production, and both reference economy plugins change; military, movement, technology, and culture keep theirs.
  • Paged checkpoint format 7. Every paged restore entry point (from_paged_checkpoint_and_journal, its _with_plugins form on Canwu and CompactedCanwu, and PersistentDomainRecordStore::from_state_pages) takes a new last argument, a restore budget (PagedRestoreBudget), and ErrorCode::RestoreBudgetExceeded is new; a page-store adapter or save wrapper of your own that calls a paged restore adds the argument. The descriptor gains assembled_bytes; the compact body is chunk pages instead of one envelope page, the knowledge ledger is paged as bodies and holder ledgers, and the decision manifest’s hot state is chunked, so a world with thousands of report holders saves as a paged checkpoint again. Write fresh paged checkpoints with 0.18.0; a page collector reads the new envelope through StatePageRetentionLedger as before.
  • A seal is ten times cheaper, with the same bytes. seal_evidence hashes each sealed record once and makes receipts only for the dependencies the run keeps; a seal of about 900,000 index entries takes 3.3 s instead of 30 to 59 s, and every segment, index, receipt, and hash is byte-identical.
  • Forks and restores are cheaper, with the same bytes. The runtime now holds its current state, scheduler, and retained journals in persistent, shared structures: fork() is constant-time and copies no journal, and restoring a large unsealed snapshot is about a third faster, with every snapshot, checkpoint, journal, hash, and refusal unchanged. The first append on either side after a fork, while both still hold the shared journals, copies them, so a save fork that is still alive at the original’s next boundary moves that copy (about 53 ms at 65,536 records) onto the original; a sealed run copies only its tail. See alternative realities.

The engine version in every checkpoint hash changes, as with any version change. First-party plugins that take their plugin version from the crate version report a new one. The exact engine-version check rejects 0.17.0 saves; keep the 0.17.0 engine to read them.

Canwu 0.17.0, the release before that, changed every persistence format (snapshot, commitment, checkpoint/evidence journal, paged-checkpoint envelope, continuation checkpoint; the state-revision, admission-cursor, and exact-replay formats stayed at 3), all for one reason: the knowledge ledger now stores each record body once and a holder entry per holder, and a schema may clear a superseded entry at publication (retention). Snapshot format 9 encodes the ledger as bodies and holder entries, each holder with its next local ID; commitment format 6 hashes the ledger in chunks (a leaf per entry, 256 local IDs a chunk, a root per holder) so an entry can leave; checkpoint/evidence-journal format 6 records each publication as its bodies and each holder’s entries, with one KnowledgePublished event per directive naming every holder; the paged-checkpoint envelope (format 6) and the continuation checkpoint (format 2) embed the new snapshot. The hash algorithms are unchanged, but every checkpoint hash, boundary hash, and state commitment differs from 0.16.0. What changes for saves and for code:

  • Saves hold only current knowledge. Under RetireWhenSuperseded a holder keeps one entry per line, so a continuation checkpoint of a world with thousands of report holders stays flat over simulated years instead of growing with every publication. In 0.17.0 a paged checkpoint still stored the whole knowledge ledger in one 4 MiB envelope page, so a world with thousands of report holders did not fit it; since 0.18.0 the ledger is paged (see paged checkpoints), and the continuation checkpoint remains the save for a run whose evidence prefix is too long to verify on restore.
  • Code to update.
    • Publishers. A plugin that published knowledge wraps its directive: PublishKnowledge { visibility, producer_correlation, supersession: KnowledgeSupersession::Explicit, groups: vec![KnowledgePublicationGroup { holders: vec![holder], records }], summary }. A plugin that paced itself by KnowledgeLimitsV1::batches_per_system_boundary (removed) reads view.knowledge_publication_budget() and publishes only what take admits. Every PluginKnowledgeSchema names its retention.
    • Event and boundary readers. KnowledgePublished carries holders, record_count, and entry_count, and its audience is EventAudience::KnowledgeHolders; BoundaryKnowledgeChange carries records (bodies) and holders (entries).
    • Ledger readers. HolderLedgers::record, records, records_after, record_count, last_record_id, and remove are gone; insert_publication, remove_entry, body, and HolderLedger::entry take their place, and KnowledgeQueryResult gains retention.
    • Fiscal. FiscalObserverBinding gains granularity and cadence (both defaulted), the root loses queue_pages, FiscalState gains report_schedule, and the report schedule is a new record.
  • Plugin identities. The fiscal plugin is 0.4.0-experimental with a new semantic hash, and its wake cadence (with_report_cadence), report share, and retention knobs are part of it. Every plugin that registers a knowledge schema records the schema’s retention in its descriptor.
  • Checks on entry. A snapshot whose knowledge ledger holds a body no holder holds, an entry naming a missing body, a local ID out of order, or a relation that does not resolve within its holder is refused; a restore rebuilds the ledger from the journal and refuses one that does not match the saved ledger.
  • The continuation gate. The continuation-gate Cargo feature of canwu-api is a release test, never enabled by applications: it runs every mutation on the run and on a sealed shadow restored from continuation checkpoints and panics on any divergence.

The engine version in every checkpoint hash changes, as with any version change. First-party plugins that take their plugin version from the crate version report a new one. The exact engine-version check rejects 0.16.0 saves; keep the 0.16.0 engine to read them.

Canwu 0.16.0, the minor release before 0.17.0, changed three formats and added one. Commitment format 5 hashes the knowledge ledger incrementally and covers every field of a boundary’s knowledge changes; checkpoint/evidence-journal format 5 drops the keyed-draw reservations, because the keyed-draw ledger now holds every drawn address; paged-checkpoint envelope format 5 pages records of up to 512 MiB in 1 MiB chunks; and the continuation checkpoint is new. The hash algorithms are unchanged, but every checkpoint hash, boundary hash, and domain-record root differs from 0.15.0, and authoritative_state_hash is now the commitment-format state hash, equal for sealed, unsealed, and restored runs. What changes for saves and for code:

  • Saves no longer grow with work in flight. A seal proceeds with pending ingress, and a continuation checkpoint saves a long campaign in a few megabytes. A first operation-keyed draw that cites sealed evidence is accepted alike in sealed and unsealed runs, and one that cites a superseded record version is refused in every run.
  • Code to update. DomainRecordChange::previous and current are Arc<DomainRecord>. KnowledgeSnapshot::records is HolderLedgers, whose iterators are forward-only (.rev() and next_back do not compile; read the newest record with last_key_value). ErrorCode is non-exhaustive and gains HistoryUnavailable. SocietyIngressQueue has queued and batches instead of entries. ProductionState::prepare_production_archive takes the archive store, and prepare_force_archive and prepare_economy_archive do too. The fiscal API is rebuilt around records, batches, and per-record revisions: FiscalActionBatch replaces FiscalActionRequest, enqueue_execution_receipts replaces enqueue_execution_receipt, FiscalState::into_records replaces into_record, and FiscalAction gains six variants. A command handler can no longer schedule a directive that enqueues plugin ingress (InvalidPayload); give EnqueuePluginIngress its own delay instead.
  • Plugin identities. The semantic hashes of the society (which now also depends on its transfer settlement mode), fiscal, resource, production, law, information, and military plugins and of the two reference economy plugins change.
  • Archives. The production archive and the reference package archive gain the lookup index of the resource archive, and the fiscal archive is new; keep every archive’s lookup pages, as for the resource archive. Production roots and archive objects, fiscal states, society queue records, and package archive directories written by 0.15.0 do not decode, but no 0.16.0 run can reach one.
  • The resource root. RESOURCE_STATE_FORMAT_VERSION is 3 (0.18.0: 4, with live buckets and the record kind bucket; 0.19.0: 5; see above): the root persists as its live root (resource:runtime:v1, payload ResourceLiveRootV1, which counts its hot terminal records in a ledger) and one record per hot terminal bucket (the new kind canwu.resource hot-terminal, resource:hot-terminal:{bucket}, format 1), and the byte cap is measured on the live root plus the entries. Archive blobs, retention handles, and directories name the v2 archive source root, so their ids differ from 0.15.0 while their shapes are unchanged. Code that read a hot terminal record from ResourceRuntimeRecord::Payload reads it through resource_root_in_view, ResourceRootReader, or whole_resource_root_in_view and declares resource_hot_terminal_state_key(); ResourceState::into_records replaces into_record for a root that holds terminal records; the force-supply restore helpers take an archive provider. See resources and production.
  • Refusals that no longer stall a run. Across the extensions, a packet that cannot settle for an expected reason is a recorded rejection, a skipped packet with an event, or a deferral: resource packets that do not fit the root or whose key is taken (skipped packets), production packets that do not decode or fail their checks, fiscal receipts, context changes, and archive packets, law packets past a budget or with a stale host version, and information packets that conflict. No budget or refusal fails a boundary any more.
  • Checks on entry. A fiscal record set that is not exactly the partition of a valid state, a society queue whose head and batches disagree, a process with more than 64 outputs, and scheduled work that would enqueue plugin ingress are refused when a run is created or restored. A plugin must not declare an initial-scenario record version as payload-required; a run whose root does cannot seal.

The engine version in every checkpoint hash changes, as with any version change. First-party plugins that take their plugin version from the crate version report a new one. The exact engine-version check rejects 0.15.0 saves; keep the 0.15.0 engine to read them.

Earlier releases (0.15.0, 0.14.0, 0.13.x) are described in the repository’s versioning and persistence contract; each kept the rule that a save loads only in the engine version that wrote it.

  • Pin an exact release or Git revision for any application that keeps saves.
  • Treat a plugin semantic-hash change as a behavior change. Any release, even a patch release such as 0.13.0 → 0.13.1, can change the semantic hashes of several extensions at once, and older saves that use those extensions then fail to load.
  • Canwu does not migrate old saves. To carry history across an engine upgrade, run your own export and import, or keep the engine that wrote the data.
  • New releases add enum variants and struct fields, so exhaustive match statements and struct literals may need updates.

For what each release changed, see the versioning and persistence contract.

  • Pin the engine version and plugin semantic environment.
  • Do one full save, process restart, and load, then compare checkpoint hashes.
  • Run one exact replay and compare the final checkpoint hash.
  • Simulate an outbox timeout and confirm that retrying by delivery_id does not repeat the outside effect.
  • Write down what an upgrade does with old saves: export and re-import them, keep the old engine to read them, or drop them.