Skip to content

Settlement system

The settlement system decides when and how authoritative state changes. Its unit of work is the boundary: one settlement pass at one simulation time. A boundary admits the work that is due, runs plugin systems in fourteen fixed phases, and then commits all of their changes together or rolls all of them back. Read this page when you write a boundary system or need to know when a write becomes visible to other systems.

The host application starts a boundary with settle_boundary(BoundaryRequest), or calls advance_canonical and step_canonical to settle queued input on the simulation clock. Terms such as ingress, admission, and holder are defined in the terminology reference.

Path of a queued command through one boundary, ending in commit or rollback. View diagram source.
View diagram source
flowchart LR
  Host["Host application<br/>enqueues a command"] --> Queue["Canonical ingress queue"]
  Queue -- "due at the boundary time" --> Admit["Phase 1<br/>admit due work"]
  Admit --> Snapshot["Phase 2<br/>fix the read baseline"]
  Snapshot --> Systems["Phases 3–14<br/>read, allocate, stage,<br/>validate, commit, hash"]
  Systems --> Check{"Every phase<br/>succeeded?"}
  Check -- "yes" --> Commit["Boundary committed;<br/>settle_boundary returns a receipt"]
  Check -- "no" --> Rollback["Whole boundary rolled back;<br/>settle_boundary returns the error"]
  1. The engine runs internal scheduled work that is due before the requested time. It then takes the canonical ingress that is due, in queue order. Canonical ingress is the persisted, ordered input queue described in the event system.
  2. It builds the admission set for this boundary: command attempts, accepted commands, admitted ingress and events, and the calendar cadences that apply, such as Daily or Monthly.
  3. It copies the current state as the boundary snapshot. Systems in phases 1 to 8 read this snapshot, so they all start from the same baseline.
  4. It runs the fourteen phases in order. Inside one phase, systems run in (plugin name, system name) order. A system runs when the request lists its cadence; an EventDriven system runs when the boundary admitted any event or ingress.
  5. It records the boundary evidence: admitted inputs, allocations, state changes, random draws, transition audits, evaluation traces, knowledge publications, and the boundary hash.
  6. If any step fails, it restores time, queues, state, journals, random streams, counters, and boundary records to their values before the boundary, and returns the error.

The phases fix the order in which work happens. Your plugin registers each boundary system in one phase. Systems may declare state writes only in phases 7, 10, 12, and 13. Phases 9 and 11 are where the kernel commits staged writes.

# Phase What happens
1 EventIngress Admit due commands, events, and plugin input
2 BoundarySnapshot Fix the read baseline for this boundary
3 DerivedFieldSolve Compute declared derived fields
4 PerceptionAndAttentionRefresh Refresh actor perception and attention
5 DecisionAndAcceptedEffectIntake Read-only plugin slot for decision and effect intake; decision ingress is admitted in phase 1
6 ReservationAndAllocation Offer resources, request them, and let the kernel allocate
7 DomainDeltaProposal Stage component, record, and event changes
8 InvariantValidation Check the staged changes against invariants
9 AtomicDomainCommit Commit the phase-7 changes together
10 HistoricalCandidateEvaluation Evaluate historical transitions; manifest participants stage their writes
11 ConditionalTransitionCommit Audit ready transition manifests, then commit the phase-10 changes
12 StrategicAggregation Build strategic-level aggregates; read transition audits
13 PerspectiveAndReportMaterialization Build actor perspectives and reports
14 SaveReplayAndDiagnosticHashing Record evidence and update replay and diagnostic hashes

The phases are an ordering. Underneath them, settlement uses four building blocks:

Building block What it does Writes state
Immediate write A command handler or legacy event reactor applies its effect at once, without staging it for a boundary commit. Legacy compatibility code, such as the Command::OrderMovement path, uses it, as do a few plugin commands such as canwu-society’s set_institutional_policy. Yes
Reservation and allocation Phase-6 systems offer supply and request it; the kernel computes who receives how much. No; it produces an allocation result
Staged boundary commit Systems stage changes against the boundary snapshot; the kernel validates them and commits them as one bundle. New mechanics use this path. Yes
Visibility Each boundary system declares SameBoundary or NextBoundary for the writes it stages. The choice decides when other systems can read them. No; it controls timing

A state key belongs to one path. Registration fails if an immediate handler and a boundary system declare writes to the same StateKey.

Allocation separates “who runs first” from “who receives resources”. In phase 6, systems declare what they offer and what they request; the kernel then settles all requests for a pool at once. Requests are ordered by pool, then by descending priority, then by tie-break key, then by reservation identity. The result depends only on these keys.

For example, a granary pool offers 100 units, and three requests compete for it:

Request Priority Quantity Result
A 5 80 Fulfilled: 80 granted
B 1 50 Partial: 20 granted
C 1 10 Rejected: 0 granted

B and C share a priority, so their tie-break keys decide which goes first; here B’s key sorts before C’s. A system reads an allocation through SimulationView::reservation and must list it in reservation_reads.

The movement lifecycle extension, canwu-movement, allocates transport capacity differently. In phase 7 it calls the pure function allocate_capacity_bookings from canwu-transport once per capacity pool. A booking is confirmed in full or fails. Bookings are visited by descending priority, then window start, tie-break key, admission sequence, and booking identity. Each result is recorded as CapacityBookingAllocationEvidenceV1, and confirmed capacity stays with its booking.

A boundary system returns a BoundaryProposal. Its directives stage writes, and the kernel commits them later. When a staged write becomes readable depends on the phase and on the visibility the system declared:

Staged in phase SameBoundary write NextBoundary write
7 Readable from phase 8 through the boundary’s overlay of staged values; committed in phase 9 Committed at the end of the boundary; readable from the next boundary
10 Committed in phase 11; readable from phase 11 on Committed at the end of the boundary
12 or 13 Committed at the end of that phase Committed at the end of the boundary

Invariant systems in phase 8 can inspect every staged value, including NextBoundary values, through proposed_component and proposed_domain_record. They still need the matching entries in their declared reads.

Events emitted during a boundary are admitted by the next boundary through the normal admission path.

Most directives write state that a plugin owns. Three directives change state that the kernel owns, and each has fixed rules:

  • SetPersonAvailability replaces one person’s life and custody state. A system in phase 7 or 10 that declares a write to StateKey::core_person_availability() may propose it. Two writes for the same person in one boundary fail the boundary. At the end of the boundary, after random decisions are applied, the kernel cancels open decision tickets whose decision maker or controller authority person became unavailable, and lists them in the boundary record.
  • CreatePerson creates a person. A phase-7 system that declares a write to StateKey::core_people() may propose it. The engine allocates the ID, and the receipt’s created_persons binds that ID to the proposing plugin, system, and correlation string. Systems can see the new person from the next boundary.
  • CancelPluginIngress withdraws a queued plugin ingress item that the system’s own plugin scheduled, strictly before the item is due. Get targets from SimulationView::cancellable_plugin_ingress, which requires a StateKey::core_ingress() read. The whole boundary fails if the target belongs to another plugin, is already due, admitted, or withdrawn, or if another proposal in the same boundary withdraws it too.

Several systems may declare the two core person keys. The kernel therefore checks for conflicting writes per person during settlement.

Some historical transitions need several plugins to write in the same boundary. A transition manifest lets a coordinating plugin name every participant in advance, so the kernel can catch a participant that stays silent. The kernel audits each manifest in phase 11, before the phase-10 writes commit.

Lifecycle of a transition manifest from registration through staging and the phase-11 audit. View diagram source.
View diagram source
flowchart LR
  Register["Phase 7, 10, or 12<br/>coordinator registers the manifest"] --> Stage["Ready boundary, phase 10<br/>listed participants stage writes"]
  Stage --> Audit{"Phase 11 audit"}
  Audit -- "all staged, versions match" --> Committed["Committed<br/>staged writes commit"]
  Audit -- "nobody staged" --> Expired["Expired<br/>nothing written"]
  Audit -- "some staged, or a version differs" --> Failed["Whole boundary fails<br/>and rolls back"]
  Committed --> Read["Phase 11 onward<br/>coordinator and participants read the audit"]
  Expired --> Read
  1. Register. A system of the coordinating plugin, in phase 7, 10, or 12, declares a write to StateKey::core_transitions() and proposes BoundaryDirective::RegisterTransitionManifest. The TransitionManifest holds a lineage_id, an attempt number, the ready_at boundary, and one entry per participant plugin. Each entry lists the record versions that participant expects before the transition (expected_pre) and after it (expected_post). The manifest’s identity is TransitionManifestId { coordinator, lineage_id, attempt }; the kernel fills in the coordinator from the registering plugin. A manifest registered in phase 7 may be ready in the same boundary. One registered in phase 10 or 12 must name a later boundary. No manifest may be ready more than 1,024 boundaries ahead.
  2. Stage. In phase 10 of the ready boundary, each listed participant’s phase-10 system that declares the same write proposes StageTransitionWrite { manifest_id, writes }. The staged writes are ordinary directives, so the system’s declared writes, ownership, phase rules, and visibility all apply. An empty writes list records that the participant was present without writing anything. If a plugin that is not listed tries to stage, the boundary fails with InvalidAuthority.
  3. Audit. Before phase 11 commits, the kernel checks every manifest that is ready in this boundary. If every participant staged and every expected version matches, the outcome is Committed. If no participant staged, the outcome is Expired and nothing is written for the manifest. If only some participants staged, the whole boundary fails with TransitionParticipantMissing. If an expected version differs, it fails with TransitionVersionMismatch.
  4. Read. The kernel stores a TransitionAuditRecord with the outcome and the number of writes each participant staged. It appears in BoundaryRecord::transition_audits and BoundaryReceipt::transition_audits. Systems of the coordinator and participants that run in phase 11 or later read it through SimulationView::transition_audits.

expected_pre refers to versions in the committed state that phase 10 reads. expected_post is checked in phase 11, after the boundary’s SameBoundary phase-10 writes commit and with its pending NextBoundary phase-7 and phase-10 writes applied. A phase-12 or phase-13 write can still change the record later in the same boundary.

Manifests are bounded, and any rule violation fails the boundary:

  • A coordinator holds at most one pending manifest per lineage and at most 32 pending manifests. All coordinators together hold at most 128. One manifest lists at most 16 participants and 64 expected versions in total. Each participant must be a registered plugin with a phase-10 system that declares core_transitions, and each expected record must be of a registered kind.
  • Only the coordinator and the participants can see a manifest. They read it through SimulationView::transition_manifests, after declaring the canwu.core.transitions read, from the phase after its registration. The host application reads the pending set with Canwu::pending_transition_manifests.
  • There is no withdrawal directive. A manifest expires when nobody stages, so if its ready boundary fails on every retry, settle that boundary with a cadence under which no participant system runs. The manifest expires, and the coordinator can register a new attempt. A manifest ID may be reused after it settles, so (manifest_id, ready_at) identifies an audit.
  • Pending manifests are saved in the snapshot and roll back with a failed boundary. Snapshot validation rebuilds them from the registration and audit evidence. A run that never registers a manifest hashes the same as a run without this feature.
  • Phase-10 directives outside any manifest are unaffected. To catch a missing participant, a manifest needs at least two participants; with only one, silence just expires it.

An evaluation trace explains how an application rule produced one number for one subject, term by term. A system in phase 7 or 12 records one by proposing BoundaryDirective::RecordEvaluationTrace { trace }; the system contract needs no extra declaration. An EvaluationTraceRecord carries the rule ID and version, the subject entity, a list of EvaluationTerm values (term ID, integer contribution, and the evidence the term read), the result as the rule computed it, and the current boundary. The kernel checks the shape, that the subject exists, and that every evidence reference is committed evidence the proposal can see. It does not check that the terms add up to the result.

  • A trace is boundary evidence, kept apart from state. The kernel records it, with the producing plugin, system, and phase, in BoundaryRecord::evaluation_traces, includes it in the boundary hash chain, and seals and archives it with the boundary record. Systems, command handlers, and decision policies cannot read traces back to decide an outcome.
  • The run configuration sets the limits with EvaluationLimitsV1 through RunConfiguration::with_evaluation_limits. The default allows 4,096 traces per boundary across all systems and 32 terms per trace; the maximum is 65,536 traces and 256 terms. Each term cites at most 16 evidence references, and each rule ID, rule version, or term ID is at most 256 bytes. A proposal set over any limit fails the boundary with EvaluationTraceLimitExceeded; a committed boundary keeps all of its traces. A limit of zero forbids traces for the run.
  • Phase 13 cannot record traces, so record the trace from the phase-7 or phase-12 system that computed the value. Traces count toward journal size.
  • Players and agents read traces through CanwuViewer::evaluation_traces. That view omits evidence and follows what the holder knows; see reading state.

The first-party extensions below use the same phases. The table helps you predict when their state changes relative to your own systems.

Extension Phase and cadence System and work
canwu-movement 7, event-driven movement_lifecycle_apply_v1 applies admitted operations and incidents in admission order, runs one allocation pass per capacity pool, applies due legs, and retires closed executions.
canwu-movement 8, event-driven movement_lifecycle_validate_v1 validates the staged movement runtime.
canwu-movement 13, event-driven movement_report_publish_v1 publishes movement reports that changed, each addressed to the holder allowed to see it.
canwu-culture (CultureBoundaryPlugin) 12, event-driven culture_exposure_intake_v1 queues admitted culture_exposure_v1 batches.
canwu-culture (CultureBoundaryPlugin) 7, Monthly culture_lifecycle_settle_v1 consumes the queue and accepted institutional decisions, settles the lifecycle against the boundary snapshot of society state, saves culture state, and schedules society deltas and signal batches as next-boundary ingress.
canwu-society 12, event-driven intake-society-ingress queues admitted cohort_headcount_rebase_v1 and society_lifecycle_delta_v1 packets.
canwu-society 7, Daily settle-social-transitions applies the queue after cohort transfers and before institutional decisions and transitions.

Movement legs are timed with the internal scheduled ingress movement_leg_due_v1. An order schedules its first departure, a departure schedules its arrival after the leg’s planned duration, and an arrival schedules the next departure. A due packet takes effect only if it matches the due time saved on the order, so an explicit operation makes older packets stale. Due work never fails a boundary.

Culture and society use intake queues because only one system may write a given state key in a given phase, and their writers run on a coarser cadence. All phase-7 systems read the same boundary snapshot, so every culture hand-off lands in a later boundary: signal batches are admitted at the next boundary, and a society delta is applied at the first Daily settlement after it is admitted. Expect up to two boundaries between a culture step and its effect on society, and between a headcount rebase packet and the rebased cohort. The society plugin stays the only writer of canwu.society:state.

Each boundary system registers a BoundarySystemContract that declares:

  • its name, phase, and cadence;
  • the StateKey values it reads and writes;
  • the reservation pools it offers and requests, and the allocations it reads (reservation_reads);
  • the random streams it owns;
  • the event types it emits (emits), the knowledge schemas it may publish (knowledge_writes), and the other plugins it may send ingress to (plugin_ingress_targets);
  • the visibility of its writes.

Kernel-owned keys follow the same rule. Transition directives need StateKey::core_transitions(), and person directives need StateKey::core_person_availability() or StateKey::core_people(). Evaluation traces need no declaration because they write no state.

The kernel rejects undeclared reads, writes, reservation reads, and random draws. Plugin registration, snapshot restore, and exact replay all require the same plugin name, version, and semantic hash.