Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/designs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ check enforces the mechanical half. Full rationale:
| DL-266 | The forge delivery cursor (`delivered_revision`) advances on the agent's in-band forge delivery ack — `ForgeNotificationAck{subscription_id, revision}`, an additive AgentFrame variant emitted at turn-end flush — NEVER on dispatch success (Matt's W3 ruling, option beta), aligning this lane with the dispatch arm's own model ("The cursor is never advanced on send — it advances only later on the recipient's delivery_ack", `go/internal/runnerhub/dispatch_control.go:31-33`). Every pre-ack loss (synchronous refusal, async RunnerError via `router.complete`, agent death before flush) leaves the cursor unadvanced and is healed by the reconcile sweep within one backstop interval. The correlation is new work: today's `delivery_ack` carries only a comms `message_id` (`agent.proto:232-239`). This reopens the frozen delivery record's advance-signal clause (`compass-notification-delivery/design.md:894-897`) and rides the freeze-gate ratification, bundled with the fresh-subscription catch-up baseline moving from DL-053's "at Subscribe time" to first-observed-event/sweep (bounded by ≤1 backstop interval) — also decided in W3's ruling. | Active (Matt, 2026-08-26) | [forge agent-notification §W3](product/compass-forge-agent-notification/design.md#resolved-decisions-matt-2026-08-26) |
| DL-267 | Forge subscriptions gain CONTAINER-SCOPE granularity via an explicit `ForgeSubscriptionScope` enum on `SubscribeForgeRequest` — Matt's W2 ruling, option (b); the `number = 0` sentinel is REJECTED (proto3 absent-vs-0 blindness would convert a forgotten `number` from an LLM tool caller into a silent whole-repo subscription). Containers are PROVIDER-NATIVE: GitHub's container is the REPO (the existing `repo` slot); Linear's is a PROJECT, carried in a slot of its own and never overloaded onto the team key in `repo` — Linear project scope is thereby IN scope (promoted from deferral). The project identifier lives in a dedicated column (OQ-1, ruled (i): additive `scope` + `project` columns, UNIQUE widened). A container subscription delivers exactly `FORGE_NOTIFICATION_KIND_OPENED` with the new artifact's number/url — never a fan-in of every event on every artifact. Two additive `ForgeNotificationKind` values land with it: `REVIEW = 5` (submitted PR review; free on webhooks where the poll design priced no review endpoint) and `OPENED = 6`. | Active (Matt, 2026-08-26) | [forge agent-notification §subscription model](product/compass-forge-agent-notification/design.md#the-subscription-model-folded-per-matts-rulings-3--w2) |
| DL-281 | Board issue ingestion is WEBHOOK-DRIVEN, not polled: the GitHub App webhook ingress (DL-264's `POST /webhooks/github`) fans accepted `issues` events to a board ingest arm that normalizes the repo, gates on `forge_repo_subscriptions` (DL-162, kept), hydrates each coordinate via a conditional GET, and sinks through the one `StripOwner`→`TranslateIssue`→stamp pipeline into `IssueProjection.PublishIssueUpdate`; reliability + cold-start are a bounded per-repo updated-order reconcile sweep (startup + 30-min ticker, per-repo `updated_at` watermark advanced only after sink), the DL-264 backstop pattern at repo-LIST granularity. Retires the DL-161 standing poll driver + `forge_list_cursors`; amends DL-053's "conditional polling in v1" transport clause (webhooks primary, conditional reads only inside the sweep) and DL-163's four-table set (`forge_list_cursors` dropped by editing `0001_init.sql` in place — Compass is pre-live, the init migration is edited directly; additive watermark + `forge_updated_at` recency-guard columns added the same way). The GitHub App is the ONLY GitHub read credential — the static read-path PAT is retired, completing the App-only cutover. Known pre-existing limit (poll had it identically): no transport removes a board row — a forge-deleted/transferred issue persists until manually removed | Active (Matt, 2026-08-27) | [board webhook ingestion §Approach](product/compass-forge-board-webhook-ingestion/design.md#approach) |
| DL-269 | Every request-input account field on the compass proto contract is handle-typed; the Server resolves handle→account_id at the service edge (the `from_handle` posture generalized). An unresolvable, invisible, foreign, or wrong-subtype handle is ONE indistinguishable in-band NOT_FOUND naming the submitted handle — the oracle invariant that forbids a caller distinguishing "exists under another owner" from "no such handle" (the ReparentAgent foreign-parent leg mirrors CreateAgent's same-owner edge pre-check to hold this). No agent or client UI ever resolves an id. | Active (Matt, 2026-08-27) | [handle-addressing cutover §Ledger impact](product/compass-handle-addressing-cutover/design.md#ledger-impact) |
| DL-270 | Response, stored, and event account fields stay id-typed — ids are the stable join keys clients already hold. A response that needs a handle for display carries it as an explicit sibling field (the `RosterEntry.agent_account_id`+`handle` dual), never by retyping the id field; the response-side getters (`SpawnPeerResponse`/`RosterEntry`/`AgentSessionStatus`) keep their id accessors. | Active (Matt, 2026-08-27) | [handle-addressing cutover §Ledger impact](product/compass-handle-addressing-cutover/design.md#ledger-impact) |
| DL-271 | Handle→id resolution is owner-namespaced, stored in a dedicated `account_handles(account_id, handle, owner_user_id NULL)` table with two partial-unique indexes (user/system handles globally unique; agent handles unique per owner). Agents are addressed owner-qualified (`matt/compass-ux`, bare = caller's own owner); users/system bare. Rename is in-place for both tiers; reclaim is allowed for both tiers (no history, tombstone, or reservation) — cross-human reclaim safety is the owner-peering authorization edge (RIG-2796), not handle reservation. The `handle` column stays on `accounts` for display only; `account_handles` is the resolution key. | Active (Matt, 2026-08-27) | [handle-addressing cutover §Ledger impact](product/compass-handle-addressing-cutover/design.md#ledger-impact) |

> Note (2026-07-31, SEA-1570 R5): DL-065's retired internal `ResumeContext
> resume = 12` envelope field is NOT silently reintroduced — the collapsed
Expand Down
37 changes: 32 additions & 5 deletions go/e2e/agent_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ func (f *Fixture) CreateAgent(ctx context.Context, handle, displayName string) (
func (f *Fixture) Provision(ctx context.Context, accountID, clientRequestID string) (containerName string, err error) {
rctx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
resp, err := f.Compass().ProvisionAgentWorkspace(rctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{
AgentAccountId: accountID,
ClientRequestId: clientRequestID,
}))
resp, err := f.Compass().ProvisionAgentWorkspace(rctx, connect.NewRequest(&compassv1.ProvisionAgentWorkspaceRequest{AgentHandle: accountID, ClientRequestId: clientRequestID}))
if err != nil {
return "", fmt.Errorf("ProvisionAgentWorkspace RPC: %w", err)
}
Expand Down Expand Up @@ -337,6 +334,26 @@ func (f *Fixture) waitRunnerEnrolled(ctx context.Context) error {
// the gate.
const rootSupervisorHandle = "supervisor"

// bootstrapAdminHandle mirrors the server's default bootstrap-admin handle
// (server/serve.go bootstrapAdminHandle) — the same cross-package literal
// coupling as rootSupervisorHandle above. The seeded root supervisor is owned by
// this admin, so the owner-qualified AgentByHandle lookup resolves the admin's
// account id through this handle first.
const bootstrapAdminHandle = "admin"

// adminAgentByHandle resolves an admin-owned agent by its bare handle. Every leg
// creates its agents through the fixture's admin-authed CreateAgent, so they all
// live in the bootstrap admin's agent namespace; after the RIG-2751 handle
// cutover AgentByHandle is owner-qualified, so the lookup first resolves the
// admin's account id (bare user handle, global index) then the agent under it.
func adminAgentByHandle(ctx context.Context, st *store.Store, handle string) (store.Account, error) {
admin, err := st.UserByHandle(ctx, bootstrapAdminHandle)
if err != nil {
return store.Account{}, err
}
return st.AgentByHandle(ctx, admin.ID, handle)
}

// waitSeedSettled blocks until the first-launch root-supervisor seed has finished
// provisioning its container, or the budget elapses. It is the seed counterpart
// to waitRunnerEnrolled: the seed (server/serve_seed.go) hangs off the Runner
Expand Down Expand Up @@ -419,7 +436,17 @@ func (f *Fixture) seedSettledProbe(ctx context.Context, st *store.Store, deadlin
}
rctx, cancel := context.WithTimeout(ctx, perProbe)
defer cancel()
sup, handleErr := st.AgentByHandle(rctx, rootSupervisorHandle)
// The supervisor is a root agent owned by the bootstrap admin, so resolve the
// admin's account id first (bare user handle in the global index) and look
// the supervisor up in that owner's agent namespace (AgentByHandle is
// owner-qualified after the RIG-2751 handle cutover). A not-yet-created admin
// or supervisor is the same "still seeding" ErrNotFound the caller polls
// through.
admin, adminErr := st.UserByHandle(rctx, bootstrapAdminHandle)
if adminErr != nil {
return classifySeedSettle(adminErr, nil)
}
sup, handleErr := st.AgentByHandle(rctx, admin.ID, rootSupervisorHandle)
if handleErr != nil {
return classifySeedSettle(handleErr, nil)
}
Expand Down
12 changes: 6 additions & 6 deletions go/e2e/comms_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,25 @@ func (f *Fixture) PostMessage(ctx context.Context, channelID, topicName, text st
return resp.Msg.GetMessage().GetId(), nil
}

// SubscribeMember adds accountID to channelID's membership AND marks it
// SubscribeMember adds memberHandle to channelID's membership AND marks it
// subscribed over CommsService.UpdateChannelMembers, so the account joins the
// channel's DELIVER set. It is the leg-4 second-recipient join: the reused leg-3
// spawner is subscribed-but-unmentioned onto the mentioned peer's home channel,
// making it a plain deliver target while the mentioned peer is steered. Both the
// add and the subscribe lists are set in the one request because the deliver set
// (store SubscribedAgents, delivery_reads.go) requires the member's subscribed
// flag on a non-home, non-mandatory channel — a bare add inserts subscribed=FALSE
// and the account is filtered out of the deliver set; subscribe_account_ids
// and the account is filtered out of the deliver set; subscribe_handles
// requires the account already be a current or added member (comms.proto:644-645),
// so the two travel together. Returns an error rather than panicking so the
// caller (a test) decides fatality; the per-call deadline is threaded from ctx.
func (f *Fixture) SubscribeMember(ctx context.Context, channelID, accountID string) error {
func (f *Fixture) SubscribeMember(ctx context.Context, channelID, memberHandle string) error {
rctx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
if _, err := f.Comms().UpdateChannelMembers(rctx, connect.NewRequest(&compassv1.UpdateChannelMembersRequest{
ChannelId: channelID,
AddMemberAccountIds: []string{accountID},
SubscribeAccountIds: []string{accountID},
ChannelId: channelID,
AddMemberHandles: []string{memberHandle},
SubscribeHandles: []string{memberHandle},
})); err != nil {
return fmt.Errorf("UpdateChannelMembers RPC: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion go/e2e/legcomms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestCommsPostMessageThroughAgentLoop(t *testing.T) {
// Resolve the poster to get its home channel id (the channel the trigger post
// lands on and the channel the agent's own post — channel_id omitted — fans
// onto).
poster, err := st.AgentByHandle(ctx, "comms-leg-poster")
poster, err := adminAgentByHandle(ctx, st, "comms-leg-poster")
if err != nil {
t.Fatalf("AgentByHandle(poster): %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion go/e2e/legfive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func TestLegFivePersistAndResume(t *testing.T) {
// exactly why post1 below must be acked (cursor advanced) before the resume:
// otherwise container2's start-sweep would redeliver it and consume the
// resumed lifetime's canned turn. So each post must precede its settle wait.
acc, err := st.AgentByHandle(ctx, "leg5-persistresume")
acc, err := adminAgentByHandle(ctx, st, "leg5-persistresume")
if err != nil {
t.Fatalf("AgentByHandle: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion go/e2e/legsix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func TestLegSixTeardownIdempotence(t *testing.T) {
t.Fatalf("store.Open (run2): %v", err)
}
defer st.Close()
persisted, err := st.AgentByHandle(ctx, handle)
persisted, err := adminAgentByHandle(ctx, st, handle)
if err != nil {
t.Fatalf("AgentByHandle(%q) (run2): %v — the postgres cluster did not re-attach across the restart, so the account run1 minted did not survive and the deterministic-name collision premise is gone", handle, err)
}
Expand Down
10 changes: 6 additions & 4 deletions go/e2e/legthreefour_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) {
return
}
defer st.Close()
peer, err := st.AgentByHandle(ctx, peerHandle)
peer, err := adminAgentByHandle(ctx, st, peerHandle)
if err != nil {
return
}
Expand Down Expand Up @@ -159,7 +159,7 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) {
// session-start sweep only redelivers messages left undelivered from a prior
// lifetime (relevant only to leg-5's post1), not this one. Must precede the
// settle wait.
spawner, err := st.AgentByHandle(ctx, "leg34-spawner")
spawner, err := adminAgentByHandle(ctx, st, "leg34-spawner")
if err != nil {
t.Fatalf("AgentByHandle(spawner): %v", err)
}
Expand All @@ -178,7 +178,7 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) {
// ── Leg 3: fresh peer account (F2 ownership) + a second real container ──

// The spawn minted a fresh agent account resolvable by its handle.
peer, err := st.AgentByHandle(ctx, peerHandle)
peer, err := adminAgentByHandle(ctx, st, peerHandle)
if err != nil {
t.Fatalf("AgentByHandle(%q): %v — the scripted spawn did not mint the peer account (RED until H3 registers the native spawn tool, design.md:655-659)", peerHandle, err)
}
Expand Down Expand Up @@ -283,7 +283,9 @@ func TestLegThreeFourSpawnAndMessaging(t *testing.T) {

// Subscribe the spawner onto the peer's home channel so it becomes a plain
// deliver target there (the second recipient, no new container).
if err := f.SubscribeMember(ctx, string(peer.Agent.HomeChannelID), spawnerID); err != nil {
// The spawner is a bare agent handle in the caller's own owner namespace
// (created via CreateAgent above); T3 resolves it to the spawner account.
if err := f.SubscribeMember(ctx, string(peer.Agent.HomeChannelID), "leg34-spawner"); err != nil {
t.Fatalf("SubscribeMember(spawner → peer home channel): %v", err)
}

Expand Down
2 changes: 1 addition & 1 deletion go/e2e/legtwo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func TestLegTwoRealTurn(t *testing.T) {
// AwaitTurnSettled waits on. The session-start sweep only redelivers
// messages left undelivered from a prior lifetime (relevant only to leg-5's
// post1), not this one.
acc, err := st.AgentByHandle(ctx, "leg2-realturn")
acc, err := adminAgentByHandle(ctx, st, "leg2-realturn")
if err != nil {
t.Fatalf("AgentByHandle: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion go/e2e/seed_settle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestSeedSettledBeforeFixtureReturns(t *testing.T) {
// return before the seed even created the supervisor.
hctx, hcancel := context.WithTimeout(ctx, rpcTimeout)
defer hcancel()
sup, err := st.AgentByHandle(hctx, rootSupervisorHandle)
sup, err := adminAgentByHandle(hctx, st, rootSupervisorHandle)
if err != nil {
t.Fatalf("root supervisor not resolvable after NewFixture returned: %v (the seed-settle gate did not hold)", err)
}
Expand Down
Loading
Loading