Skip to content

feat(compass): durable CommitConversationFrame sink-ack handler - #43

Open
seal-agent wants to merge 2 commits into
mainfrom
compass-server-sink-ack-handler
Open

feat(compass): durable CommitConversationFrame sink-ack handler#43
seal-agent wants to merge 2 commits into
mainfrom
compass-server-sink-ack-handler

Conversation

@seal-agent

Copy link
Copy Markdown
Contributor

What

Implements RunnerService.CommitConversationFrame — the durable, at-most-once counterpart to the loss-tolerant PublishEvents/Deliver conversation path (SEA-1364 T3 substrate landed in #32; sink-ack RPC landed in #42). This is the handler that gives the Runner a synchronous, idempotent commit for agent-authored conversation frames.

The handler resolves the relayed session_id to its bound agent account (fail-closed, mirroring RelayCommsCall: unbound → CodeNotFound, never a stale account or bootstrap admin), then commits one agent-authored conversation frame at most once, keyed on the agent-minted idempotency_key. The key threads to the store's client_request_id, so a retried frame hits AppendMessage's ON CONFLICT (author_account_id, client_request_id) path: no second row, no re-fan of MessagePosted, and the original row's id is returned — committed=true on both a fresh commit and a replay.

Layers

  • comms (agent_caller.go): CommitAgentPostKeyed threads idempotency_keyClientRequestId; CommitAgentUpdateKeyed is a documented pass-through (an UPDATE is idempotent by replacement, addressed by message.id, so no dedup key is needed and no store-write signature changes). Both added beside the untouched unkeyed methods the Deliver-path sink still drives.
  • runnerhub (hub.go, relay_comms.go): Hub.CommitConversationFrame (nil-comms → CodeUnavailable; unbound session → CodeNotFound) delegates to commitFrame, a oneof dispatcher (posted → keyed post, updated → keyed update, neither → CodeInvalidArgument). Comms errors propagate as-is (already Connect-coded via edgeError) so the Runner's retryable/terminal split reads the right code; a non-commit is never committed=false with a nil error.
  • handler (handler.go): (*Handler).CommitConversationFrame with the runnerSubjectFrom guard, fully implementing the generated interface.

Contract notes

  • committed: true on a fresh commit AND on an idempotent replay. A non-commit is NEVER committed=false + nil-err — always a Connect status error. The Runner drives at-least-once off err==nil ⇒ committed.
  • message_id: real and replay-stable (the original row id, re-read via AppendMessage's ON CONFLICT path, inserted=false, no re-fan).
  • seq: shipped as 0 (deferred) — the downstream consumer reads neither message_id nor seq today; the proto doc-comment carries the caveat.

Tests

  • 5 hub unit tests: fail-closed NotFound, nil-comms Unavailable, neither-variant InvalidArgument, keyed-attribution happy path (asserts exactly-1 call, correct account attribution, exact frame forwarded, key threaded, committed/message_id/seq), and comms-error code propagation.
  • 4 comms pgtests including a deterministic at-most-once replay proof: replay of the same key returns the original id, ListMessages count stays 1, and store head does not advance (⇒ no re-fan).

Refs SEA-1364

Implement RunnerService.CommitConversationFrame, the durable at-most-once
counterpart to the loss-tolerant PublishEvents/Deliver path (#24 / OQ-3).

The handler resolves the relayed session_id to its bound agent account
(fail-closed, mirroring RelayCommsCall: unbound -> CodeNotFound, never a
stale account or bootstrap admin), then commits one agent-authored
conversation frame at most once keyed on the agent-minted idempotency_key.
The key threads to the store's client_request_id, so a retried frame hits
AppendMessage's ON CONFLICT (author_account_id, client_request_id) path,
writes no second row, does not re-fan MessagePosted, and returns the
original row's id -- committed=true on both a fresh commit and a replay.

- comms: CommitAgentPostKeyed threads idempotency_key -> ClientRequestId;
  CommitAgentUpdateKeyed is a documented pass-through (an UPDATE is
  idempotent by replacement, addressed by message.id, so no dedup key is
  needed and no store-write signature changes). Both added beside the
  untouched unkeyed methods the Deliver-path sink still drives.
- runnerhub: Hub.CommitConversationFrame (nil-comms -> CodeUnavailable;
  unbound session -> CodeNotFound) delegates to commitFrame, a oneof
  dispatcher (posted -> keyed post, updated -> keyed update, neither ->
  CodeInvalidArgument). Comms errors propagate as-is (already Connect-coded
  via edgeError) so the Runner's retryable/terminal split reads the right
  code; a non-commit is never committed=false with a nil error.
- handler: (*Handler).CommitConversationFrame with the runnerSubjectFrom
  guard, fully implementing the generated interface.
- seq shipped as 0 (deferred): the downstream consumer reads neither
  message_id nor seq today; the proto doc-comment carries the caveat.

Tests: 5 hub unit tests (fail-closed NotFound, nil-comms Unavailable,
neither-variant InvalidArgument, keyed-attribution happy path, error
propagation) and 4 comms pgtests including a deterministic at-most-once
replay proof (row count stays 1, original id, store head does not advance).

Refs SEA-1364

Co-Authored-By: seal <noreply@sealedsecurity.com>
@linear-code

linear-code Bot commented Jul 30, 2026

Copy link
Copy Markdown

SEA-1364

Two review-loop test additions on the durable sink-ack handler (PR #43),
both defending an invariant the first-round suite left to inference:

- runnerhub/commit_frame_test.go: a hub UPDATE-arm test symmetric to the
  posted happy path. The ConversationUpdated dispatch arm was proven only at
  the comms layer + via the fake recorder, never through Hub.commitFrame
  itself; this asserts the arm forwards under the bound account with the key
  threaded and returns the UPDATE's own row id (m-99) — a mis-wire that
  returned the posted id or the wrong frame now fails here (verified red).
- runnerhub/auth_test.go: TestAccountTokenRejectedOnEveryRPCPath gains a
  commit_conversation_frame subtest, replacing two dead duplicate
  relay_comms_call slots (Go suffixed #1/#2, so they ran but covered
  nothing new). The new RPC's door-interceptor wiring is now asserted
  Unauthenticated for an account token, closing the door-coverage gap.

Refs SEA-1364

Co-Authored-By: seal <noreply@sealedsecurity.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants