feat(compass): SEA-1364 T3 — commit relayed agent conversation frames to comms - #32
Conversation
|
Cross-branch integration verified locally. This repo has no CI yet, so this is the only integration evidence there is — and it is the check that caught a real defect in the sealed copies of these same diffs (a deleted symbol still cited by a sibling branch, invisible to any single-diff review). Merged all five of this lane's branches together onto All five merge clean, no conflicts. On the combined tree, against a live Worth stating plainly: A per-package throwaway Postgres container also starves under this suite's parallelism (the store package hit its 600s timeout four times over); one shared database runs the same suite in ~30s. Relevant when CI is set up here. |
Greptile SummaryThe PR replaces the observed conversation sink with a durable, account-attributed comms write-through.
Confidence Score: 2/5The PR is not safe to merge until authored updates serialize with membership revocation and surplus ask IDs are reconciled safely. The update authorization reads membership from a statement snapshot without coordinating with concurrent removal, so an edit can persist after revocation; separately, surplus asks bypass server-owned ID reconciliation and can introduce an identifier collision that makes ask resolution ambiguous. Files Needing Attention: go/internal/store/messages.go, go/internal/comms/agent_caller.go
|
| Filename | Overview |
|---|---|
| go/internal/comms/agent_caller.go | Adds durable post/update handling and ask-ID reconciliation; the previously reported surplus-ask reconciliation gap remains. |
| go/internal/store/messages.go | Adds membership-and-authorship-gated block updates; the previously reported concurrent revocation race remains. |
| go/internal/runnerhub/hub.go | Resolves frame identity, dispatches conversation writes, and records refusal and contract-defect diagnostics. |
| go/server/sinks.go | Replaces the observation-only sink with the comms adapter and adds final diagnostic logging. |
| go/server/serve.go | Refactors shutdown draining to close streams, drain configured servers, and report frame diagnostics. |
Reviews (3): Last reviewed commit: "fix(compass): reconcile ask_id on relaye..." | Re-trigger Greptile
Review of record — BLOCKING on 1 finding (medium), parked as an Open Question for MattRan the mandatory review ( OQ (medium, latent) — the UPDATE path strips the immutable
|
c05b74d to
852f079
Compare
This stack of pull requests is managed by Graphite. Learn more about stacking. |
49aa1a0 to
548a8ec
Compare
There was a problem hiding this comment.
Greptile has paused reviews on this repository — it used its 100 free open-source review credits for this billing period. Reviews resume automatically on August 1. To continue before then, an organization admin can keep reviews running past the free credits — those bill as normal usage.
Review-of-record: CLEAN (2 rounds)The ask_id reconciliation fix went through two adversarial review rounds:
Red-green regression tests: Deferred (low, defense-in-depth): SEA-1552The review noted a low: |
…o comms Replaces the observedConversationSink stub with the real comms write-through: a relayed conversation frame now commits a durable Message row under the agent's resolved account instead of being acked and discarded. The Server half is complete; the end-to-end path is still dead and the missing piece is not in this lane. The agent's only conversation emitter never stamps a message id (Message.id is server-assigned by contract), and nothing between the agent and the hub stamps one, so every real frame is an id-less update that addresses no row. Until the Runner reconciles server-minted ids, zero conversation rows commit. Rather than hide that, it is pinned: id-less frames get their own contract-defect classification and counter, distinct from ordinary per-frame refusals, logged at ERROR stating the consequence; a test drives the exact shape the agent emits and asserts today's real behavior; counters are exported and logged on drain. Also: a nil-deref panic in homeChannel for a non-agent account, a silently dropped parent_message_id, and an impossible-state error mapped into the routine-refusal bucket. Co-Authored-By: seal <noreply@sealedsecurity.com>
…hrough The UPDATE write-through mapped frames through blocksFromWire -> askFromWire, which unconditionally stripped ask_id (correct for POST, where the server mints via mintAskIDs, but wrong for UPDATE). UpdateMessageBlocksAsAuthor then rejected the id-less ask, so any ask-bearing update was refused and never persisted. Add an UPDATE-path mapper (updateBlocksFromWire / askFromWireForUpdate) that preserves the wire ask_id, and reconcile it at the comms edge against the stored row before the write: ask_id is immutable once minted, so a scoped, message-id read (store.MessageAskIDs) is race-free and does not touch the store's single-statement authz gate. An id-less update ask is filled from the stored id; a non-empty wire ask_id that disagrees with the stored one is rejected as a distinct, clearly-messaged forged-frame error (still InvalidArgument), never conflated with the generic id-less case. Co-Authored-By: seal <noreply@sealedsecurity.com>
548a8ec to
53dc634
Compare

Replaces the
observedConversationSinkstub with the real comms write-through. This is #900 T3 (SEA-1364).Read this first: what this PR does and does not deliver
The Server half is complete and adversarially verified. The end-to-end path is still dead, and the missing piece is not in this lane.
The first-party agent's only conversation-frame emitter is
EventMapper.#appendBlock(packages/compass-agent/src/mapping.ts:386-393). It emitsconversationUpdatedwith noid— the comment there states the agent has no server id to mint and never stamps one.conversationPostedappears only in the type union and in tests; no production path emits it. Nothing stamps an id in between:relay.goforwards verbatim (pinned byrelay_test.go:187-189, "relay forwards, does not reclassify") and the Runner'sPostConversationFrameis stillUnimplemented.An update addresses a row by id, so an id-less one addresses nothing. Until the Runner reconciles server-minted ids into update frames, zero conversation rows commit. That reconciliation is compass-runner's lane and is deliberately not built here; #900's D-7 already echoes
committed_message_idon the ack, which is the id source it will use.So this PR is not "the conversation write-through works." It is: the Server-side write path is correct, proven, and waiting on its input. The review caught this — my first revision would have replaced a stub that silently discarded 100% of agent turns with a write-through that silently discarded 100% of agent turns, which is the same defect one layer down.
Rather than paper over it, the gap is now loud and pinned:
message.idget their own contract-defect classification and counter, distinct from ordinary per-frame refusals — so total frame loss can never be mistaken for a healthy relay rejecting the odd bad frame. Logged at ERROR stating the consequence outright: "agent conversation relay is misconfigured: NOTHING is being committed to comms."TestDeliverIDLessConversationUpdateIsAContractDefectAndCommitsNothingdrives the exact shape#appendBlockproduces and asserts today's real behavior: zero sink calls,ContractDefects == 1,RefusedFrames == 0, stream survives. It cites the emitter and states it flips to asserting a committed row once reconciliation lands. I mutation-tested it — neutering the shape check turns it red, so it is not vacuous.FrameDiagnostics()(one snapshot under one lock) and logged on drain, so a 100%-refusal steady state is visible without log archaeology.The stale comment that hid this work
sinks.goclaimed the write-through "needs the session→channel mapping T5's store adds." That mapping landed in0003_agent_ownership.sql. Three lanes read that comment and concluded the work was gated — it was gated by prose, not reality. Every trace is deleted; grepping forsession→channel mapping,T5 write-through,T4-minimal, andobservedConversationSinkreturns zero matches.Three forks, ruled before implementation
1. The hub resolves session→account;
ConversationSinkgains the account. Resolution happens once at theDeliversite against the sameaccountForSessionbindingRelayCommsCalluses. One binding site is a security property — two resolvers can disagree, and attribution is the last place you want ambiguity.2.
MessageUpdateduses a forked authorizing update.store.UpdateMessageBlockstakes a bareMessageIDwith no membership and no authorship check — fine where it is used (AnswerAskalready resolved through a membership JOIN and deliberately permits a non-author member to answer), unusable for a relayed id. NewUpdateMessageBlocksAsAuthoris a singleUPDATE … RETURNINGgated on membership AND authorship. Both halves matter: authorship alone lets an account edit its message in a channel it was removed from; membership alone lets any member rewrite another's words. Unknown id, wrong author, and revoked member collapse to one identicalErrNotFound.3. Unbound session fails closed. No fallback, no bootstrap-admin attribution, no silent drop.
enroll()clearssessionAccountson re-enroll deliberately, so a frame crossing a reconnect lands here — ruled behavior, documented with an explicit "do not fix this with a fallback," pinned by a test.Also fixed from review
homeChanneldereferencedacc.Agent, which is nil for a user account (store/accounts.go:318-322) — a real SIGSEGV inside thePublishEventsgoroutine, confirmed RED before the fix. Now a fail-closedCodeFailedPrecondition.parent_message_idwas silently dropped, though plumbed end-to-end on both messages. Threaded, with the docstring now explaining why this field is forwarded whileContainer/ClientRequestIdare not.errNoConversationVariantremappedInvalidArgument→Internal: it marks an impossible state, so it should tear down loudly rather than hide among expected refusals.errors.Newtears the stream down and increments neither counter — pinning the fail-safe that was only implicit inconnect.CodeOf.Verified clean under adversarial review
The authz predicate:
EXISTScorrectly correlated tom.channel_id, one parameter bound to both halves so they cannot be satisfied by different principals,RETURNINGcolumn order matchingscanMessagesexactly, D9 collapse total (identical sentinel, identical text, single round-trip so no timing channel), and genuinely race-free under READ COMMITTED. Verified by diff thatupdateMessageBlocksExec,UpdateMessageBlocks,AnswerAsk, andRequireAgentSessionSubscriberare byte-identical to main — the enumeration-oracle hazard is honored in fact, not just prose.Verification
go build,go vet,gofmt -l(empty),go test -race ./internal/... ./server/...all clean. Real-Postgres tests genuinely executed — including all 13AnswerAskregressions and the non-author-member case a shared-helper change would break.Two known non-issues: the relayed-path idempotency retry is not exercisable (no key reaches
PublishEventsRequest; #894/T2 addsidempotency_key = 4, and the hook is commented at the exact line), andTestServeShutdownWithLiveCommsSubscriberReturnsCleanfails on this base — pre-existing, fixed by #973, verified as a chain (fails on a cleanorigin/mainworktree, passes on #973's branch).Worth recording: my first pgtest run printed
okwhile every test skipped.pgtest.RequireDSNskips when it cannot start a container — podman here is atnum_locks (2048)with 2039 leaked volumes from concurrent agents. Re-ran against a live Postgres to get real execution. Same failure shape as SEA-1510 (pgtest never runs in CI).Targets
mainfor review; lands insealedsecurity/compassafter the port.Spec-impact: docs/specs/product/compass.md
The relayed-events requirement (
:405-417) mandates that an unrecognized frame be "logged and counted rather than silently discarded". This PR extends that posture to a case the spec does not yet name: a frame that is recognized but structurally unusable because the relay's two ends disagree. Deliberately not written into the spec in this PR, because the spec states what the system SHALL do and the honest claim today is that relayed conversation does not commit at all. Writing a requirement describing the working path would put the same fiction in the spec that this PR just removed fromsinks.go. The requirement lands with the Runner-side id reconciliation that makes it true.Ported from
sealedsecurity/sealed#996, unchanged in content — Compass code now lives here, so this is where it ships. The sealed PR is closed in favour of this one.Verification (this repo has no CI yet, so a local run is the only evidence there is):
go build ./...,go vet -tags "pgtest unix" ./...,gofmt -lclean,go test -race ./internal/... ./server/..., and thepgtestsuites for./internal/store/,./internal/comms/,./server/against a livepostgres:17— all green on this branch.