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.
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.
Choose the recovery goal
Section titled “Choose the recovery goal”| 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 |
Save and restore a snapshot (excerpt)
Section titled “Save and restore a snapshot (excerpt)”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 starterCheckpoint journals and exact replay
Section titled “Checkpoint journals and exact replay”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.
Alternative realities
Section titled “Alternative realities”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, andseal_evidenceread 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 returningNoneor an error, which a boundary would take for a missing or unreadable object. A store a run holds is passed asArc<dyn … + Send + Sync>; keep its maps behind aMutexorRwLock. - 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.
Deliver outside effects: the outbox
Section titled “Deliver outside effects: the outbox”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.
- Store each
delivery_idwith its delivery and acknowledgement state in your own storage. - Deliver at least once, so a timeout or restart can retry safely.
- Deduplicate by
delivery_idon the receiving side; a network call may happen more than once. - Exact replay regenerates the same
delivery_idvalues 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.
Continuation checkpoints
Section titled “Continuation checkpoints”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 anchorlet continued = CompactedCanwu::from_continuation_checkpoint_verified( checkpoint, &continuation_hash, &[&plugin],)?;assert_eq!(continued.checkpoint_hash(), canwu.checkpoint_hash());- Write.
Canwu::continuation_checkpoint()andCompactedCanwu::continuation_checkpoint()return the checkpoint and itscontinuation_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 withArchiveNotReadyexactly 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_pluginsand 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 aCompactedCanwu, never aCanwu. - 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 nocause, 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_hashfield 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 traitCanwuandCompactedCanwuimplement, 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: callvalidate_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).
Advanced storage and archives
Section titled “Advanced storage and archives”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.
Paged checkpoints
Section titled “Paged checkpoints”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 aPreparedPagedSimulationCheckpoint. Its delta leaves out every page the provider already holds;sourceis the earlier checkpoint, if there is one.store_and_verifystores 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)andfrom_paged_checkpoint_and_journal_with_pluginsrebuild 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 withjournal_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 withStatePageUnavailableinstead of reading as empty. Of the archived decision history, the restore reads only the pages that hot decision state depends on; other archived history reportsUnresolveduntildecision_history_location_with_providerlooks it up through aDecisionArchiveProviderthat serves the same pages. - Compact mode.
CompactedCanwuoffers the same calls. Its evidence prefix is the segments you have sealed, in order, followed by whatjournal_segment_sincereturns 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_verifychecks only the new pages and the root; useStatePageRetentionLedger::prepareandverifyto check a checkpoint’s complete page set. - A compact restore needs the
_with_pluginsform 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
CanwuorCompactedCanwu, 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.
Restart a compact run
Section titled “Restart a compact run”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:
- Until the restored run seals again, a checkpoint of it uses the prefix you restored it from, followed by what
journal_segment_sincereturns from that prefix’s end. - Its next
seal_evidence()returns one segment that starts at the beginning of the run and replaces every segment sealed before it. - From then on, the prefix starts with that new segment, followed by what
journal_segment_sincereturns from its end. - 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.
Version and save compatibility
Section titled “Version and save compatibility”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()onCanwu,CompactedCanwu, andCanwuRunlists every queued ingress record in queue order, including the records a seal or a continuation restore keeps outside the retained journal, whereingress_logholds 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 nocauseor with your own request IDs, before showing anything to a player. It changes no format, hash, or replay input. - Hold a leg.
DelayLegholds 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_attemptmust 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_recordor 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: itsexact_evidenceis empty until its terminal disposition. A lease’s eligibility envelope (EligibilityEnvelopeV1) may carry anoperation_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
Loseadds the escrow left tolost. - Code to update.
- Movement.
MovementOperationgainsDelayLeg;MovementOrderRecordV1andMovementReportDetailV1gainhold(MovementLegHoldV1, omitted when absent, so a runtime or report without a hold encodes as before). - Run handles.
CanwuRungains the required methodpending_ingress; a type of your own that implements the trait adds it. - Resource.
ResourceOperationRequestV1gainsRecordTransferLossandRescopeAccount,ResourceOperationKindgainsRescopeAccount,ResourceTransfergainslink_history,EligibilityEnvelopeV1gainsoperation_binding, andResourceCreditSourceV1::ExternalInflowholds a record version (DomainRecordVersionRef). A tracked inflowCreditneeds a single-step lease bound to exactly that credit (EligibilityEnvelopeV1::bound_to), and a place scope changes only throughenqueue_resource_account_rescope; see completion leases.
- Movement.
- Plugin identities. The movement plugin’s semantic hash and its
movement_reportschema 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 withRequestOutcomeRetiredwithout an attempt, a revision, or an event. Every host branch that treatsIdempotencyConflictas “this ID is taken, take another” acceptsRequestOutcomeRetiredthe 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.
RequestIndexgainsretired_commands, canonical ranges of retired request IDs;commandsholds only outcomes inside the retention andqueued_commandsno 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_commandrefuses 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)replacesFiscalState::prepare_fiscal_archive,enqueue_fiscal_archiveloses itssealed_cutargument, and the sealed cut comes from the run (sealed_evidence_cursor, a new required method ofCanwuRun); a sealing host seals at each save. Thefiscal_reportschema is version 6 (a compact part payload) and the plugin is0.5.0-experimental. - Resource.
RESOURCE_STATE_FORMAT_VERSIONis 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 kindcanwu.resourcebucket(ResourceBucketRecordreplacesResourceHotTerminalBucketRecord), and a hot bucket holds about two records.ResourceState::into_recordrefuses a root with opening accounts; useinto_records.max_mutations_per_boundaryismax_batch_items_per_boundaryand is enforced;ResourceConsumptiongainsfulfillment; a hostExpireis refused at enqueue; a finished lease leaves the root. - Routing.
TraversalModelgainsRecurring,RoutingErrorgainsInvalidConditions,RoutePlangainsedge_conditions_digest, andROUTING_ALGORITHM_VERSIONiscanwu-routing.v2, so stored snapshot and plan digests differ from 0.17.0’s. - Society.
SocietyStategainsrelation_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.
LegalDefinitiongainscaseload;LEGAL_SHARD_PAYLOAD_FORMAT_VERSIONis 2;LegalRuntimeserializes withoutplan;max_provider_packetsdefaults to 256. The informationdelegated_authorityrecord is one versioned ledger per command grant, anddelegated_authority_referencetakes only the grant. - Reference integrations.
ForceSupplyStateV1andEconomyReferenceStateV1are format 2 with an archive receipt chain;prepare_economy_archivetakes the run’s force-supply root; the finalize helpers returnOption<IngressReceipt>. - Production.
ProductionStategainsscope,limits, andincident_history; a host with more than one root installs one per scope undercanwu.production:runtime:scope:{scope};ProductionLimitsV1replacesmax_mutations_per_boundarywithmax_batch_itemsandmax_operations_per_boundaryand dropsmax_incident_receipts;PRODUCTION_ARCHIVE_BLOB_FORMAT_VERSIONis 3 (residue records, three new archive key kinds,decision_receiptson archived executions,duplicate_settlementson receipts,scopeon batches, commits, and receipts, thecanwu.production.archive-source-root.v2domain); every packet carriesscope; a scope root’s lease and output keys begin with{scope}/and the primary root’s hold no/.
- Fiscal.
- 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_pluginsform onCanwuandCompactedCanwu, andPersistentDomainRecordStore::from_state_pages) takes a new last argument, a restore budget (PagedRestoreBudget), andErrorCode::RestoreBudgetExceededis new; a page-store adapter or save wrapper of your own that calls a paged restore adds the argument. The descriptor gainsassembled_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 throughStatePageRetentionLedgeras before. - A seal is ten times cheaper, with the same bytes.
seal_evidencehashes 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
RetireWhenSupersededa 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 byKnowledgeLimitsV1::batches_per_system_boundary(removed) readsview.knowledge_publication_budget()and publishes only whattakeadmits. EveryPluginKnowledgeSchemanames itsretention. - Event and boundary readers.
KnowledgePublishedcarriesholders,record_count, andentry_count, and its audience isEventAudience::KnowledgeHolders;BoundaryKnowledgeChangecarriesrecords(bodies) andholders(entries). - Ledger readers.
HolderLedgers::record,records,records_after,record_count,last_record_id, andremoveare gone;insert_publication,remove_entry,body, andHolderLedger::entrytake their place, andKnowledgeQueryResultgainsretention. - Fiscal.
FiscalObserverBindinggainsgranularityandcadence(both defaulted), the root losesqueue_pages,FiscalStategainsreport_schedule, and the report schedule is a new record.
- Publishers. A plugin that published knowledge wraps its directive:
- Plugin identities. The fiscal plugin is
0.4.0-experimentalwith 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-gateCargo feature ofcanwu-apiis 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::previousandcurrentareArc<DomainRecord>.KnowledgeSnapshot::recordsisHolderLedgers, whose iterators are forward-only (.rev()andnext_backdo not compile; read the newest record withlast_key_value).ErrorCodeis non-exhaustive and gainsHistoryUnavailable.SocietyIngressQueuehasqueuedandbatchesinstead ofentries.ProductionState::prepare_production_archivetakes the archive store, andprepare_force_archiveandprepare_economy_archivedo too. The fiscal API is rebuilt around records, batches, and per-record revisions:FiscalActionBatchreplacesFiscalActionRequest,enqueue_execution_receiptsreplacesenqueue_execution_receipt,FiscalState::into_recordsreplacesinto_record, andFiscalActiongains six variants. A command handler can no longer schedule a directive that enqueues plugin ingress (InvalidPayload); giveEnqueuePluginIngressits 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_VERSIONis 3 (0.18.0: 4, with live buckets and the record kindbucket; 0.19.0: 5; see above): the root persists as its live root (resource:runtime:v1, payloadResourceLiveRootV1, which counts its hot terminal records in a ledger) and one record per hot terminal bucket (the new kindcanwu.resourcehot-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 fromResourceRuntimeRecord::Payloadreads it throughresource_root_in_view,ResourceRootReader, orwhole_resource_root_in_viewand declaresresource_hot_terminal_state_key();ResourceState::into_recordsreplacesinto_recordfor 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
matchstatements and struct literals may need updates.
For what each release changed, see the versioning and persistence contract.
Production checklist
Section titled “Production checklist”- 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_iddoes 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.