Skip to content

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 accepted command produces an event; the audience rule decides which actor views show it; the next boundary admits it. View diagram source.
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.

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.

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) or Actors(people): the listed people;
  • KnowledgeHolder(holder): one knowledge holder, such as a person or an institution;
  • AffectedActors: the people named in the event’s affected_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.

CauseRef records one of four sources:

  • Command: an accepted engine command or domain command;
  • Event: an earlier recorded event;
  • Boundary: the boundary (by BoundaryId) 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 publication

Every 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.

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.

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.

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.

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.

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.