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.
One boundary, start to finish
Section titled “One boundary, start to finish”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"]
- 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.
- It builds the admission set for this boundary: command attempts, accepted commands, admitted ingress and events, and the calendar cadences that apply, such as
DailyorMonthly. - 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.
- 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; anEventDrivensystem runs when the boundary admitted any event or ingress. - It records the boundary evidence: admitted inputs, allocations, state changes, random draws, transition audits, evaluation traces, knowledge publications, and the boundary hash.
- 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 fourteen phases
Section titled “The fourteen phases”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 |
Four building blocks
Section titled “Four building blocks”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.
Resource allocation
Section titled “Resource allocation”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.
Visibility and commit
Section titled “Visibility and commit”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.
Person and ingress directives
Section titled “Person and ingress directives”Most directives write state that a plugin owns. Three directives change state that the kernel owns, and each has fixed rules:
SetPersonAvailabilityreplaces one person’s life and custody state. A system in phase 7 or 10 that declares a write toStateKey::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.CreatePersoncreates a person. A phase-7 system that declares a write toStateKey::core_people()may propose it. The engine allocates the ID, and the receipt’screated_personsbinds that ID to the proposing plugin, system, and correlation string. Systems can see the new person from the next boundary.CancelPluginIngresswithdraws a queued plugin ingress item that the system’s own plugin scheduled, strictly before the item is due. Get targets fromSimulationView::cancellable_plugin_ingress, which requires aStateKey::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.
Transition manifests
Section titled “Transition manifests”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.
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
- Register. A system of the coordinating plugin, in phase 7, 10, or 12, declares a write to
StateKey::core_transitions()and proposesBoundaryDirective::RegisterTransitionManifest. TheTransitionManifestholds alineage_id, anattemptnumber, theready_atboundary, 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 isTransitionManifestId { 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. - 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 emptywriteslist records that the participant was present without writing anything. If a plugin that is not listed tries to stage, the boundary fails withInvalidAuthority. - 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 isExpiredand nothing is written for the manifest. If only some participants staged, the whole boundary fails withTransitionParticipantMissing. If an expected version differs, it fails withTransitionVersionMismatch. - Read. The kernel stores a
TransitionAuditRecordwith the outcome and the number of writes each participant staged. It appears inBoundaryRecord::transition_auditsandBoundaryReceipt::transition_audits. Systems of the coordinator and participants that run in phase 11 or later read it throughSimulationView::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 thecanwu.core.transitionsread, from the phase after its registration. The host application reads the pending set withCanwu::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.
Evaluation traces
Section titled “Evaluation traces”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
EvaluationLimitsV1throughRunConfiguration::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 withEvaluationTraceLimitExceeded; 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.
Where extension systems run
Section titled “Where extension systems run”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.
Plugin contract
Section titled “Plugin contract”Each boundary system registers a BoundarySystemContract that declares:
- its name, phase, and cadence;
- the
StateKeyvalues 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.