Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/designs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ check enforces the mechanical half. Full rationale:
| DL-018 | The frozen `RunnerService` transport recommendation #2 is socket-only, superseding the earlier off-stdio carrier clause | Active (Matt, 2026-07-22) | [agent comms tools §The frozen transport](agent/compass-agent-comms-tools/design.md#the-frozen-transport-this-rides-was-the-keystone-fork-now-decided) |
| DL-313 | NATS is the single eventing substrate — run as a standalone stack service (alongside Postgres and the OTel collector) in every deployment, reached over `nats://`, with no embedded/in-process mode and no transport phase; single-node or clustered NATS is selected by connection string, never by application code. JetStream is the durable comms-delivery transport; core NATS carries routing/binding invalidation; queue groups partition delivery work. Connect stays the synchronous RPC edge; the agent↔Runner hop stays vsock (RIG-2394); no LISTEN/NOTIFY phase. Supersedes DL-014 and DL-021 (RIG-2861 OQ-1) | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#q3--the-eventing-substrate-one-nats-eventfabric-a-standalone-stack-service-jetstream-as-the-delivery-transport) |
| DL-316 | Server↔Runner transport is TWO-PLANE (amends DL-013's Runner↔Server clause, RIG-2861 OQ-5 Variant B): async command-push + Runner event fan-in ride NATS (per-Runner command subjects + queue-group fan-in — `Sessions`/`PublishEvents` reshaped to pub/sub for the non-sticky-wake fabric); the typed request/reply legs (enrollment, unary `Relay*Call`s/`CommitConversationFrame`/`FetchSecrets`, bulk `FetchAgentConfig`) stay on the reduced Connect/gRPC edge (deadline propagation, typed proto errors, generated stubs); the per-Runner provisioned token is retained as the NATS-credentials seed via auth-callout. Client↔Server stays Connect; Runner↔Agent stays vsock. Supersedes DL-013 | Active (Matt, 2026-08-31) | [multi-tenancy & NATS substrate](infra/runtime/compass-managed-multitenancy/design.md#resolved-decisions-freeze--matt-2026-08-31) |
| DL-327 | The delivery work-queue consumer's JetStream ack for a HELD deliver (agent-authored message held until its author's session settles) is ACK-ON-RECEIVE (OQ-1): the fabric callback acks as soon as the message is classified and held/dispatched, not at fire — the held registry stays in-RAM and a crash between hold and fire recovers via the Postgres delivery-cursor sweep, exactly as today's in-process bus. Ack-on-fire is rejected (AckWait=30s << an agent turn ⇒ healthy held messages redeliver mid-turn and DLQ-park after MaxDeliver=5, absent per-message InProgress heartbeats) | Active (Matt, 2026-09-05) | [delivery cutover §OQ-1](infra/runtime/compass-managed-delivery-cutover/design.md#oq-1-load-bearing-jetstream-ack-timing-for-held-delivers) |
| DL-328 | The migrated delivery consumer runs `onEventRef` (re-read + classify + hold/dispatch) DIRECTLY on the fabric callback goroutine under `c.mu` + the per-session gates (OQ-2), concurrent with the settle/start drain loop — NOT enqueued onto a single goroutine. The relaxed cross-channel ordering is one today's non-deterministic `select` never actually guaranteed; the cursor sweep remains the no-loss floor. Two Option-A obligations are invariants, not forks: `scanMissedMentions`'s held-check and `MarkMentionsRouted` run under one critical section (scan-vs-hold), and per-callback dispatch work stays bounded so the ack does not block past AckWait behind a long `sweepSession` | Active (Matt, 2026-09-05) | [delivery cutover §OQ-2](infra/runtime/compass-managed-delivery-cutover/design.md#oq-2-load-bearing-concurrency-model--callback-direct-vs-loop-enqueue) |
| DL-329 | The delivery consumer splits its DB role once each event carries an explicit `ref.Tenant` (OQ-3 part 1): the inherently cross-tenant background sweeps/drains keep `WithSystemRole` (BYPASSRLS), but per-event processing runs under `store.WithTenant(baseCtx, ref.Tenant)` for the `MessageByID` re-read and the whole `onMessagePosted` chain — and `heldEntry` gains a `tenant` field so the `fireHeld` re-read is tenant-scoped too. Fail-closed: a forged/corrupted ref whose row belongs to another tenant reads zero rows under RLS instead of cross-tenant-delivering under BYPASSRLS. Whole-loop system-role (ref.Tenant routing-only) rejected — forfeits the stamped ref's isolation dividend | Active (Matt, 2026-09-05) | [delivery cutover §OQ-3](infra/runtime/compass-managed-delivery-cutover/design.md#oq-3-load-bearing-rls-scope-split--lag-recovery-replacement) |
| DL-330 | Recovery after a publish-SIDE fabric failure (commit ok, publish fails — the loss mode the infallible bus never had) is triggered by a FABRIC-RECONNECT HOOK plus a minutes-scale PERIODIC FLOOR TICK, each running `sweepAllLive` + `scanMissedMentions` (OQ-3 part 2). This replaces the deleted `sub.Lagged()` bus-ring branch. The draft's original mapping was falsified by the design-critic red-team: `scanMissedMentions` routes only mentions/ask-answers (never plain delivers) and NATS auto-reconnect (`MaxReconnects(-1)`) keeps the ConsumeContext alive so a "re-subscribe" trigger never fires across an outage — leaving a publish-failed plain deliver to an always-live agent silently undelivered until its next session restart. `sweepAllLive` (not just the scan) is the load-bearing plain-deliver recovery path; publisher-side bounded retry MAY be added but is not sufficient alone | Active (Matt, 2026-09-05) | [delivery cutover §OQ-3 part 2](infra/runtime/compass-managed-delivery-cutover/design.md#oq-3-load-bearing-rls-scope-split--lag-recovery-replacement) |
| DL-334 | The delivery work-queue consumer's JetStream ack for a HELD deliver (agent-authored message held until its author's session settles) is ACK-ON-RECEIVE (OQ-1): the fabric callback acks as soon as the message is classified and held/dispatched, not at fire — the held registry stays in-RAM and a crash between hold and fire recovers via the Postgres delivery-cursor sweep, exactly as today's in-process bus. Ack-on-fire is rejected (AckWait=30s << an agent turn ⇒ healthy held messages redeliver mid-turn and DLQ-park after MaxDeliver=5, absent per-message InProgress heartbeats) | Active (Matt, 2026-09-05) | [delivery cutover §OQ-1](infra/runtime/compass-managed-delivery-cutover/design.md#oq-1-load-bearing-jetstream-ack-timing-for-held-delivers) |
| DL-335 | The migrated delivery consumer runs `onEventRef` (re-read + classify + hold/dispatch) DIRECTLY on the fabric callback goroutine under `c.mu` + the per-session gates (OQ-2), concurrent with the settle/start drain loop — NOT enqueued onto a single goroutine. The relaxed cross-channel ordering is one today's non-deterministic `select` never actually guaranteed; the cursor sweep remains the no-loss floor. Two Option-A obligations are invariants, not forks: `scanMissedMentions`'s held-check and `MarkMentionsRouted` run under one critical section (scan-vs-hold), and per-callback dispatch work stays bounded so the ack does not block past AckWait behind a long `sweepSession` | Active (Matt, 2026-09-05) | [delivery cutover §OQ-2](infra/runtime/compass-managed-delivery-cutover/design.md#oq-2-load-bearing-concurrency-model--callback-direct-vs-loop-enqueue) |
| DL-336 | The delivery consumer splits its DB role once each event carries an explicit `ref.Tenant` (OQ-3 part 1): the inherently cross-tenant background sweeps/drains keep `WithSystemRole` (BYPASSRLS), but per-event processing runs under `store.WithTenant(baseCtx, ref.Tenant)` for the `MessageByID` re-read and the whole `onMessagePosted` chain — and `heldEntry` gains a `tenant` field so the `fireHeld` re-read is tenant-scoped too. Fail-closed: a forged/corrupted ref whose row belongs to another tenant reads zero rows under RLS instead of cross-tenant-delivering under BYPASSRLS. Whole-loop system-role (ref.Tenant routing-only) rejected — forfeits the stamped ref's isolation dividend | Active (Matt, 2026-09-05) | [delivery cutover §OQ-3](infra/runtime/compass-managed-delivery-cutover/design.md#oq-3-load-bearing-rls-scope-split--lag-recovery-replacement) |
| DL-337 | Recovery after a publish-SIDE fabric failure (commit ok, publish fails — the loss mode the infallible bus never had) is triggered by a FABRIC-RECONNECT HOOK plus a minutes-scale PERIODIC FLOOR TICK, each running `sweepAllLive` + `scanMissedMentions` (OQ-3 part 2). This replaces the deleted `sub.Lagged()` bus-ring branch. The draft's original mapping was falsified by the design-critic red-team: `scanMissedMentions` routes only mentions/ask-answers (never plain delivers) and NATS auto-reconnect (`MaxReconnects(-1)`) keeps the ConsumeContext alive so a "re-subscribe" trigger never fires across an outage — leaving a publish-failed plain deliver to an always-live agent silently undelivered until its next session restart. `sweepAllLive` (not just the scan) is the load-bearing plain-deliver recovery path; publisher-side bounded retry MAY be added but is not sufficient alone | Active (Matt, 2026-09-05) | [delivery cutover §OQ-3 part 2](infra/runtime/compass-managed-delivery-cutover/design.md#oq-3-load-bearing-rls-scope-split--lag-recovery-replacement) |
| DL-331 | SINGLE-INSTANCE is a transitional deployment constraint for the delivery cutover (OQ-4, design-critic HIGH): the cutover moves the delivery TRIGGER cross-instance (durable queue-group single-claim) but the dispatch plane it feeds — `SessionForAccount`/`LiveAgentSessions`, the held registry, settle edges, per-session gates — is instance-local hub RAM, so a two-instance deploy breaks hold/settle for a message claimed by the instance not hosting the author's session (immediate fire from partial mid-turn blocks, late-block mention loss, undelivered settled suffix). A single Server is assumed until the parent record's durable session bindings land (parent T4, sequenced after this cutover); this record's two-instance integration proof is scoped to fabric TRANSPORT claim semantics only, not multi-instance delivery correctness | Active (Matt, 2026-09-05) | [delivery cutover §OQ-4](infra/runtime/compass-managed-delivery-cutover/design.md#oq-4-load-bearing-cross-instance-session-locality-single-instance-transitional-constraint) |
| DL-332 | Publishing `message_posted` on BOTH the in-process `events.Bus` (client gRPC stream + presence, until the client edge migrates) and the fabric (delivery) during the phased, multi-step migration is NOT a violation of DL-313's "one eventing substrate — NATS only": that constraint bans a second SWAPPABLE `EventFabric` implementation (an in-process channel impl of the seam), not the pre-existing bus coexisting during the phased cutover. The two publishes serve disjoint consumer sets (no consumer reads both), so no double-handling occurs; the transitional shape ends when the client edge migrates and the bus retires. Interpretation frozen here (per the red-team) so the bus-retirement inherits it explicitly | Active (Matt, 2026-09-05) | [delivery cutover §double-publish](infra/runtime/compass-managed-delivery-cutover/design.md#why-the-double-publish-is-not-a-global-constraint-violation) |
| DL-333 | The OQ-3-part-2 recovery trigger's fabric-reconnect hook (DL-330) needs a seam the frozen 3-method `EventFabric` (DL-316/DL-313 — `Publish`/`Subscribe`/`SubscribeKind`) does not expose; the fabric's own `ReconnectHandler` is log-only, set once at `New()`, and REPLACED (not chained) via `Config.Options` (a replacing caller loses the fabric's outage diagnostics). Ruling: ADD an `EventFabric` method `OnReconnect(fn func()) (Unsubscribe, error)` on the interface and `*Fabric`, chained onto the fabric's existing reconnect handler so its log survives, reached by the delivery consumer through the interface value it holds. Grows the seam to four methods (accepted, consistent with the `SubscribeKind` precedent). Assembly-side wiring through `Config.Options` rejected — splits the trigger across packages and re-implements the fabric's diagnostics | Active (Matt, 2026-09-05) | [delivery cutover §OQ-3](infra/runtime/compass-managed-delivery-cutover/design.md#oq-3-load-bearing-rls-scope-split--lag-recovery-replacement) |
| DL-333 | The OQ-3-part-2 recovery trigger's fabric-reconnect hook (DL-337) needs a seam the frozen 3-method `EventFabric` (DL-316/DL-313 — `Publish`/`Subscribe`/`SubscribeKind`) does not expose; the fabric's own `ReconnectHandler` is log-only, set once at `New()`, and REPLACED (not chained) via `Config.Options` (a replacing caller loses the fabric's outage diagnostics). Ruling: ADD an `EventFabric` method `OnReconnect(fn func()) (Unsubscribe, error)` on the interface and `*Fabric`, chained onto the fabric's existing reconnect handler so its log survives, reached by the delivery consumer through the interface value it holds. Grows the seam to four methods (accepted, consistent with the `SubscribeKind` precedent). Assembly-side wiring through `Config.Options` rejected — splits the trigger across packages and re-implements the fabric's diagnostics | Active (Matt, 2026-09-05) | [delivery cutover §OQ-3](infra/runtime/compass-managed-delivery-cutover/design.md#oq-3-load-bearing-rls-scope-split--lag-recovery-replacement) |

## Storage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Status: Active
Ratified: OQ-1..OQ-4 decided by Matt (2026-09-05, see Resolved decisions); frozen on merge
Parent: `docs/designs/infra/runtime/compass-managed-multitenancy/design.md` (frozen), T3
Ledger-impact: appends DL-327..333 for the OQ-1/OQ-2/OQ-3/OQ-4 rulings, the reconnect-seam shape, and the double-publish interpretation (design-ledger-gate)
Ledger-impact: appends DL-331..337 for the OQ-1/OQ-2/OQ-3/OQ-4 rulings, the reconnect-seam shape, and the double-publish interpretation (design-ledger-gate)

## Problem / Intent

Expand Down Expand Up @@ -132,7 +132,7 @@ an IMMEDIATE in-process trigger (`sub.Lagged()` → `sweepAllLive` +
with the lag branch; and (2) `scanMissedMentions` routes ONLY mentions +
ask-answers (`scan.go:35-70`), never plain delivers, so it recovers a
publish-failed PLAIN message not at all. **What triggers full-set recovery after
a publish-side failure is resolved by OQ-3 part 2 / DL-330: a fabric-reconnect
a publish-side failure is resolved by OQ-3 part 2 / DL-337: a fabric-reconnect
hook plus a minutes-scale periodic floor tick, each running `sweepAllLive` +
`scanMissedMentions`.**
No outbox table in this PR — Postgres remains "the sole durability source of
Expand Down Expand Up @@ -404,7 +404,7 @@ exists anymore) and re-derive the no-loss argument from JetStream durability.
overrun.
- **Test cycle:** a red-green unit test that a publish-failed PLAIN
(non-mention) message to a live, never-restarting recipient IS recovered by
the ruled trigger — this is the DL-330 silent-stall hole the red-team
the ruled trigger — this is the DL-337 silent-stall hole the red-team
promoted to CRITICAL, so the record's headline recovery ruling ships with a
test proving it closes; a test that the start-time scan still runs before the
first event; and a test that `OnReconnect`'s chained callback fires the
Expand All @@ -417,7 +417,7 @@ exists anymore) and re-derive the no-loss argument from JetStream durability.

- **Interfaces:** consumes `docs/designs/DECISIONS.md`; this record. Produces:
the changelog entry and this record's cross-references. The DL rows for the
ratified OQ rulings and the reconnect-seam shape (DL-327..333, incl. the
ratified OQ rulings and the reconnect-seam shape (DL-331..337, incl. the
double-publish-is-not-a-Global-Constraint-violation interpretation) landed
WITH this record's own freeze PR per the "Ledger delta owed" Global
Constraint — they are NOT re-produced here (the append-only unique-ID rule
Expand Down Expand Up @@ -445,7 +445,7 @@ exists anymore) and re-derive the no-loss argument from JetStream durability.
(`sweepAllLive` / `scanMissedMentions`) per OQ-3 part 2; PRODUCES the
reconnect seam (does not exist yet); lands in T2's PR; plain-deliver
recovery test
- [ ] T6: changelog + record cross-references (DL-327..333 already landed with
- [ ] T6: changelog + record cross-references (DL-331..337 already landed with
this record's freeze PR)

## Resolved decisions
Expand Down
Loading