Skip to content

protocol: signal-consumer ledger — every AgentEvent variant declares Behavioral/Surfaced/RecordedOnly{issue}, parity-tested (worker: opus) #2702

Description

@macanderson

Worker model: claude-opus-5 — this designs a new registry in stella-protocol (the architecture crown: types crossing crate boundaries, invariant numbering, a new gate-enforced discipline). Design judgment here is the whole deliverable; a cheaper model risks a ledger that can't stay true. Everything downstream of it (#2703, #2707) is mechanical and goes to Haiku.

Part of epic #2701.

Problem

Signals in this repo are produced and consumed ad hoc, and every gap is invisible until a bench run pays for it. The instances so far: flip.json written with no reader (#1536, since fixed), verify_done confirmations tallied but not feeding the halt until 0367554 (#2661), the flip transition still emitting no durable event, the observatory surfacing 8 of ~41 event types. Each was fixed as whack-a-mole. The content-free encoder registry (crates/stella-store/src/content_free.rs) proves the structural alternative: a reviewed ledger + parity tests that make an undeclared gap a compile/test failure instead of a field incident.

What to build

A signal-consumer ledger in stella-protocol, mirroring content_free.rs's shape exactly (trait/const ledger → registered_*() → parity tests → negative controls):

// crates/stella-protocol/src/event/consumers.rs  (new module)

/// Where an emitted signal's value is realized. Every `AgentEvent` variant
/// must declare one posture. `RecordedOnly` is the DRAIN_FORMATS
/// `NotYetBuilt` analog: a declared, issue-tracked gap — never a silent one.
pub enum ConsumerPosture {
    /// Something *branches* on this event — a halt, a verdict, a projection
    /// that feeds a decision. `site` cites the consuming code
    /// (`"crates/stella-pipeline/src/verify.rs::has_flip_receipt"`).
    Behavioral { site: &'static str },
    /// Rendered to a human. Names each surface so "TUI-only" is a visible
    /// fact, not an assumption.
    Surfaced { surfaces: &'static [Surface] },
    /// Persisted + replay-tagged only. Legal ONLY with a tracking issue —
    /// an empty issue string fails the parity test.
    RecordedOnly { issue: &'static str },
}

#[derive(PartialEq, Eq, Debug)]
pub enum Surface { Tui, Observatory, Serve, Replay }

pub struct SignalConsumers {
    /// Must match an entry in KNOWN_TYPE_TAGS (event/tests/tag_table.rs).
    pub type_tag: &'static str,
    pub posture: ConsumerPosture,
}

/// One row per AgentEvent variant. The parity tests below make this total.
pub const SIGNAL_CONSUMERS: &[SignalConsumers] = &[ /* populated in S2 */ ];

Parity tests (in-crate #[test], so make test is the gate — no new gate step needed, same as every_built_drain_format_has_a_registered_encoder at content_free.rs:878-901):

  1. Totality both directions against KNOWN_TYPE_TAGS (crates/stella-protocol/src/event/tests/tag_table.rs:13+): every tag has exactly one ledger row; every ledger row names a real tag. Adding an AgentEvent variant without a ledger row must be a red test.
  2. RecordedOnly must cite a non-empty issue — the exact assertion shape of the NotYetBuilt arm.
  3. Tags unique (the registered_encoder_ids_are_unique analog).
  4. Negative control: a test proving the harness itself can fail (a deliberately malformed fixture row is caught), per content_free.rs:940-1010's pattern — a harness that cannot fail is a claim, not a check.

Also in this PR

  • Append architectural invariant Restore build + line-by-line bug/security/hardening sweep #9 to AGENTS.md § "Architecture: ports, not concretions": "Every emitted signal names its consumer" — one paragraph pointing at consumers.rs as the enforced home, the way feat: port Phase 3/4/5 crates (context/fleet/graph/mcp/media/pipeline/tui) from the monorepo #3 points at content_free.rs. Append only, never renumber — the numbering is an address (scripts/check-invariants.sh guards this).
  • Module doc on consumers.rs stating the honest limit: the ledger makes a gap declared and reviewed, not mechanically proven consumed — site strings are documentation for humans; the machine-checked half is totality + the issue-citation requirement. (Same epistemic position as DRAIN_FORMATS.)

Constraints

Definition of done

  • consumers.rs exists with the three-posture enum, the ledger const, and the four tests above, matching content_free.rs's structure closely enough that a reader of one recognizes the other (name the exemplar in the PR description).
  • Invariant Restore build + line-by-line bug/security/hardening sweep #9 appended; make invariants green.
  • make gate green; no baseline/allow-list widened.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:protocolstella-protocol — shared types & portsgoal:tracesPillar 2 — every trace perfectly labelled, always: correct, complete, joinable

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions