Skip to content

feat(comms): OpenDM handler + AsAccount adapter + relay dispatch + spawn auto-open (RIG-2964) - #757

Merged
mattwilkinsonn merged 3 commits into
mainfrom
compass-server/rig-2964-peer-dm-comms-hub
Aug 30, 2026
Merged

feat(comms): OpenDM handler + AsAccount adapter + relay dispatch + spawn auto-open (RIG-2964)#757
mattwilkinsonn merged 3 commits into
mainfrom
compass-server/rig-2964-peer-dm-comms-hub

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a stack containing 2 PRs:

  1. main
  2. "feat(comms): OpenDM handler + AsAccount adapter + relay dispatch + spawn auto-open (RIG-2964)" (this PR)
  3. test(server): e2e pgtest — peer-DM tasking loop open→post→deliver→reply-lands-in-DM (RIG-2967) #758

Peer-DM task T3: wire the OpenDM RPC end to end and auto-open the manager↔peer DM at spawn. Builds on T1 (proto, #738) and T2 (store dm.go, #746), both merged.

What changed

  • Comms.OpenDM handler (internal/comms/comms.go) — replaces the T1 CodeUnimplemented stub. Resolves the peer owner-namespaced (resolveAgentAccount), guards self-DM (invalid_argument), enforces same-owner authz collapsed oracle-safe to the merged not_found (a cross-owner peer is byte-identical to an unknown one, mirroring ReparentAgent), derives the deterministic sorted-handle name, and runs the whole open in one store tx.
  • internal/comms/dm.go (new) — dmChannelName (sorted-handle dm--<lo>--<hi>), openDMTx (one-tx LockOwnerDMTxEnsureOwnerDMGroupTxUpsertDMChannelTx, mirroring the store's openDM test helper and EnsureCoordinationChannel), and emitDMCreated (best-effort post-commit ChannelChanged, create-only).
  • OpenDMAsAccount adapter (internal/comms/agent_caller.go) — the agent-tool entry, mirroring UpdateChannelMembersAsAccount (errNoActor guard + WithActor + the shared handler path).
  • Relay dispatchCommsCaller gains OpenDMAsAccount; executeCall gains the open_dm arm; fakeCommsCaller gains the recorder.
  • Spawn auto-open (R8) (server/lifecycle.go) — lifecycleService gains a narrow dmOpener seam; SpawnAsAccount sets DmChannelName from one post-spawn site covering fresh/resume/idempotent paths (OpenDM is resolve-or-create, so a re-spawn returns the same DM). A post-spawn open failure is logged and returns an empty dm_channel_name — never a spawn rollback (the DM is recoverable next turn via comms_open_dm).

Tests

  • internal/comms/dm_open_pgtest_test.go (new): same-owner create (kind=DM ∧ mandatory ∧ both parties ∧ deterministic name); reopen resumes the same channel in either handle order; unknown / cross-owner → not_found; self → invalid_argument; empty account → errNoActor; ChannelChanged on create; AsAccount resolve-or-create.
  • internal/runnerhub/relay_open_dm_test.go (new): the open_dm arm forwards under the bound account, result oneof + call_id round-trip, tool error rendered in-band.
  • server/lifecycle_pgtest_test.go: spawn returns a live dm_channel_name whose channel is a real DM; idempotent re-spawn returns the same name.

All green against a real Postgres; build + vet + gofmt + golangci-lint (0 issues) clean.

Spec-impact: none. Refs RIG-2964

Co-authored-by: Matt Wilkinson matt@rigel.build

@linear-code

linear-code Bot commented Aug 30, 2026

Copy link
Copy Markdown

RIG-2964

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-2964-peer.compass-eng-docs.pages.dev

Deployed from compass-server/rig-2964-peer-dm-comms-hub at b510774.

rigel-mintaka added a commit that referenced this pull request Aug 30, 2026
… tests (RIG-2964)

Addresses the T3 review (PR #757):

- **medium (security):** `dmChannelName` was not injective — the `--` delimiter collides when a handle itself contains `--` (the handle grammar permits consecutive hyphens), so pair {a, b--c} and {a--b, c} both derived `dm--a--b--c` and the second open resumed onto the first's channel, cross-adding members (a same-owner private-DM confidentiality break). Switch the separator to `:`, a byte the handle grammar excludes, so `dm:<lo>:<hi>` is injective. New pgtest opens both pairs and asserts DISTINCT channels + uncorrupted membership (RED-verified against the old delimiter).
- **medium (test):** add the R8 headline-safety test — a post-spawn DM-open FAILURE (and a nil opener) never rolls back the spawn: peer placed, `dm_channel_name` empty.
- **low (test):** assert a DM RESUME emits no second ChannelChanged (the created-only guard), canary-gated.
- **low (perf):** `emitDMCreated` takes the already-read channel instead of re-reading it.

Refs RIG-2964

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka and others added 2 commits August 30, 2026 18:43
…awn auto-open (RIG-2964)

Peer-DM task T3: wire the `OpenDM` RPC end to end and auto-open the manager↔peer DM at spawn. Builds on T1 (proto, #738) and T2 (store `dm.go`, #746), both merged.

### What changed

- **`Comms.OpenDM` handler** (`internal/comms/comms.go`) — replaces the T1 `CodeUnimplemented` stub. Resolves the peer owner-namespaced (`resolveAgentAccount`), guards self-DM (`invalid_argument`), enforces same-owner authz collapsed oracle-safe to the merged `not_found` (a cross-owner peer is byte-identical to an unknown one, mirroring `ReparentAgent`), derives the deterministic sorted-handle name, and runs the whole open in one store tx.
- **`internal/comms/dm.go`** (new) — `dmChannelName` (sorted-handle `dm--<lo>--<hi>`), `openDMTx` (one-tx `LockOwnerDMTx` → `EnsureOwnerDMGroupTx` → `UpsertDMChannelTx`, mirroring the store's `openDM` test helper and `EnsureCoordinationChannel`), and `emitDMCreated` (best-effort post-commit `ChannelChanged`, create-only).
- **`OpenDMAsAccount` adapter** (`internal/comms/agent_caller.go`) — the agent-tool entry, mirroring `UpdateChannelMembersAsAccount` (`errNoActor` guard + `WithActor` + the shared handler path).
- **Relay dispatch** — `CommsCaller` gains `OpenDMAsAccount`; `executeCall` gains the `open_dm` arm; `fakeCommsCaller` gains the recorder.
- **Spawn auto-open (R8)** (`server/lifecycle.go`) — `lifecycleService` gains a narrow `dmOpener` seam; `SpawnAsAccount` sets `DmChannelName` from one post-spawn site covering fresh/resume/idempotent paths (OpenDM is resolve-or-create, so a re-spawn returns the same DM). A post-spawn open failure is logged and returns an empty `dm_channel_name` — never a spawn rollback (the DM is recoverable next turn via `comms_open_dm`).

### Tests

- `internal/comms/dm_open_pgtest_test.go` (new): same-owner create (kind=DM ∧ mandatory ∧ both parties ∧ deterministic name); reopen resumes the same channel in either handle order; unknown / cross-owner → `not_found`; self → `invalid_argument`; empty account → `errNoActor`; `ChannelChanged` on create; `AsAccount` resolve-or-create.
- `internal/runnerhub/relay_open_dm_test.go` (new): the `open_dm` arm forwards under the bound account, result oneof + `call_id` round-trip, tool error rendered in-band.
- `server/lifecycle_pgtest_test.go`: spawn returns a live `dm_channel_name` whose channel is a real DM; idempotent re-spawn returns the same name.

All green against a real Postgres; build + vet + gofmt + golangci-lint (0 issues) clean.

Spec-impact: none. Refs RIG-2964

Co-authored-by: Matt Wilkinson <matt@rigel.build>
… tests (RIG-2964)

Addresses the T3 review (PR #757):

- **medium (security):** `dmChannelName` was not injective — the `--` delimiter collides when a handle itself contains `--` (the handle grammar permits consecutive hyphens), so pair {a, b--c} and {a--b, c} both derived `dm--a--b--c` and the second open resumed onto the first's channel, cross-adding members (a same-owner private-DM confidentiality break). Switch the separator to `:`, a byte the handle grammar excludes, so `dm:<lo>:<hi>` is injective. New pgtest opens both pairs and asserts DISTINCT channels + uncorrupted membership (RED-verified against the old delimiter).
- **medium (test):** add the R8 headline-safety test — a post-spawn DM-open FAILURE (and a nil opener) never rolls back the spawn: peer placed, `dm_channel_name` empty.
- **low (test):** assert a DM RESUME emits no second ChannelChanged (the created-only guard), canary-gated.
- **low (perf):** `emitDMCreated` takes the already-read channel instead of re-reading it.

Refs RIG-2964

Co-authored-by: Matt Wilkinson <matt@rigel.build>
…t test comments (RIG-2964)

Round-2 review lows: three test doc-comments illustrated the pre-PR 2-arg newLifecycleService signature (now 3-arg with the dmOpener seam), and the resume-emit test comment described a 'scope after create' the assertion does not do (it counts all ChannelChanged for the DM and requires exactly 1). Comment-only — no behavior change.

Refs RIG-2964

Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka added a commit that referenced this pull request Aug 30, 2026
…ly-lands-in-DM (RIG-2967)

Peer-DM task T6: the integration proof of the full tasking loop over the real spine. Stacks on T3 (#757). No production code — consumes T0-T3 (design.md T6:831 "Interfaces: none new").

### What changed

- **`server/dm_e2e_pgtest_test.go`** (new, `//go:build pgtest && unix`) — assembles production's exact delivery wire inline (a real Postgres store, the real `runnerhub.Hub` with a `recordingRunner` door, a real comms service on the comms bus, and the delivery consumer driven by the REAL resume-based waker whose DM opener is that same comms service, so a spawn's auto-open runs through the real T3 handler — mirrors `sinks.go`). Reuses the full-stack placement harness (`attachFakeRunner`/`recordingRunner`) and the offline-mention e2e's control accessors.

### Tests

- `TestPeerDMTaskingLoopEndToEnd` — open (kind=DM ∧ mandatory ∧ both members ∧ created) → A posts naming the DM channel+topic (T0 name-addressed `create_topic`) → B comes live, the message is cursor-swept as a deliver whose recorded source channel+topic names ARE the DM's (the T0 denorm the reply-routing fix hangs on) → B replies naming that same DM channel+topic and the reply lands in the SAME DM topic, asserted two-sided (present in the DM ∧ absent from B's home channel — the dogfood-symptom misroute regression reddens both halves) → re-open is idempotent (same id, created=false) → an outsider same-owner agent's list on the DM collapses to `not_found` (D9 visibility clip).
- `TestPeerDMSpawnPathDelivers` — a manager spawns a peer; the spawn auto-opens the manager↔peer DM and returns a live `dm_channel_name` whose channel satisfies the DM invariants; the spawn runs the real hub Provision→Start so the peer is genuinely hub-live, and a manager post into that DM reaches it via the live fan-out, event-gated on the recording runner.

Every async wait event-gates on an observed wire fact (`waitForControlDelivers`) — never a sleep. Both green against a real Postgres; vet + gofmt clean.

Spec-impact: none. Refs RIG-2967

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@mattwilkinsonn
mattwilkinsonn merged commit afcce53 into main Aug 30, 2026
15 checks passed
@mattwilkinsonn
mattwilkinsonn deleted the compass-server/rig-2964-peer-dm-comms-hub branch August 30, 2026 23:44
mattwilkinsonn added a commit that referenced this pull request Aug 30, 2026
…ly-lands-in-DM (RIG-2967) (#758)

* feat(comms): OpenDM handler + AsAccount adapter + relay dispatch + spawn auto-open (RIG-2964)

Peer-DM task T3: wire the `OpenDM` RPC end to end and auto-open the manager↔peer DM at spawn. Builds on T1 (proto, #738) and T2 (store `dm.go`, #746), both merged.

### What changed

- **`Comms.OpenDM` handler** (`internal/comms/comms.go`) — replaces the T1 `CodeUnimplemented` stub. Resolves the peer owner-namespaced (`resolveAgentAccount`), guards self-DM (`invalid_argument`), enforces same-owner authz collapsed oracle-safe to the merged `not_found` (a cross-owner peer is byte-identical to an unknown one, mirroring `ReparentAgent`), derives the deterministic sorted-handle name, and runs the whole open in one store tx.
- **`internal/comms/dm.go`** (new) — `dmChannelName` (sorted-handle `dm--<lo>--<hi>`), `openDMTx` (one-tx `LockOwnerDMTx` → `EnsureOwnerDMGroupTx` → `UpsertDMChannelTx`, mirroring the store's `openDM` test helper and `EnsureCoordinationChannel`), and `emitDMCreated` (best-effort post-commit `ChannelChanged`, create-only).
- **`OpenDMAsAccount` adapter** (`internal/comms/agent_caller.go`) — the agent-tool entry, mirroring `UpdateChannelMembersAsAccount` (`errNoActor` guard + `WithActor` + the shared handler path).
- **Relay dispatch** — `CommsCaller` gains `OpenDMAsAccount`; `executeCall` gains the `open_dm` arm; `fakeCommsCaller` gains the recorder.
- **Spawn auto-open (R8)** (`server/lifecycle.go`) — `lifecycleService` gains a narrow `dmOpener` seam; `SpawnAsAccount` sets `DmChannelName` from one post-spawn site covering fresh/resume/idempotent paths (OpenDM is resolve-or-create, so a re-spawn returns the same DM). A post-spawn open failure is logged and returns an empty `dm_channel_name` — never a spawn rollback (the DM is recoverable next turn via `comms_open_dm`).

### Tests

- `internal/comms/dm_open_pgtest_test.go` (new): same-owner create (kind=DM ∧ mandatory ∧ both parties ∧ deterministic name); reopen resumes the same channel in either handle order; unknown / cross-owner → `not_found`; self → `invalid_argument`; empty account → `errNoActor`; `ChannelChanged` on create; `AsAccount` resolve-or-create.
- `internal/runnerhub/relay_open_dm_test.go` (new): the `open_dm` arm forwards under the bound account, result oneof + `call_id` round-trip, tool error rendered in-band.
- `server/lifecycle_pgtest_test.go`: spawn returns a live `dm_channel_name` whose channel is a real DM; idempotent re-spawn returns the same name.

All green against a real Postgres; build + vet + gofmt + golangci-lint (0 issues) clean.

Spec-impact: none. Refs RIG-2964

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* fix(comms): injective DM name (colon sep) + R8 rollback-safety & emit tests (RIG-2964)

Addresses the T3 review (PR #757):

- **medium (security):** `dmChannelName` was not injective — the `--` delimiter collides when a handle itself contains `--` (the handle grammar permits consecutive hyphens), so pair {a, b--c} and {a--b, c} both derived `dm--a--b--c` and the second open resumed onto the first's channel, cross-adding members (a same-owner private-DM confidentiality break). Switch the separator to `:`, a byte the handle grammar excludes, so `dm:<lo>:<hi>` is injective. New pgtest opens both pairs and asserts DISTINCT channels + uncorrupted membership (RED-verified against the old delimiter).
- **medium (test):** add the R8 headline-safety test — a post-spawn DM-open FAILURE (and a nil opener) never rolls back the spawn: peer placed, `dm_channel_name` empty.
- **low (test):** assert a DM RESUME emits no second ChannelChanged (the created-only guard), canary-gated.
- **low (perf):** `emitDMCreated` takes the already-read channel instead of re-reading it.

Refs RIG-2964

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(comms): correct stale newLifecycleService signature + resume-emit test comments (RIG-2964)

Round-2 review lows: three test doc-comments illustrated the pre-PR 2-arg newLifecycleService signature (now 3-arg with the dmOpener seam), and the resume-emit test comment described a 'scope after create' the assertion does not do (it counts all ChannelChanged for the DM and requires exactly 1). Comment-only — no behavior change.

Refs RIG-2964

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* test(server): e2e pgtest — peer-DM tasking loop open→post→deliver→reply-lands-in-DM (RIG-2967)

Peer-DM task T6: the integration proof of the full tasking loop over the real spine. Stacks on T3 (#757). No production code — consumes T0-T3 (design.md T6:831 "Interfaces: none new").

### What changed

- **`server/dm_e2e_pgtest_test.go`** (new, `//go:build pgtest && unix`) — assembles production's exact delivery wire inline (a real Postgres store, the real `runnerhub.Hub` with a `recordingRunner` door, a real comms service on the comms bus, and the delivery consumer driven by the REAL resume-based waker whose DM opener is that same comms service, so a spawn's auto-open runs through the real T3 handler — mirrors `sinks.go`). Reuses the full-stack placement harness (`attachFakeRunner`/`recordingRunner`) and the offline-mention e2e's control accessors.

### Tests

- `TestPeerDMTaskingLoopEndToEnd` — open (kind=DM ∧ mandatory ∧ both members ∧ created) → A posts naming the DM channel+topic (T0 name-addressed `create_topic`) → B comes live, the message is cursor-swept as a deliver whose recorded source channel+topic names ARE the DM's (the T0 denorm the reply-routing fix hangs on) → B replies naming that same DM channel+topic and the reply lands in the SAME DM topic, asserted two-sided (present in the DM ∧ absent from B's home channel — the dogfood-symptom misroute regression reddens both halves) → re-open is idempotent (same id, created=false) → an outsider same-owner agent's list on the DM collapses to `not_found` (D9 visibility clip).
- `TestPeerDMSpawnPathDelivers` — a manager spawns a peer; the spawn auto-opens the manager↔peer DM and returns a live `dm_channel_name` whose channel satisfies the DM invariants; the spawn runs the real hub Provision→Start so the peer is genuinely hub-live, and a manager post into that DM reaches it via the live fan-out, event-gated on the recording runner.

Every async wait event-gates on an observed wire fact (`waitForControlDelivers`) — never a sleep. Both green against a real Postgres; vet + gofmt clean.

Spec-impact: none. Refs RIG-2967

Co-authored-by: Matt Wilkinson <matt@rigel.build>

* docs(server): document manager-offline + exact-count invariants in peer-DM e2e (RIG-2967)

Round-2 review lows: comment the load-bearing invariants the spawn-path e2e relies on — the manager is intentionally not hub-live so its DM post fires the deliver immediately (offline-author path, no author-settle hold), and the exact-count deliver check is airtight because B's only owed message is the single DM post. Also clarified the got (event-gate) vs dd (denorm re-snapshot) split. Comment-only — no behavior change.

Refs RIG-2967

Co-authored-by: Matt Wilkinson <matt@rigel.build>

---------

Co-authored-by: Matt Wilkinson <matt@rigel.build>
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