Skip to content
GitHub
Decisions

ADR-064: Notification-shaped reads — event-driven local replica

Status: Accepted (2026-04-28) — D3 broadened 2026-04-30 to cover T3 memory in addition to rule-candidate outcomes; the verdict generalizes to all inter-context notification-shaped reads Supersedes: ADR-055 D3 Option A vs B sub-decision (partial supersession of ADR-055; D1, D2, D4, D5 remain authoritative); ADR-056 D3 Reader Protocol path (partial supersession of ADR-056; D1, D2, D4, D5, D6 remain authoritative)

Context

ADR-055 D3 specified the inter-context mechanism by which the Operations context reads accepted/rejected rule-candidate outcomes recorded by the Worlds context. The original D3 specified a worlds_outcomes_reader SELECT-only SQL grant. ADR-063 removed inter-context SQL grants entirely. ADR-055 D3 was then reframed to leave the replacement mechanism as a SPEC-310 close-out decision between two options:

  • Option A — Worlds emits a richer converse event (rule_candidate_outcome_recorded) on each outcome row insert; Operations materializes a local replica at consume time and subsequent reads come from the same-context replica.
  • Option BRuleCandidateOutcomeReader Protocol minted as a callee-owned OHS Protocol in spectral.worlds.contracts.protocols.* (per the ADR-065 D3 placement); impl in spectral.worlds.application; bridge tool composed in apps/* per ADR-065 D5.

SPEC-253 refinement (the Operations Agent epic) needed the resolution to land — the consumer handler that materializes outcomes and the replica table on which the Operations Agent’s outcome-related queries depend are both squarely in scope. Leaving the framing hedged would force the SPEC-253 implementation to either (a) stage both options or (b) make the choice silently in implementation. Neither is acceptable per “ADRs are why; Codex is what; Linear is when.”

Decision

The inter-context mechanism for notification-shaped reads is event-driven notification with a consumer-side local replica. No Reader Protocols are minted for notification-shaped reads. Concretely:

D1 — Converse outcome event published by worlds

Worlds emits rule_candidate_outcome_recorded on each worlds.rule_candidate_outcomes row insert. The event lives in spectral.worlds.contracts.events.rule_candidate_outcome_recorded per ADR-065 D2 (the original spectral.core.events.signals.worlds.* placement is superseded; producer-owned typed payloads relocate to <producer>.contracts.events.*). The payload carries event_id, rule_candidate_id, outcome (accepted / rejected), reason_code, reason, recorded_at. Idempotent under at-least-once delivery per ADR-044.

The producer-side AC is owned by SPEC-238 (the World Agent epic; AC23). The event payload module is owned by spectral.worlds.contracts.events.rule_candidate_outcome_recorded per ADR-065 D2 (the prior central spectral.core.events.signals.worlds.* placement was retired by ADR-065 D9).

D2 — Operations-side local replica in platform.rule_candidate_outcomes_replica

The Operations consumer materializes outcomes into platform.rule_candidate_outcomes_replica, keyed by event_id (primary key) with rule_candidate_id, outcome, reason_code, reason, recorded_at mirroring the upstream payload. Subsequent Operations reads of “did Worlds accept or reject this candidate?” hit the local replica — never an inter-context SQL connection to worlds.rule_candidate_outcomes.

The consumer handler is idempotent under at-least-once delivery: re-applying an event with the same event_id is a no-op. Eventual consistency is acceptable; the Operations Agent’s outcome-related tools (get_candidates, candidate-detail views) tolerate the replica lag. The consumer-side AC + table migration are owned by SPEC-253.

D3 — Inter-context Reader Protocols are not minted for notification-shaped reads

No Reader Protocol is added for inter-context reads where the consumer’s flow shape is notification (eventual consistency on a fact already recorded elsewhere). The canonical pattern is event-driven local replica per D1 + D2. Two specific surfaces fall under this verdict:

  • RuleCandidateOutcomeReader is not minted in spectral.worlds.contracts.protocols.* (the callee-owned OHS Protocol placement per ADR-065 D3 — worlds owns the rule-candidate outcomes table). Operations consumes via worlds.rule_candidate_outcome_recorded event + Operations-side local replica per D1 + D2 above.
  • T3MemoryReader is not minted in spectral.platform.contracts.protocols.*. Worlds consumes via platform.t3_memory.written event + worlds-local projection (sized to the World Agent’s evolution-loop needs). The opaque-pointer payload framing in ADR-056 D2 evolves under additive-only versioning per ADR-044 D11: payload carries enough body content for worlds-side projection without requiring a separate body-fetch path. Producer-side AC owned by SPEC-242; worlds-side projection consumer + replica table owned by SPEC-238.

The Codex pages that previously hedged with “if call-flow proves preferable” framing have been aligned with the Option A resolution (commit c7dc7aa); the T3MemoryReader Protocol-vs-projection deferred decision is settled in favor of projection.

If a future surface emerges that genuinely needs synchronous request-response semantics over either of these notification-shaped reads (current SPEC-253 / SPEC-238 designs do not), a Protocol can be added at that point — but adding it ahead of a concrete need would invert the precedent set by WorldAgentRunner, which was minted alongside its first concrete consumer.

D4 — Retention + RLS posture for the replica

Per the data-retention sweep (commit 376fbd2):

  • (rule_candidate_outcomes_replica, PLATFORM) → 730d / 30d / STRIP_PAYLOAD — two-year active window matches the upstream worlds-side outcome window so paired Operations-side and Worlds-side outcome history remain co-queryable. STRIP_PAYLOAD removes the reason free-text on tombstone-grace expiry while preserving the action-linkage row.
  • Operator-scoped (consumer of operator-action upstream); no workspace RLS.

Alternatives considered

Option B — RuleCandidateOutcomeReader Protocol via DI. Rejected for SPEC-253. The flow shape is genuinely notification, not call: Operations does not need a synchronous answer at the moment a rule-candidate outcome is recorded — the outcome is a fact, already recorded, that Operations consumes when its own surface needs it. Call-flow DI exists for shapes where the caller dispatches and needs a result back synchronously (ask_world_agent is the canonical example: operator question → World Agent answer → Operations chat surface, all in one in-flight tool call). Forcing notification-shaped reads through a call-flow seam adds correlation IDs, timeouts, and lost-response handling for what is structurally an “I want my own copy of the data” pattern.

The Codex framing this resolves drove the alignment: per contract-surfacescall flow → DI through the framework-layer composition seam; notification flow → events. Eventual-consistency reads of facts already recorded elsewhere belong in notification flow + local replica.

Both options simultaneously (event for live update + Reader for backfill). Rejected. The Reader was originally framed as a fallback if call-flow proves preferable, not as a complement. Running both means twice the surface area to maintain, twice the test matrix, and ongoing ambiguity about which path is canonical. Pick one; pick A.

No replica — query the local outbox / event log directly. Rejected. The outbox is a substrate for delivery, not a queryable view of business state. Querying core.outbox for outcome facts conflates substrate with domain. The local replica is a proper Operations-owned table that participates in retention, RLS, and the workspace lifecycle the same way any other Operations record does.

Consequences

  • Lower inter-context runtime coupling — Operations does not hold a connection or DI handle into spectral.worlds for outcome reads. The dependency surface is the event payload contract only.
  • Idempotency lives in the Operations consumer — re-applied events are no-ops. The consumer handler MUST be tested for at-least-once-delivery semantics. Owned by SPEC-253.
  • Eventual consistency is now the contract for Operations-side outcome reads — the Ops Agent’s outcome-related tools and the candidate-detail UIs surface “as of last replica apply” semantics. In practice the lag is sub-second; the contract is “eventually consistent” not “real-time.”
  • No worlds_outcomes_reader SQL grant ships (already removed by ADR-063; this ADR ratifies the absence going forward).
  • RuleCandidateOutcomeReader is not in the SPEC-236 scope. SPEC-236 nth-wave extensions for SPEC-253 add OutboxReader + OutboxReplayer Protocols (DLQ inspection family, context-internal substrate access) and the failure_cluster_detected event schema — but no outcome Reader.
  • Codex alignmentcontract-surfaces.mdx worked-examples table now names rule_candidate_outcome_recorded + platform.rule_candidate_outcomes_replica directly; the prior “if call-flow proves preferable” hedge is gone (commit c7dc7aa).
  • ADR-055 D1, D2, D4, D5 remain authoritative. Only D3’s Option A vs B sub-decision is resolved here. D3’s outcome-table schema, reason-code enum, additive-only versioning, and backpressure framing all stay live.

References

  • ADR-017 — strict-unidirectional inter-context signal path
  • ADR-044 — event substrate; envelope shape; additive-only versioning
  • ADR-054 — retry semantics for the Worlds consumer
  • ADR-055 — Curation → Worlds interface contract; D3 partially superseded by this ADR
  • ADR-060 — agent tool invocation; RuleCandidateOutcomeReader is not a part of this scope going forward
  • ADR-063 — inter-context access pattern; removed the original D3 grant; framed Options A and B
  • SPEC-253 — Operations Agent; the consumer for rule_candidate_outcome_recorded
  • SPEC-238 AC23 — producer side of the converse event emit
  • SPEC-310 — spike whose close-out this ADR delivers
  • Codex system-design/foundations/contract-surfaces.mdx — worked-examples table aligned (commit c7dc7aa)
  • Codex system-design/agents/agent-tool-invocation.mdx — forward-compatibility framing aligned (commit c7dc7aa)
  • Codex system-design/foundations/contract-surfaces/event-system.mdxrule_candidate_outcome_recorded cataloged (commit 2e9c56b)
  • Codex system-design/foundations/data-retention.mdx — replica retention policy registered (commit 376fbd2)