Skip to content

feat(forge): wire agent forge subscribe/unsubscribe — store writer + DL-053 GC (RIG-2732) - #631

Open
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-server/rig-2732-forge-sub-writer
Open

feat(forge): wire agent forge subscribe/unsubscribe — store writer + DL-053 GC (RIG-2732)#631
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-server/rig-2732-forge-sub-writer

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

Lights up the two dormant forge subscription tools (forge_subscribe / forge_unsubscribe) at the store + server layer — the first piece of the forge agent-notification slice (RIG-2732). Both server arms previously returned CodeUnimplemented because agent_forge_subscriptions landed writer-less (poll-driver design, DL-163). This adds the writer and wires the arms.

Store writer (go/internal/store/forge_subscriptions.go, new)

  • EnsureAgentForgeSubscription — idempotent insert keyed by the UNIQUE (agent_account_id, provider, host, repo, kind, number). A repeat subscribe by the same agent to the same artifact returns the EXISTING subscription id (the ON CONFLICT … DO UPDATE is a no-op touch that makes RETURNING id fire on the conflict path), never a duplicate row. An unknown agent (FK RESTRICT) → ErrInvalidArgument.
  • DeleteAgentForgeSubscription — deletes by id scoped to the calling agent (WHERE id = $1 AND agent_account_id = $2); zero rows (unknown id, or an id owned by another agent) → ErrNotFound. Runs the DL-053 garbage-collection invariant in one transaction: after removing the subscription row, its coordinate's forge_artifact_cursors row is collected IFF that was the last subscription for the coordinate (a NOT EXISTS guard leaves the cursor in place while any other agent still subscribes). The coordinate comes from the deleted row's RETURNING.
  • AgentForgeSubscriptionsForArtifact — a minimal row-count reader for the GC/idempotency assertions. NOT the poll driver's subscriber-enumeration reader (that is Piece 2).

This file never inserts a forge_artifact_cursors row — the poll driver owns that writer. The only cursor-table touch here is the last-subscription GC delete.

Server arms (go/server/forge.go)

Wired the two CodeUnimplemented stubs: subscribeForge (resolve coordinate → map wire ForgeArtifactKind to the store enum, rejecting UNSPECIFIED as in-band invalid_argumentEnsureAgentForgeSubscriptionSubscribed{subscription_id}) and unsubscribeForge (DeleteAgentForgeSubscription by id → Unsubscribed). A subscribe stamps no owner (it authors nothing). Store errors flow through the existing storeForgeError mapping (ErrNotFound → not_found, ErrInvalidArgument → invalid_argument).

Tests (red-green)

  • Store pgtests (forge_subscriptions_pgtest_test.go): idempotent subscribe (same id, one row); two agents on the same coordinate → two distinct ids; delete scoping (unknown id + foreign-agent id both ErrNotFound, owner delete succeeds); DL-053 GC (seed cursor + 2 agents → delete A leaves the cursor, delete B collects it); coordinate validation (zero provider / empty host / empty repo / zero kind / zero number → ErrInvalidArgument).
  • Server default-lane tests (forge_test.go): subscribe returns an id + is idempotent; unspecified kind and empty repo → in-band invalid_argument; unsubscribe succeeds then not_found on repeat; a bogus id → in-band not_found (never a Connect teardown).

Pieces 2 (poll-driver change-detect → ForgeNotification) and 3 (turn-end delivery) follow as separate PRs; the wire path is already proto-provisioned (AgentControl.forge_notification, SessionsResponse.forge_notification).

Verified: go build ./..., go vet, gofmt, compass-go:lint (0 issues), and the affected internal/store / server suites all green (pgtests via the throwaway-container path).

Refs RIG-2732.

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

@linear-code

linear-code Bot commented Aug 25, 2026

Copy link
Copy Markdown

RIG-2732

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-2732-forg.compass-eng-docs.pages.dev

Deployed from compass-server/rig-2732-forge-sub-writer at e371cf0.

…DL-053 GC (RIG-2732)

Lights up the two dormant forge subscription tools (`forge_subscribe` / `forge_unsubscribe`) at the store + server layer — the first piece of the forge agent-notification slice (RIG-2732). Both server arms previously returned `CodeUnimplemented` because `agent_forge_subscriptions` landed writer-less (poll-driver design, DL-163). This adds the writer and wires the arms.

### Store writer (`go/internal/store/forge_subscriptions.go`, new)

- `EnsureAgentForgeSubscription` — idempotent insert keyed by the `UNIQUE (agent_account_id, provider, host, repo, kind, number)`. A repeat subscribe by the same agent to the same artifact returns the EXISTING subscription id (the `ON CONFLICT … DO UPDATE` is a no-op touch that makes `RETURNING id` fire on the conflict path), never a duplicate row. An unknown agent (FK RESTRICT) → `ErrInvalidArgument`.
- `DeleteAgentForgeSubscription` — deletes by id scoped to the calling agent (`WHERE id = $1 AND agent_account_id = $2`); zero rows (unknown id, or an id owned by another agent) → `ErrNotFound`. Runs the **DL-053 garbage-collection invariant in one transaction**: after removing the subscription row, its coordinate's `forge_artifact_cursors` row is collected IFF that was the last subscription for the coordinate (a `NOT EXISTS` guard leaves the cursor in place while any other agent still subscribes). The coordinate comes from the deleted row's `RETURNING`.
- `AgentForgeSubscriptionsForArtifact` — a minimal row-count reader for the GC/idempotency assertions. NOT the poll driver's subscriber-enumeration reader (that is Piece 2).

This file never inserts a `forge_artifact_cursors` row — the poll driver owns that writer. The only cursor-table touch here is the last-subscription GC delete.

### Server arms (`go/server/forge.go`)

Wired the two `CodeUnimplemented` stubs: `subscribeForge` (resolve coordinate → map wire `ForgeArtifactKind` to the store enum, rejecting UNSPECIFIED as in-band `invalid_argument` → `EnsureAgentForgeSubscription` → `Subscribed{subscription_id}`) and `unsubscribeForge` (`DeleteAgentForgeSubscription` by id → `Unsubscribed`). A subscribe stamps no owner (it authors nothing). Store errors flow through the existing `storeForgeError` mapping (`ErrNotFound` → not_found, `ErrInvalidArgument` → invalid_argument).

### Tests (red-green)

- Store pgtests (`forge_subscriptions_pgtest_test.go`): idempotent subscribe (same id, one row); two agents on the same coordinate → two distinct ids; delete scoping (unknown id + foreign-agent id both `ErrNotFound`, owner delete succeeds); **DL-053 GC** (seed cursor + 2 agents → delete A leaves the cursor, delete B collects it); coordinate validation (zero provider / empty host / empty repo / zero kind / zero number → `ErrInvalidArgument`).
- Server default-lane tests (`forge_test.go`): subscribe returns an id + is idempotent; unspecified kind and empty repo → in-band `invalid_argument`; unsubscribe succeeds then not_found on repeat; a bogus id → in-band `not_found` (never a Connect teardown).

Pieces 2 (poll-driver change-detect → `ForgeNotification`) and 3 (turn-end delivery) follow as separate PRs; the wire path is already proto-provisioned (`AgentControl.forge_notification`, `SessionsResponse.forge_notification`).

Verified: `go build ./...`, `go vet`, `gofmt`, `compass-go:lint` (0 issues), and the affected `internal/store` / `server` suites all green (pgtests via the throwaway-container path).

Refs RIG-2732.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-2732-forge-sub-writer branch from 55be3c1 to e371cf0 Compare August 25, 2026 23:31
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.

1 participant