Event system
An event in Canwu is a serializable record of something that happened, together with what caused it. Events are part of the authoritative evidence: snapshots, boundary records, and exact replay all include them. Read this page when your plugin emits events, schedules follow-up work, or needs to explain to a player why something happened.
A client that wants notifications builds them from the events its actor is allowed to see. The engine owns the event log, and clients have read access only.
An event from cause to viewer
Section titled “An event from cause to viewer”View diagram source
flowchart LR
Command["Accepted command"] --> Event["SimEvent<br/>cause = CauseRef::Command"]
Event --> Audience{"EventAudience<br/>declared by the plugin"}
Audience -- "visible to this actor" --> View["CanwuViewer<br/>visible_changes_since"]
Audience -- "private" --> Hidden["Trusted reads only"]
Event --> Next["Next boundary<br/>admits the event"]
Next --> Systems["Event-driven<br/>systems run"]
An event records its cause, so tools can walk back from any event to the command or system that produced it. Its audience decides which actor-relative views show it. Once recorded, it is admitted by the next boundary, where EventDriven boundary systems react to it.
What an event contains
Section titled “What an event contains”Every SimEvent contains:
| Field | Meaning |
|---|---|
id |
Stable event identity, assigned in increasing order |
timestamp |
Simulation time at which the event occurs |
kind |
Event type and structured payload |
affected_entities |
Entities the event affects |
summary |
Short explanation for tools and debug views |
cause |
Command, event, boundary, or system that produced it |
correlation_id |
Groups the evidence of one processing chain |
EventKind stores a stable type tag and a flat set of structured fields, and each domain defines its own event types. Define a typed payload in your own crate, encode it with EventKind::from_payload, and read it back with decode_payload. Plugin events carry the plugin name and event type, which plugin_identity returns. These tags and the field order are part of the snapshot format, so changing them breaks saved runs.
Who can see an event
Section titled “Who can see an event”A plugin declares the audience of each event type it emits with PluginRegistrar::register_event_audience. The declaration is saved in the plugin descriptor. An EventAudience is one of:
Public: every actor;Actor(person)orActors(people): the listed people;KnowledgeHolder(holder): one knowledge holder, such as a person or an institution;AffectedActors: the people named in the event’saffected_entities;Private: no actor-relative view. This is the default for any event type without a declaration.
CanwuViewer::visible_changes_since returns the changes one actor may see, filtered by these rules. Trusted host code, such as a debug tool, can still read every event.
Causal chain
Section titled “Causal chain”CauseRef records one of four sources:
Command: an accepted engine command or domain command;Event: an earlier recorded event;Boundary: the boundary (byBoundaryId) whose system produced the event;System: a kernel system or a controlled plugin system.
A plugin-owned domain action can form this chain of evidence:
plugin command └─ typed domain event └─ scheduled plugin work ├─ domain-state change ├─ follow-up domain event └─ actor-scoped report └─ knowledge publicationEvery link keeps its time, entities, cause, and correlation ID. A debug view can therefore explain what happened, and replay can check why it happened in that order.
Scheduling and admission
Section titled “Scheduling and admission”Canwu keeps two ordered queues:
- Internal scheduled work is ordered by
(simulation timestamp, insertion sequence). - Canonical ingress is the persisted queue for input from the host application and from plugins. It is ordered by due time, then ingress class, then descending priority, then issue time, then ingress ID. The classes, in order, are command, communication, acknowledgement, information, decision, and scheduled system.
The issuer of a queued plugin item can withdraw it before it is due. The withdrawal is recorded as its own terminal ingress record, and the withdrawn item is never admitted. See plugin ingress cancellation.
Admission decides which queued items belong to a boundary. Input due earlier than the committed simulation time is rejected with LateIngress, so nothing is inserted behind a boundary that has already committed. A zero-delay packet that a boundary generates waits for the current admission cut to close, then enters the next boundary at the same simulation time.
Some plugins admit input in one phase and apply it later, because their writer runs on a coarser cadence. canwu-society and the culture boundary plugin queue packets in event-driven phase-12 intake systems and apply them at their next Daily or Monthly settlement. canwu-movement settles each leg through the internal scheduled ingress movement_leg_due_v1. See where extension systems run.
Event opportunities and expiry settlement
Section titled “Event opportunities and expiry settlement”A large event system scales better with persistent event opportunities than with a per-frame scan that rolls a die for every entity. A domain plugin creates an event opportunity when a state change, an admitted event, or a declared cadence makes an event possible, and schedules the opportunity at a deterministic due time. At expiry, the plugin settles only the candidates that are due. It writes the random stream, causal source, deduplication key, cooldown, and outcome into boundary evidence.
Keep three steps separate: creating opportunities, settling their outcomes, and publishing reports to actors. A candidate keeps the same outcome after a reload. Different commands or decisions from the same checkpoint create a new alternative reality.
Events and actor knowledge
Section titled “Events and actor knowledge”Authoritative domain state and actor knowledge are stored separately. When an army arrives, its commander may know the new position at once, while a distant actor learns it only after a report is delivered. CanwuViewer reads only the knowledge of the holder it is bound to, so a client sees what that holder knows and nothing more.
Boundary events and replay
Section titled “Boundary events and replay”Events emitted by boundary systems are admitted by the next boundary through the normal admission path. Each successful boundary stores its events, commands, ingress, random draws, changes, producers, phases, visibility, and hashes. Loading and replay recompute this evidence and compare it with the stored copy.
Traces and audits: evidence beside events
Section titled “Traces and audits: evidence beside events”Boundary records hold two more kinds of evidence. An evaluation trace explains how an application rule produced one result for one subject. A transition audit records how a transition manifest settled. Both are hashed into the boundary chain and replayed with it. Neither is a SimEvent or part of state: no system reads a trace back, and an audit is read-only evidence for the systems of the coordinator and participants.
A holder sees a trace only through CanwuViewer::evaluation_traces. It sees traces about its own entity, and traces about another subject that were evaluated after a knowledge publication naming that subject reached its ledger. Research and developer principals see every trace. A read by the public principal fails with InvalidKnowledgeAuthority.