Eventing v2: grammar, feed, triggers, and receipts¶
Bernstein records what happens across several subsystems: the SSE stream, the TriggerEvent sources, the HMAC-chained audit log, the per-step journal, and the telemetry taxonomy. Eventing v2 unifies them behind one canonical grammar that is a deterministic projection over the audit chain, so the thing you query, alert on, and act from is the chain itself rather than a sixth event system.
Source of truth: src/bernstein/core/events/.
The canonical grammar¶
A canonical event is a pure projection of one audit-chain entry:
| Field | Meaning |
|---|---|
hmac | The audit entry HMAC. This is the event's chain-position identity. |
prev_hmac | The predecessor entry HMAC. Completeness and order verify against this. |
resource_id | The primary resource the event concerns. |
label | Dot-delimited label path, for example task.completed or gate.result. |
related_resource_ids | Sorted lineage ancestor ids: the edges the sequence layer walks. |
payload_digest | sha256: digest of the entry's details. |
The projection is a pure function of the entry bytes. Two hosts holding the same on-disk chain bytes project byte-identical canonical events, because every field is either copied from the entry or a deterministic hash of the entry's own bytes.
Querying the feed¶
The response is a canonical JSON envelope that embeds the from/to HMAC fence-posts of the underlying chain slice plus the projected events. Because the serialisation is canonical (sorted keys, minimal separators, UTF-8), the output is byte-identical across hosts for the same window and can be hashed, diffed, or shipped to a downstream verifier.
Offline verification: completeness and order¶
verify needs no signing key. It checks the window's internal prev_hmac linkage against the embedded fence-posts:
- completeness: the first event's
hmacequals the lower fence-post and the last event'shmacequals the upper fence-post, so no event was dropped from either end; - order: every event's
prev_hmacequals its predecessor'shmac.
Deleting, inserting, or reordering any single event inside the window breaks the linkage or moves a fence-post and fails the check. "What happened, in what order" settles by chain position, not by timestamps scattered across five files.
Composable trigger semantics¶
Every evaluator is a pure fold over a projected window, so a fire decision can be re-derived from a chain slice alone.
- Threshold: N matching events within a window (a span of chain positions), with
for_eachbucketing on grammar labels (resource_id,actor,label). Example: three gate failures on one adapter version within ten positions. - Absence: after event A, expect event B within N positions, else a violation whose bounds are two named chain positions. This is the negative-proof input.
- Sequence: an ordered pair keyed on lineage descent, not wall-clock order. The later event fires only when it provably descends from the earlier one through the
related_resource_idsedges. Two unrelated events in the right wall-clock order never fire. - Compound: any / all / count over a set of label conditions.
Receipt-backed actions¶
Rules dispatch typed actions (notify, pause or resume a schedule, cancel or suspend a task, clamp a budget envelope, drain the warm pool, pin an adapter to its last green version, force a checkpoint-fork retry). Automation runs under the same receipt discipline as everything else:
- A rule fire mints a fire receipt on the chain (rule hash, matched event HMACs, rendered action digest) before the effect executes.
- The effect runs only if the receipt authorises exactly that rendered action; an executed action whose digest no receipt commits to is rejected.
- The executed action is itself a chain event, referencing its fire receipt and the triggering event, so automation is observable in the same feed it reacts to.
The same rule against the same event bytes renders a byte-identical action, so the receipt's commitment is reproducible by any verifier.
An expired absence expectation writes an expectation-expired receipt carrying a negative proof: no event matching a label exists between two named chain positions. A verifier confirms it against the stored window; injecting a matching event into the window makes the receipt fail.
Webhook payload templates¶
The generic inbound webhook path accepts a new external system's POST through configuration instead of code. A declarative template maps JSON payload paths to canonical event fields. The discipline is content-addressing before rendering: the raw payload bytes are hashed into the chain first, so a render is always reproducible from the recorded bytes, and a render failure emits a diagnostic feed event carrying only the payload digest, never the payload.
Compatibility¶
Eventing v2 is additive. Existing SSE consumers and existing triggers.yaml configs work unchanged, and the create-task action path produces identical results for existing configs. Counter and expectation state lives per project under .sdd/runtime/triggers/, isolated across concurrent workers.