From c3ff9d72f0016a3386dba75cfee2a75a285c82be Mon Sep 17 00:00:00 2001 From: seal Date: Mon, 10 Aug 2026 11:50:24 -0400 Subject: [PATCH 1/3] =?UTF-8?q?docs(product):=20design=20record=20?= =?UTF-8?q?=E2=80=94=20remove=20initial=5Fprompt,=20channel-first-turn=20d?= =?UTF-8?q?elivery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Freezes the design for removing `initial_prompt` from the whole Compass start contract (proto -> server -> runner -> agent SDK -> UI -> e2e harness). Matt has ruled the field out of existence: an agent session **always starts idle**, and its first turn arrives through a channel — never a prompt field the Runner silently drops today (`(*agentHost).Start` reads only the container name; every prompt riding the field is dropped and the agent idles forever). This record is the contract that unblocks SEA-1792 H8 / PR #256 — the dogfood e2e `TestLegTwoRealTurn` is red precisely because its first turn rides the dropped field. ### What it decides (DL-186..189, appended to the product ledger by T7) - **DL-186** — `initial_prompt` removed from the whole contract (three fields, numbers + names reserved), consumers cut over atomically with no fallback; first turn arrives over the SEA-1569 deliver path. - **DL-187** — `@compass` reserved alias frozen as the system-sender mechanism for **any** system-level message; case-1 root-manager boot scoped out to follow-up **SEA-1820**. - **DL-188** — fresh-start barrier-lift: on a fresh start the Runner sends `AgentControl{replay_complete}` as the first control op after Bind (symmetric with the resume path, no agent change). - **DL-189** — the case-2 brief carrier is a **per-pair manager↔peer DM channel** (`ChannelKindDM`), auto-provisioned on the spawn edge — not the coordination broadcast, so siblings never receive briefs they don't need. ### Staging - **PR-A** (additive, lands first) — Runner deliver-lane build (`DeliverControl` dispatch arm, `representable()` admits Deliver/Steer, fresh-start barrier-lift) + spawn-edge DM auto-provision. Greens nothing yet; independently revertable. - **PR-B** (lands after PR-A) — the atomic `initial_prompt` removal + e2e harness re-model; greens leg-2. ### Honest state The server + agent halves of the deliver seam exist; the **Runner middle leg is UNBUILT** at three source-verified points (dispatch has no `DeliverControl` arm; gateway `representable()` rejects Deliver/Steer; no fresh-start `replay_complete` sender + the agent replay barrier strands a pre-`replayComplete` deliver). The record does not claim case-2 is verified end-to-end today — PR-A builds the leg. ### Open for Matt at this gate - **[FORK]** `StartAgentDialog` disposition — removing the prompt empties the dialog, but DL-185 (SEA-1932, Active) explicitly *keeps* it. Delete vs bare-confirm is Matt's call (recommendation: delete). One conditional status-flip on DL-185's "Kept" clause. - **[Minor]** settle-primitive naming (`AwaitTurnSettled`). Every cited file:line grounded against `main@origin`. No code, codegen, or proto changed — design record only. Spec-impact: none. Refs SEA-1681, SEA-1820. Co-authored-by: Matt Wilkinson --- .../compass-first-turn-delivery/design.md | 655 ++++++++++++++++++ 1 file changed, 655 insertions(+) create mode 100644 docs/designs/product/compass-first-turn-delivery/design.md diff --git a/docs/designs/product/compass-first-turn-delivery/design.md b/docs/designs/product/compass-first-turn-delivery/design.md new file mode 100644 index 00000000..953cc138 --- /dev/null +++ b/docs/designs/product/compass-first-turn-delivery/design.md @@ -0,0 +1,655 @@ +# Compass first-turn delivery — remove `initial_prompt` + +Status: Draft + +## Problem / Intent + +Compass carries an `initial_prompt` field through its whole start contract +(proto → server → runner → agent SDK → UI → e2e harness), but the Runner never +sends it into the container — `(*agentHost).Start` reads only the container +name (`go/internal/runner/host.go:264-265`: `func (h *agentHost) Start(ctx +context.Context, req *compassv1.StartAgentSessionRequest, resumeBody string)` +… `name := req.GetContainerName()`; no `GetInitialPrompt` call exists in the +file), so every prompt riding the field is silently dropped and the agent +idles forever. Matt has ruled the field out of existence: **remove +`initial_prompt` from everything; an agent session always starts idle; its +first turn arrives through its channel.** This record designs the removal and +the channel-first-turn model, and is what unblocks SEA-1792 H8 / PR #256 +(dogfood e2e `TestLegTwoRealTurn` is red today precisely because its first +turn rides the dropped field). + +## Approach + +### The frozen ruling (design HOW, not WHETHER) + +Matt, verbatim: + +> "Managers get started in two ways: 1. the initial root manager that starts +> on boot, and 2. a manager that was provisioned by another manager. For 2, +> the initial prompt would just be a DM from the starting manager to the new +> one — it doesn't need an initial_prompt field. For 1, this is part of the +> whole Setup flow — the manager needs to ask the user what repos/projects, +> set up the tree/devenv shell(s), etc. The best way to do this might be an +> initial Setup thread in their home channel, created by Compass itself. But +> then we need a `@compass` reserved alias for Compass itself to send messages +> into threads. So ultimately I think we get rid of the initial_prompt, and +> drop it from everything: the provisioning Manager sends a message to the new +> one in 99% of cases, and then in the setup one it's a custom flow based +> around an initial Setup thread." + +No `initial_prompt` fallback survives anywhere. The cutover is clean: field +removed and reserved, every consumer flipped in one atomic change. + +### The seam the first turn rides (case 2 — the server + agent halves exist; the Runner middle leg is UNBUILT) + +**Honest state (driver source-verified).** The server-side fan-out/dispatch and +the agent-side idle-deliver arm both exist, but a posted channel message does +NOT reach a production agent session today: the Runner middle leg is unbuilt at +three points. Case 2 therefore needs a **Runner deliver-lane build** (gaps a+b) +plus a **fresh-start barrier-lift** (gap c) — see PR-A (T-R3, the OQ-A ruling +folded below). The three gaps: + +- **Gap (a) — runner dispatch has no `DeliverControl` arm.** + `go/internal/runner/dispatch.go:243` switches on + Start/Provision/Stop/Remove/Reload/Status/SecretsVersion/ConfigVersion only; + a server-pushed `SessionsResponse_DeliverControl` falls to the default and + returns `errorResult(id, errors.New("unrecognized session command variant"))` + (`dispatch.go:320-323`). The server DOES send this variant — + `go/internal/runnerhub/dispatch_control.go:44-53` wraps the op in + `SessionsResponse_DeliverControl` and `router.send1`s it — and the wire + variant exists (`go/internal/gen/compass/v1/runner.pb.go:599,634`). The send + path is live; the receive arm is missing. **T-R1 builds it.** +- **Gap (b) — gateway control lane rejects deliver/steer.** + `go/internal/runner/gateway/control.go:192-203` `representable()` returns + `false` for `AgentControl_Deliver` and `AgentControl_Steer` (and + Replay/Config) — "the four parked variants are empty shells." Even with arm + (a) built, the send is dropped as non-representable. **T-R2 fixes it** (both + now carry a comms `Message`). +- **Gap (c) — no fresh-start `replay_complete` sender + the agent barrier + defaults CLOSED, and a refused deliver is acked away (stranded).** Runner-side + `HoldForReplay` has "No production caller exists yet" + (`go/internal/runner/gateway/control.go:421-429`). Agent-side barrier: + `packages/compass-agent/src/transport/control-source.ts:278` + `let replayComplete = false`; a `deliver`/`steer` arriving before + `replayComplete` is decoded is refused-and-counted ("live immediate op before + ReplayComplete — refused by replay barrier", `control-source.ts:373-377`), + then `acks.markApplied(seq)` (`:390`) acks it — pruning Runner retention, so + it is NEVER redelivered. On a FRESH (non-resume) start nothing sends + `replay_complete`, so the barrier never lifts and the first deliver is + refused-and-stranded. **T-R3 lifts it**: on a FRESH start the Runner sends + `AgentControl{replay_complete}` as the first control op after Bind (Matt's + OQ-A ruling, folded below). + +The pieces that DO exist, and that PR-A/PR-B compose with: + +1. **Fan-out** — the delivery consumer wraps a posted message in the deliver + op: `go/internal/delivery/consumer.go:277-281` — + `func deliverOp(msg *compassv1.Message) *compassv1internal.AgentControl` + builds `AgentControl_Deliver{Deliver: &DeliverControl{Message: msg}}`. +2. **Dispatch** — `consumer.go:42-43`: `ControlDispatcher` is + `DispatchControl(ctx context.Context, sessionID string, op + *compassv1internal.AgentControl) error`; `:55` `LiveAgentSessions() map + [store.AccountID]string` resolves live recipients. +3. **Relay** — `go/internal/runnerhub/dispatch_control.go:38-53` + `DispatchControl` wraps the op and `router.send1`s it down the server→runner + `SessionsResponse` stream — the send that gaps (a)+(b) currently strand. +4. **Turn start** — `packages/compass-agent/src/agent.ts:183` `deliver(msg: + Message)` with the RT-3 contract at `:83-85`: "A delivered channel message + is coalesced to a turn-end prompt: mid-turn delivers queue and flush as ONE + prompt when the turn settles; **an idle deliver starts a turn at once.**" + This is downstream of the barrier (gap c) — a pre-`replayComplete` deliver is + refused at `control-source.ts:373-390` before it ever reaches `deliver()`. +5. **Owed-message sweep** — a message posted before the session is live is not + lost once the lane is built: `go/internal/delivery/settle.go:47-53` + `OnSessionStarted(sessionID, account)` (the hub's `SessionStartSink`, + `go/internal/runnerhub/hub.go:95-98`, fired at `promoteSession` right after + `StartAgentSession` binds account→session) ENQUEUES the start edge onto the + consumer's queue (`settle.go:51-53`); the reconnect sweep runs later in the + consumer loop and redelivers every owed message to the freshly-live session. + +So the intended first-turn model is: **start the session idle, then post the +brief into a channel the new agent receives on; the idle-deliver arm starts its +first turn** — once the Runner deliver-lane (PR-A) carries the deliver and the +barrier lifts. Both orderings (post-then-start via the owed sweep, +start-then-post via live dispatch) converge on the same turn. + +**Which channel carries the case-2 "DM":** a **per-pair manager↔peer DM +channel** — a `ChannelKindDM` channel (`go/internal/store/types.go:74-75`: +`ChannelKindDM ChannelKind = 1`, "a direct conversation between two accounts") +whose two members are the spawning manager and the new peer, auto-provisioned on +the spawn edge (Matt's OQ-B ruling). This is a point-to-point carrier, NOT the +manager's coordination channel — that channel is a BROADCAST: its members are +the manager plus ALL of its direct reports (`go/internal/store/coordination.go` +`CoordinationReports` seeds `members := []AccountID{managerAgentID}` then appends +every `agent_accounts` row with `parent_agent_id = manager`, `coordination.go:314-334`) +under an OWNER_ONLY + mandatory_subscription policy (`CoordinationChannelSpec`, +`coordination.go:54-70`), so a brief posted there starts/queues a turn on EVERY +sibling report, not just the addressee. Matt's intent is token-minimization: +most messages should flow through these pairwise DM channels so siblings do not +spend tokens on briefs they don't need to see. + +The DM channel reuses primitives that already exist: +`store.CreateChannel` + `expandOwnerMembership` (`go/internal/store/channels.go:72-140`, +`expandOwnerMembership` at `:198`) create a 2-member channel and carry BOTH +members' owning users into membership ("an agent↔agent DM carries both owners", +`channels.go:76-79`), so the operator (the manager's and the peer's owning user) +retains read/respond visibility into every DM channel. What does NOT exist today +is any manager↔peer DM AUTO-PROVISION on the spawn edge — nothing get-or-creates +a pairwise DM when a peer is spawned — so this record ADDS that machinery (see +the Plan task T-R0 below), modeled on the existing coordination reconcile hook +(`CoordinationHook`, `coordination.go:10-23`, fired inside the CreateAgent tx) +but keyed on the `{manager, peer}` pair, 2-member, and idempotent on re-spawn. + +`SpawnAsAccount` creates the peer with `ParentAgentID: caller` +(`go/server/lifecycle.go:139-148`). Once the DM exists, the manager posts the +brief into it with its existing `comms_post_message` tool +(`packages/compass-agent/src/comms.ts:263-270`); the peer receives it via +ordinary DM membership — both parties are members, so no mandatory-subscription +broadcast disjunct is needed. For a **human-started** agent, the equivalent is +the owner posting into the agent's home channel — the owner and the agent are +both members from creation and the agent is always-subscribed +(`go/internal/store/accounts.go:124-131`, RT-2); this is unchanged (a home +channel is already a 2-party owner+agent channel). No new tool, RPC, or proto +SHAPE is needed for case 2 — but the Runner deliver-lane AND the DM auto-provision +(PR-A) ARE new machinery. The UI channel-proliferation UX (many DM channels +cluttering the operator's view) is a KNOWN later problem Matt has deferred +("work on how to manage the UX of this better later") — out of scope here. + +### Per-site removal (the blast radius, each site re-verified) + +| Site | Today (file:line, `ws-first-turn`) | Removal | +| --- | --- | --- | +| `StartAgentSessionRequest.initial_prompt = 2` | `proto/compass/v1/compass.proto:594` (`string initial_prompt = 2;`, doc `:592-593` "Optional initial prompt … Empty = start idle") | Delete field + doc; add `reserved 2; reserved "initial_prompt";` | +| `SpawnAgentRequest.initial_prompt = 2` | `compass.proto:618` (`string initial_prompt = 2;` on the DL-166 composite) | Delete; `reserved 2; reserved "initial_prompt";` | +| `SpawnPeerRequest.initial_prompt = 3` | `proto/compass/v1/agent_gateway.proto:170` (`string initial_prompt = 3; // threaded to StartAgentSessionRequest.initial_prompt`) | Delete; `reserved 3; reserved "initial_prompt";` | +| Generated code | `go/gen/compass/v1/compass.pb.go:2767,2885`, `go/internal/gen/compass/v1/agent_gateway.pb.go:595`, `packages/compass-agent/src/gen/compass/v1/{compass_pb.ts:1143,1206, agent_gateway_pb.ts:299}`, `packages/compass-client/src/gen/compass/v1/compass_pb.ts:1143,1206` | Regenerate via the repo's buf lanes (`buf.gen.yaml`, `buf.gen.internal-go.yaml`, `buf.gen.agent-ts.yaml`) — never hand-edit | +| Server: agent-spawn leg | `go/server/lifecycle.go:325` — `InitialPrompt: req.GetInitialPrompt(),` inside `hub.Start(ctx, "", &compassv1.StartAgentSessionRequest{...})` | Drop the field from the literal | +| Server: composite SpawnAgent | `go/server/spawn.go:135` — `InitialPrompt: msg.GetInitialPrompt(),` in `runSpawn` | Drop the field from the literal | +| Runner | `go/internal/runner/host.go:227-228` — `Start` reads only `req.GetContainerName()`, never the field (the standing bug) | Removing the field makes the drop correct-by-construction (no `host.go` change). NOT "already enrolled for delivery": the Runner deliver-lane is UNBUILT (dispatch has no `SessionsResponse_DeliverControl` arm, `dispatch.go:243/320-323`; `representable()` rejects Deliver/Steer, `gateway/control.go:192-203`) — built additively in PR-A (T-R1/T-R2), NOT in this removal PR | +| Agent SDK spawn tool | `packages/compass-agent/src/lifecycle.ts:90-92` — `"initial_prompt?": type("string").describe("Initial prompt to seed the new peer's first turn")`; `:159` `initialPrompt: params.initial_prompt ?? ""`; tool description "optionally a display name and an initial prompt…" (`:146-148`) | Remove the schema key, the request field, and the description clause; describe the replacement ("post the peer's brief to your DM channel with it after spawn") | +| UI spawn state | `apps/ui/src/spawn.ts:38` `SessionBinding.initialPrompt`, `:55` `SpawnSpec.initialPrompt`, `:82` capture in `beginSpawn`, `:168` `return b.initialPrompt ? "working" : "idle"` | Remove both fields; `bindingDotState` `running` arm returns `"idle"` unconditionally (every spawn is a start-idle spawn) | +| UI start dialog | `apps/ui/src/components/StartAgentDialog.tsx:10-24` — the dialog's only input is the prompt textarea (`spec: Omit`, submit re-attaches `initialPrompt`) | Delete `StartAgentDialog` (+ its test); the board's start affordance calls the spawn action directly with `{agentAccountId, workstreamId}`. The user prompts the agent by posting in its home channel — the exact model this record ratifies | +| Go tests | `go/server/lifecycle_pgtest_test.go:79,130`; `go/server/lifecycle_e2e_pgtest_test.go:120,337`; `go/server/service_spawn_pgtest_test.go:39`; `go/internal/runner/gateway/lifecycle_test.go:65` (all `InitialPrompt: "go"` fixtures) | Drop the field from the fixtures (no assertion depends on its value) | +| TS tests | `packages/compass-agent/src/lifecycle.test.ts:179,190,207`; `apps/ui/src/spawn.test.ts:24,38,60,65-67,177,183`; `apps/ui/src/components/StartAgentDialog.test.tsx` (whole file); `apps/ui/src/components/NewWorkstreamDialog.test.tsx:134` (asserts the spec carries NO `initialPrompt` — keep, now trivially true; reword comment) | Update/remove per the code changes | +| e2e harness | `go/e2e/agent_ops.go:50-56` `StartSession(ctx, containerName, initialPrompt)`; `:70-77` `Resume(ctx, container, resumeSessionID, initialPrompt)`; `go/e2e/legtwo_test.go:108` `StartSession(ctx, containerName, "say hello and stop")`; `go/e2e/legthreefour_test.go:58-61` spawn args JSON `"initial_prompt":%q` | See the harness re-model below — the prompt parameter is removed and the first turn moves to `PostMessage` | + +Proto discipline: a removed field number/name MUST be `reserved` so the wire +number is never reused — this is a hard Global Constraint below. Because the +regenerated types delete the `InitialPrompt`/`initialPrompt` members, every +consumer breaks at compile the moment the proto lands: **the removal is ONE +atomic PR (PR-B)** across the monorepo (proto + regen + server + runner tests + +SDK + UI + harness) — it cannot be staged internally, since a partial removal +leaves the tree non-compiling. + +**Two-PR staging (finding 4).** PR-B does NOT stand alone: the harness re-model +in it (leg-2's first turn moving to a channel `PostMessage`) only greens once a +posted message actually reaches the agent session — which today it does not (the +Runner deliver-lane is unbuilt, §"the seam", gaps a–c). So the Runner +deliver-lane build lands FIRST as its own PR: + +- **PR-A — Runner deliver-lane + spawn-edge DM auto-provision (additive, + independently landable, greens nothing yet).** T-R0 (per-pair manager↔peer DM + auto-provision on the spawn edge), T-R1 (`dispatch.go` + `SessionsResponse_DeliverControl` arm), T-R2 (`representable()` admits + Deliver/Steer), T-R3 (fresh-start barrier-lift, the OQ-A ruling). Purely + additive — no proto removal, no consumer break — so it is revertable on its + own and reviewable in isolation. +- **PR-B — atomic `initial_prompt` removal + harness re-model (greens leg-2).** + The single atomic PR above (T1–T7), landing AFTER PR-A so its re-modeled + leg-2 turn can actually settle. + +This deliberately does NOT fold a new Runner subsystem into the same atomic PR +as the proto removal: coupling a revertable feature (the deliver-lane) to an +unrevertable removal (the reserved field) would be past reviewable size and +would make the feature un-revertable independently of the removal. + +### The e2e harness re-model (the H8 unblock) + +`TestLegTwoRealTurn` (`go/e2e/legtwo_test.go:65-66`: "CreateAgent -> Provision +-> StartSession(initial_prompt) -> AwaitSessionSettled -> assert … transcript +is non-empty") drives its turn through the removed seam, and the frozen +dogfood-e2e record documents that seam as a primitive +(`docs/designs/platform/compass-dogfood-e2e/design.md:220`: +`StartSession(container, prompt, resumeID)` → +`StartAgentSession{initial_prompt}`). Re-model: + +- `StartSession(ctx, container)` — no prompt; `Resume(ctx, container, + resumeSessionID)` — no prompt. Both still return the minted session id. +- The first turn becomes a **post into the agent's home channel**: the harness + user is the agent's owner and therefore a home-channel member from creation + (`accounts.go:124-131`), and `CreateAgentResponse`'s account carries + `home_channel_id` (`proto/compass/v1/comms.proto:173`) — the fixture's + `CreateAgent` (`go/e2e/agent_ops.go:18`) grows a home-channel return. The + existing `PostMessage` primitive (`go/e2e/comms_ops.go:22`) posts the + scripted prompt; the deliver path carries it; the idle-deliver arm starts + the turn against the canned model (`go/e2e/fixture.go:80` + `WithCannedModel`). +- **Settle-wait re-spec — split to fix a subscribe-after-post race.** + `AwaitSessionSettled` today opens its `SubscribeAgentSession` stream WHEN + CALLED and returns on the FIRST `AGENT_SESSION_STATE_READY` frame + (`go/e2e/agent_ops.go:91-101`). The re-modeled order is + `StartSession` → `PostMessage` → settle-wait, but if the wait opens its + stream AFTER the post, a canned turn can start AND settle in the + post→subscribe window, fanning WORKING/READY to zero subscribers → the wait + hangs. `SubscribeAgentSession` is live-fan (no replay ring), so a missed edge + is gone. **Fix is ORDERING, not the state machine:** split into + - `OpenSessionTail(ctx, sessionID) (stream, error)` — opens the + `SubscribeAgentSession` stream BEFORE `PostMessage`, so it is already + subscribed when the turn fans. This mirrors the in-repo precedent + `legthreefour_test.go:176-180` ("Open one subscription before the post so it + sees the live fan of the deliver-side MessagePosted event"). + - `AwaitTurnSettled(ctx, stream) error` — reads the already-open stream, + skips frames until a WORKING state is observed, then returns on the next + READY (WORKING→READY = one settled turn). Same event-gated, no-poll, + ctx-bounded contract as today. + + Guard note: the "boot-idle READY" premise was an over-claim. On the STREAM the + agent emits STARTING at boot (`packages/compass-agent/src/agent.ts:147-149`) + and READY only at `agent_end` (`packages/compass-agent/src/mapping.ts:145-146`); + the READY at `go/internal/runner/host.go:346,480` is the Runner's Status-answer + state, NOT a stream frame. So the guard is stated as "ignore everything until + the first WORKING, then return on the next READY" — not a claim about a + boot-READY frame appearing on the stream. This also closes the dogfood-e2e + record's own flag that `AwaitSessionSettled` is "the ONE primitive with no + grounded wire contract today" (`compass-dogfood-e2e/design.md:223`) — the + contract is now WORKING→READY on `SubscribeAgentSession`. +- `legthreefour_test.go:59-61` drops `"initial_prompt"` from the canned spawn + tool-call JSON. This is HYGIENE, not a wire guard: ArkType KEEPS/ignores + undeclared keys by default (`spawnParameters` sets no `"+": "reject"`), so a + leftover `initial_prompt` key would be silently IGNORED, not rejected — the + turn would not fail on it. See the arktype note below. + +Post-then-await ordering: open the tail (`OpenSessionTail`) BEFORE `PostMessage`, +which is posted AFTER `StartSession` returns. On the freshly-promoted session the +start edge is ENQUEUED before `StartAgentSession` responds — the +`SessionStartSink` (`go/internal/runnerhub/hub.go:95-98`) only appends the start +edge (`go/internal/delivery/settle.go:51-53`); the reconnect sweep runs later in +the consumer loop — so the post lands as either a live dispatch or an async owed +sweep, both reaching the agent (convergence via live dispatch OR async sweep). +Once the Runner deliver-lane (PR-A) carries the deliver and the barrier is +lifted (OQ-A), the agent's replay barrier no longer strands it: a +pre-`replayComplete` deliver is otherwise refused-and-acked-away at +`packages/compass-agent/src/transport/control-source.ts:373-390`, so the +barrier-lift is a precondition, not a "held until ReplayComplete" guarantee. + +### Case 1 — root-manager boot: scoped OUT to a follow-up record + +Recommendation: **(b) scope case 1 to its own follow-up record**, with this +record freezing only its shape: the root manager's first turn is a +Compass-authored **initial Setup thread in its home channel**, which requires +a **reserved `@compass` system-sender alias**. Rationale for the split: + +- The `@compass` sender breaks a store invariant this record otherwise never + touches: every `messages.author_account_id` today resolves to a real + `accounts` row, and `CreateAgent`/handle validation has **no reserved-name + guard** (`accounts.go:131-137` checks only non-empty — nothing stops a user + registering the handle `compass` today). A system sender needs its own + design: reserved-handle enforcement at account creation, whether `@compass` + is a real reserved account row or a sentinel author, how the UI renders it, + and how delivery treats its posts (it must never be a deliver *recipient*). +- The Setup flow itself (what the thread says, the repos/projects + interrogation, tree/devenv bring-up) is product-design work with no + dependency on — and no blocker for — the `initial_prompt` removal: nothing + boots a root manager through `initial_prompt` today, so removing the field + strands no existing case-1 path. +- Case 2 + the harness re-model unblock SEA-1792 H8 now; coupling them to the + system-sender design would serialize an unrelated, larger decision in front + of a red CI gate. + +The follow-up record owes: the reserved-alias representation, reserved-handle +validation, Setup-thread creation trigger (root-manager first +`StartAgentSession`), thread content/versioning, and its ledger rows. This +record's DL-187 row (below) freezes the shape so the interim cannot regress +into a prompt-field revival. Matt has ruled (OQ-C): the `@compass` reserved +alias is the frozen system-sender mechanism, used for ANY system-level message +sender (not just the root-manager Setup thread) — "the @compass alias will be +used for any system-level messages, so we can freeze it now." The follow-up +record inherits the mechanism and details only its representation + the Setup +flow. + +### Alternatives considered + +- **Keep `initial_prompt` as a deprecated no-op field** — rejected by Matt + verbatim ("drop it from everything"); also perpetuates the silent-drop bug + as API surface. +- **Case-2 brief via the manager's coordination channel** — rejected (OQ-B, + Matt): the coordination channel is a BROADCAST to the manager plus ALL direct + reports (`coordination.go:314-334` / `CoordinationChannelSpec`), so every + sibling would receive a brief meant for one peer, spending tokens on messages + they don't need. The chosen carrier is a per-pair `ChannelKindDM` channel + (§"Which channel carries the case-2 'DM'"). +- **Bake an @-mention-the-addressee convention onto the coordination broadcast** + — rejected (OQ-B): it leaves every sibling receiving the deliver and only + upgrades the named peer to a steer, still burning sibling tokens; the per-pair + DM avoids the broadcast entirely. +- **A dedicated `PromptControl` first-turn op** — redundant: `DeliverControl` + with the idle-deliver arm IS the first-turn primitive, already ratified + (DL-071/073). Its agent-side arm is wired (`agent.ts:183`); its Runner middle + leg is not (PR-A builds it, §"the seam") — a new `PromptControl` would need + the SAME Runner deliver-lane, so it adds a redundant op for no saved work. + +## Global Constraints + +1. **Reserved on removal.** Every removed proto field reserves BOTH its number + and its name: `StartAgentSessionRequest` → `reserved 2; reserved + "initial_prompt";`; `SpawnAgentRequest` → `reserved 2; reserved + "initial_prompt";`; `SpawnPeerRequest` → `reserved 3; reserved + "initial_prompt";`. Wire numbers are never reused. +2. **No fallback, no shim.** No deprecated field, no server-side tolerance + read, no "empty means idle" remnant text. A session starts idle, + unconditionally; the only turn-start inputs are the existing control lane + ops (deliver/steer/prompt-from-resume-replay). +3. **Gen code is generated.** All four gen trees (`go/gen`, `go/internal/gen`, + `packages/compass-agent/src/gen`, `packages/compass-client/src/gen`) + regenerate via the repo buf lanes; hand-edits are review failures. +4. **Two-PR staging (PR-A then PR-B).** PR-B (the removal) cannot be staged + internally (the regenerated types break every consumer at compile); its + tasks T1–T7 land as slices of that one atomic PR. But it lands AFTER PR-A, + the additive Runner deliver-lane + DM auto-provision (T-R0/T-R1/T-R2/T-R3), because PR-B's re-modeled + leg-2 turn only greens once a posted message reaches the session (§"the + seam"; §"Two-PR staging"). +5. **Frozen records stay frozen.** `compass-agent-spawn-despawn`, + `compass-spawn-control`, and `compass-dogfood-e2e` contain now-superseded + `initial_prompt` content; they are NOT edited. Supersession is recorded in + the ledger (DL-186) and in this record. +6. **Event-gated tests only.** The re-modeled harness wait stays no-sleep, + no-poll (the same bar as today's `AwaitSessionSettled`, + `agent_ops.go:91-101`), now via `OpenSessionTail` + `AwaitTurnSettled`. + +## Plan + +### PR-A — Runner deliver-lane + spawn-edge DM auto-provision (additive; lands first) + +These tasks build the Runner middle leg §"the seam" shows is unbuilt, plus the +per-pair manager↔peer DM auto-provision the case-2 brief posts into. +Additive only (no proto removal), independently landable, greens nothing yet. + +#### T-R1 — Runner dispatch: `DeliverControl` arm (lane: implement-hard, Go runner) + +Add a `case *compassv1internal.SessionsResponse_DeliverControl:` arm to the +`dispatcher.execute` switch (`go/internal/runner/dispatch.go:243`), mirroring +the existing arms' shape. It unwraps `c.DeliverControl.GetOp()` (the +`AgentControl`) and `c.DeliverControl.GetSessionId()` and routes the op to that +session's control producer, so a server-pushed deliver is no longer met by the +`:320-323` default's `"unrecognized session command variant"`. The send side is +already live (`go/internal/runnerhub/dispatch_control.go:44-53`; +`go/internal/gen/compass/v1/runner.pb.go:599,634`). + +#### T-R2 — gateway control: admit payload-carrying Deliver/Steer (lane: implement, Go runner) + +`go/internal/runner/gateway/control.go:192-203` `representable()` currently +returns `false` for `AgentControl_Deliver` and `AgentControl_Steer`. Both now +carry a comms `Message` (SEA-1569 populated the parked shells), so they ARE +representable — remove them from the reject set (leaving Replay/Config, still +empty shells). Without this the send from T-R1 is dropped before it reaches the +socket. + +#### T-R0 — spawn-edge manager↔peer DM auto-provision (lane: implement-hard, Go store + comms) + +On the spawn edge, get-or-create a per-pair manager↔peer DM channel so the +manager can post the peer's brief point-to-point (OQ-B ruling; §"Which channel +carries the case-2 'DM'"). Model it on the existing coordination reconcile hook: +`CoordinationHook` (`go/internal/store/coordination.go:10-23`) is a comms-layer +callback the store invokes INSIDE the `CreateAgent`/`ReparentAgent` tx right +after writing `parent_agent_id`, so the channel reconcile commits atomically +with the parent-edge write without the store importing comms types. This task +adds an analogous pairwise reconcile that get-or-creates a `ChannelKindDM` +channel (`go/internal/store/types.go:74-75`) for the `{manager, peer}` pair via +`CreateChannel` + `expandOwnerMembership` (`go/internal/store/channels.go:72-140`) +— a 2-member channel that carries both members' owning users, so the operator +keeps visibility (`channels.go:76-79`). It must be idempotent/get-or-create, +keyed on the pair, so a re-spawn resolves the SAME DM rather than minting a +duplicate. Reuses existing primitives only (`ChannelKindDM`, `CreateChannel`, +`expandOwnerMembership`, the hook precedent); the impl lane details the keying +and the get-or-create SQL. Lands in PR-A (additive, no proto removal) so PR-B's +harness re-model can post the first turn into this DM. + +#### T-R3 — fresh-start barrier-lift (lane: implement-hard, Go runner + TS agent) + +A FRESH (non-resume) session's agent barrier defaults CLOSED +(`packages/compass-agent/src/transport/control-source.ts:278` +`let replayComplete = false`) and refuses+acks-away a pre-`replayComplete` +deliver (`:373-390`), and nothing sends `replay_complete` on a fresh start +(`go/internal/runner/gateway/control.go:421-429`: `HoldForReplay` has no +production caller). Per Matt's OQ-A ruling, on a FRESH start the Runner sends +`AgentControl{replay_complete}` as the first control op after Bind (seq 1, +FIFO-first on the control stream, so it always drains before any deliver) — ONE +mechanism symmetric with the future resume path, needing no agent change. The +PR-A deliver-lane (T-R1/T-R2) is landable ahead of it, but case-2 delivery to a +fresh session is not green until T-R3 lands. + +### PR-B — atomic removal + harness re-model (lands after PR-A) + +### T1 — Proto removal + reservation + regeneration (lane: implement, Go/proto) + +Remove the three fields, reserve numbers+names, regenerate all gen trees. + +Interfaces: + +- `proto/compass/v1/compass.proto` — `message StartAgentSessionRequest { + string container_name = 1; reserved 2; reserved "initial_prompt"; string + resume_session_id = 3; }`; `message SpawnAgentRequest { string + agent_account_id = 1; reserved 2; reserved "initial_prompt"; string + client_request_id = 3; }`. +- `proto/compass/v1/agent_gateway.proto` — `message SpawnPeerRequest { string + handle = 1; string display_name = 2; reserved 3; reserved "initial_prompt"; + string client_request_id = 4; }`. +- Regenerate: `go/gen/compass/v1/compass.pb.go`, + `go/internal/gen/compass/v1/agent_gateway.pb.go`, + `packages/compass-agent/src/gen/compass/v1/{compass_pb.ts, + agent_gateway_pb.ts}`, `packages/compass-client/src/gen/compass/v1/ + compass_pb.ts` — `GetInitialPrompt()`/`initialPrompt` members disappear. + +### T2 — Server: drop the two forwarding sites (lane: implement, Go server) + +Interfaces: + +- `go/server/lifecycle.go:323-326` — `l.hub.Start(ctx, "", + &compassv1.StartAgentSessionRequest{ContainerName: container})` (the + `InitialPrompt:` line at `:325` deleted). +- `go/server/spawn.go:133-136` — `s.StartAgentSession(ctx, + connect.NewRequest(&compassv1.StartAgentSessionRequest{ContainerName: + container}))` (the `InitialPrompt:` line at `:135` deleted). +- Test fixtures drop `InitialPrompt: "go"`: + `go/server/lifecycle_pgtest_test.go:79,130`, + `go/server/lifecycle_e2e_pgtest_test.go:120,337`, + `go/server/service_spawn_pgtest_test.go:39`. + +### T3 — Runner: idle-start assert + gateway test (lane: implement, Go runner) + +The `initial_prompt` removal itself is behavior-preserving in the runner +(`host.go:227-228` never read the field). The runner's PRODUCTION change for +first-turn delivery is NOT in this task — it is the additive deliver-lane build +in PR-A (T-R1 `dispatch.go` `SessionsResponse_DeliverControl` arm, T-R2 +`representable()` fix, T-R3 barrier-lift). This T3 slice is only the +compile-against-regenerated-types proof plus gateway fixture cleanup. + +Interfaces: + +- `go/internal/runner/host.go:227` — signature unchanged: `Start(ctx + context.Context, req *compassv1.StartAgentSessionRequest, resumeBody + string) (string, error)`; compiles against the regenerated type. +- `go/internal/runner/gateway/lifecycle_test.go:65` — drop `InitialPrompt: + "go"` from the `SpawnPeerRequest` fixture. + +### T4 — Agent SDK: spawn tool sheds the parameter (lane: implement, TS agent) + +Interfaces: + +- `packages/compass-agent/src/lifecycle.ts` — `spawnParameters = type({ + handle: …, "display_name?": … })` (the `"initial_prompt?"` key at `:90-92` + removed); the `SpawnPeerRequestSchema` create at `:156-166` loses + `initialPrompt:` (`:159`); tool `description` (`:146-148`) rewritten to: + "Spawn a new peer agent owned by your owner. Provide a unique handle and + optionally a display name. The peer starts idle — post its brief to your DM + channel with the new peer to start its first turn." +- `packages/compass-agent/src/lifecycle.test.ts:178-207` — remove + `initial_prompt` from the params fixture and the `spawn.initialPrompt` + asserts. Do NOT add an unknown-key reject assert: ArkType KEEPS/ignores + undeclared keys by default (rejecting needs a `"+": "reject"` config + `spawnParameters` does not set), so a stray `initial_prompt` key would be + silently IGNORED, not rejected — the negative assert would be false. Dropping + the key from the leg-3/4 canned JSON is pure hygiene (the ignored key is + harmless), not a wire guard. See the arktype note under the harness re-model. + +### T5 — UI: promptless start (lane: implement, UI) + +Interfaces: + +- `apps/ui/src/spawn.ts` — `SessionBinding` loses `initialPrompt` (`:35-38`); + `SpawnSpec` becomes `{ readonly agentAccountId: string; readonly + workstreamId: string }` (`:55` deleted); `beginSpawn(spec, requestId)` + no longer captures a prompt (`:82`); `bindingDotState` `running` arm + (`:168`) returns `"idle"` unconditionally (live `AgentSessionState` takes + over from the first attributed status, per the existing DL-167 reconcile). +- `apps/ui/src/components/StartAgentDialog.tsx` + `.test.tsx` — its only input + (the prompt textarea) is gone. Its disposition is OQ-1 (a genuine fork: + DL-185 explicitly KEEPS this dialog), Matt-ruled at this design-PR gate: + either deleted (the board start affordance invokes the spawn action directly + with `SpawnSpec`) or reduced to a bare confirm dialog. T5 implements whichever + Matt rules. +- `apps/ui/src/spawn.test.ts:24,38,60,65-67,177,183` — drop prompt fixtures; + the two `bindingDotState` running-arm cases collapse to one (`running` → + `"idle"`). +- `apps/ui/src/components/NewWorkstreamDialog.test.tsx:134` — keep the + no-lifecycle-fields assert; update its comment (there is no prompt field + anywhere anymore). + +### T6 — e2e harness re-model onto the channel first-turn seam (lane: implement-hard, Go e2e) — unblocks SEA-1792 H8 / PR #256 + +Interfaces: + +- `go/e2e/agent_ops.go` — `func (f *Fixture) StartSession(ctx context.Context, + containerName string) (sessionID string, err error)`; `func (f *Fixture) + Resume(ctx context.Context, containerName, resumeSessionID string) + (sessionID string, err error)`; `func (f *Fixture) CreateAgent(ctx + context.Context, handle, displayName string) (accountID, homeChannelID + string, err error)` (home channel read from + `CreateAgentResponse.account.agent.home_channel_id`, `comms.proto:173`). +- `func (f *Fixture) OpenSessionTail(ctx context.Context, sessionID string) + (stream, error)` + `func (f *Fixture) AwaitTurnSettled(ctx context.Context, + stream) error` — together REPLACE `AwaitSessionSettled` and fix the + subscribe-after-post race. `OpenSessionTail` opens the + `SubscribeAgentSession` stream (called BEFORE `PostMessage`, mirroring + `legthreefour_test.go:176-180`); `AwaitTurnSettled` reads that already-open + stream, skips frames until a WORKING state is seen, then returns on the next + READY (WORKING→READY = the settled turn); same derived deadline contract as + today (`agent_ops.go:92-93`). +- `go/e2e/legtwo_test.go` (`TestLegTwoRealTurn`) — new scenario order: + `CreateAgent` → `Provision` → `StartSession(container)` → + `OpenSessionTail(sessionID)` → `PostMessage(homeChannelID, "leg2", "say hello + and stop")` (`comms_ops.go:22`) → `AwaitTurnSettled(stream)` → transcript + asserts unchanged (canned reply present). The tail opens BEFORE the post so a + fast canned turn cannot fan WORKING/READY into the post→subscribe gap. + `TestLegTwoPrimitives` (`legtwo_test.go:56`) drops its prompt argument. +- `go/e2e/legthreefour_test.go:59-61` — canned spawn args JSON becomes + `{"handle":%q,"display_name":%q}`. + +### T7 — Ledger delta + case-1 follow-up reference (lane: driver) + +The driver lands the ledger rows below in the same PR +(`design-ledger-gate`), sets this record's `Status:` header on merge. The +case-1 follow-up is already tracked as **SEA-1820** ("Auto-seed root Manager +'supervisor' on embedded first-launch (dogfood)", parent SEA-1681) — updated +with the thread+sender specifics (Compass-authored Setup thread in the root +manager's home channel; `@compass` reserved-alias system sender; no +`initial_prompt`). This record references SEA-1820 as the case-1 follow-up; it +files no new issue. + +Exact `docs/designs/product/DECISIONS.md` delta (append under a new +`## First-turn delivery` section; DL-185 already landed on `main` (the SEA-1932 +add-surface drop), so the highest row is now DL-185 and these are DL-186..189): + +```markdown +## First-turn delivery + +| ID | Decision | Status | Record | +| --- | --- | --- | --- | +| DL-186 | `initial_prompt` is REMOVED from the whole contract (`StartAgentSessionRequest` field 2, `SpawnAgentRequest` field 2, `SpawnPeerRequest` field 3 — numbers AND names reserved; server/runner/SDK/UI/e2e consumers cut over atomically, no fallback): an agent session ALWAYS starts idle and its first turn arrives as a channel message over the SEA-1569 deliver path (`DeliverControl` → the idle-deliver arm starts a turn); a provisioned peer's brief is a post from its provisioning manager into their per-pair DM channel (home channel for human owners) | Active (Matt, 2026-08-10) | [first-turn delivery §Approach](compass-first-turn-delivery/design.md#approach) | +| DL-187 | The `@compass` reserved alias is FROZEN as the system-sender mechanism for ANY system-level message sender (not just the root-manager Setup thread), requiring reserved-handle validation at account creation; case-1 root-manager boot (a Compass-authored initial Setup thread in the manager's home channel) uses it and is scoped OUT to follow-up SEA-1820, which owes only the sender representation + Setup flow; ratified in shape here so the interim can never revive a prompt field | Active (Matt, 2026-08-10) | [first-turn delivery §Case 1](compass-first-turn-delivery/design.md#case-1--root-manager-boot-scoped-out-to-a-follow-up-record) | +| DL-188 | Fresh-start barrier-lift: on a FRESH (non-resume) start the Runner sends `AgentControl{replay_complete}` as the first control op after Bind (seq 1, FIFO-first, drains before any deliver) — one mechanism symmetric with the resume path, no agent change; lifts the agent-side replay barrier so the first case-2 deliver is not refused-and-stranded (T-R3) | Active (Matt, 2026-08-10) | [first-turn delivery §the seam](compass-first-turn-delivery/design.md#the-seam-the-first-turn-rides-case-2--the-server--agent-halves-exist-the-runner-middle-leg-is-unbuilt) | +| DL-189 | The case-2 brief carrier is a PER-PAIR manager↔peer DM channel (`ChannelKindDM`, 2 members: spawning manager + new peer, both owners carried by `expandOwnerMembership` so the operator retains visibility), auto-provisioned on the spawn edge (T-R0) — NOT the manager's coordination channel, which is a broadcast to all reports. Token-minimization: siblings do not receive briefs they don't need. UI channel-proliferation UX is a known deferred problem | Active (Matt, 2026-08-10) | [first-turn delivery §Approach](compass-first-turn-delivery/design.md#approach) | +``` + +Note for the driver: DL-186's "first turn arrives over the SEA-1569 deliver +path" is a DECISION, but the path's Runner middle leg (and the T-R0 DM +auto-provision) is unbuilt today — PR-A (T-R0/T-R1/T-R2/T-R3) builds it. Do NOT +land DL-186 as "verified end-to-end": the Runner leg is PR-A, not yet exercised. + +**Status flips: one, conditional on OQ-1.** No existing DL row rules on +`initial_prompt`, start-idle semantics, or first-turn carriage (verified: +`DECISIONS.md` has no occurrence of "initial" or "prompt" in any Decision cell +bearing on the prompt field; DL-166/DL-164 rule the composite-spawn and +start-affordance shapes without it). **But DL-185 (SEA-1932, Active) explicitly +KEEPS `StartAgentDialog`** as the human's only start affordance — so T5's +disposition of that dialog (OQ-1) amends DL-185's "Kept" clause IF Matt rules +delete. That amendment is NOT applied here: it is Matt's call at this +design-PR gate (OQ-1). If Matt keeps a bare confirm dialog, no DL row flips. +The superseded content lives in frozen RECORDS, not +ledger rows. All six records carrying now-superseded `initial_prompt` content +are superseded-in-part by DL-186 (the citable overturn) — three already noted +plus three the completeness pass adds, so a reader of ANY of the six finds the +supersession: + +- `compass-agent-spawn-despawn/design.md:836-841` ("spawn ships + `initial_prompt` only…"); +- `compass-spawn-control/design.md:538-541,692-696` (prompt-conditional dot + + prompt textarea); +- `compass-dogfood-e2e/design.md:220` (`StartSession(container, prompt, + resumeID)`); +- `compass-dogfood-loop/design.md:266,472-473,490-491` (the + `StartAgentSession{initial_prompt}` scenario step); +- `compass-agent-session-persistence/design.md:676-677` (quotes `string + initial_prompt = 2`); +- `compass-0.8-threading-and-session-renderer/design.md:594-595,777-778` (the + request modeled as `container_name` + `initial_prompt`). + +Per Constraint 5 those records are NOT edited here — only named, so DL-186 is +the single overturn point. + +## Tasks + +PRs are staged in two (see §"PR staging" below): **PR-A** = the additive Runner +deliver-lane + spawn-edge DM auto-provision build (T-R0/T-R1/T-R2/T-R3), lands +FIRST, greens nothing yet; **PR-B** = the atomic `initial_prompt` removal + +harness re-model (T1–T7), lands SECOND, greens leg-2. + +PR-A (Runner deliver-lane + DM auto-provision — additive, independently landable): + +- [ ] T-R0 — spawn-edge manager↔peer DM auto-provision: get-or-create a per-pair `ChannelKindDM` channel (`store/types.go:74-75`) for the `{manager, peer}` pair via `CreateChannel` + `expandOwnerMembership` (`store/channels.go:72-140`), modeled on the `CoordinationHook` reconcile (`store/coordination.go:10-23`, fired in the CreateAgent tx) but pairwise + idempotent, so the manager can post the brief point-to-point (implement-hard, Go store + comms) +- [ ] T-R1 — runner dispatch: add a `SessionsResponse_DeliverControl` arm in `dispatch.go` (`:243` switch) routing the wrapped op to the container's control producer, so a server-pushed deliver is no longer met by the `:323` "unrecognized session command variant" default (implement-hard, Go runner) +- [ ] T-R2 — gateway control: fix `representable()` (`gateway/control.go:192-203`) to admit payload-carrying `Deliver`/`Steer` (they carry a comms `Message`), so the send is no longer dropped as an empty shell (implement, Go runner) +- [ ] T-R3 — fresh-start barrier-lift: on a fresh (non-resume) start the Runner sends `AgentControl{replay_complete}` as the first control op after Bind (OQ-A ruling), so the first deliver is not refused-and-stranded by the agent barrier (`control-source.ts:278,373-390`) (implement-hard, Go runner + TS agent) + +PR-B (atomic removal + harness re-model — greens leg-2, lands after PR-A): + +- [ ] T1 — proto removal + `reserved` + regen of all four gen trees (implement) +- [ ] T2 — server: drop `InitialPrompt` at `lifecycle.go:325` and `spawn.go:135` + pgtest fixtures (implement) +- [ ] T3 — runner: compile against regenerated types; gateway fixture cleanup (no production change beyond PR-A's deliver-lane, which T-R1/T-R2 already landed) (implement) +- [ ] T4 — agent SDK: spawn tool loses `initial_prompt?`; tests updated (drop the JSON key as hygiene; NO unknown-key reject assert — see arktype note) (implement) +- [ ] T5 — UI: promptless `SpawnSpec`/binding, `running`→`idle` dot, dispose of `StartAgentDialog` per OQ-1 (delete, or reduce to a bare confirm dialog — Matt's design-gate ruling) (implement) +- [ ] T6 — e2e harness re-model: promptless Start/Resume, home-channel `PostMessage` first turn, split into `OpenSessionTail`(before post) + `AwaitTurnSettled`(WORKING→READY), leg-2/leg-3-4 scenario updates (implement-hard; unblocks SEA-1792 H8 / PR #256) +- [ ] T7 — ledger rows DL-186/DL-187/DL-188/DL-189 + `Status:` header; references case-1 follow-up SEA-1820 (no new issue filed) (driver) + +## Open Questions + +The four design-fork OQs (case-1 scope, deliver-lane carrier, barrier-lift +mechanism, system-sender freeze) are Matt-ruled and folded above +(DL-186..189 + SEA-1820). Two items remain for Matt at this design-PR gate: + +1. **[FORK — intersects an Active decision] `StartAgentDialog` disposition.** + Removing `initial_prompt` empties this dialog (its only input was the prompt + textarea). But **DL-185 (SEA-1932, Active, Matt 2026-08-07) explicitly KEEPS + `StartAgentDialog`** as the human's sole start affordance after the + add-a-workstream surface was dropped — so this is NOT a driver call. + **Recommendation: delete it** and start directly from the board affordance + with `{agentAccountId, workstreamId}` (spawn is already guarded by the + DL-164/168 live-session predicate and idempotent under its request id) — an + empty dialog is pure friction. **Alternative: keep a bare confirm dialog** + ("Start this agent?"). Whichever Matt rules, T5 implements it; a delete + amends DL-185's "Kept" clause (the one conditional status-flip noted above). +2. **[Minor, driver-resolved] Settle-primitive naming → `AwaitTurnSettled`.** + The new name (WORKING→READY, one settled turn) is the accurate one T6 uses; + the old `AwaitSessionSettled` named a session-ready wait the re-model no + longer performs. Pure rename; flip is free if the dogfood-e2e lane prefers + the old name with the new contract. From a355df84e7a13b6e8de058b071d50ab582feefe6 Mon Sep 17 00:00:00 2001 From: seal Date: Mon, 10 Aug 2026 15:52:23 -0400 Subject: [PATCH 2/3] docs(product): re-pin first-turn record citations to main@origin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1 (sole review-of-record) found 0 high / 2 medium / 3 low, all citation line-number accuracy — no design-judgment findings. The record's file:line were pinned to a superseded base (f8ab68c5); the record's actual parent and the ws-first-turn worktree it names as its citation source is main@origin, where SEA-1575 (2ebdf09) restructured dispatch.go and host.go. Re-pinned every drifted citation, ground-verified against main@origin: - dispatch.go execute switch :243 -> :359; default error :320-323 -> :446-449 - host.go Start/container read :264-265 and the wrong :227-228/:227 -> :284-285 - host.go READY status-answer :346,480 -> :405,785 - consumer.go deliverOp :277-281 -> :288-291 (plain miscite, not drift) - runner.pb.go DeliverControl :599,634 -> :618,655 (was pointing at the wrong variant) - mapping.ts agent_end READY :145-146 -> :115-116 Semantics were correct throughout; this is a locational fix only. The Per-site table header already labels citations as ws-first-turn (= main@origin), so the numbers now match the stated base. Additive commit atop the pushed head so the PR shows the interdiff. Refs SEA-1681, SEA-1820 Co-authored-by: Matt Wilkinson --- .../compass-first-turn-delivery/design.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/designs/product/compass-first-turn-delivery/design.md b/docs/designs/product/compass-first-turn-delivery/design.md index 953cc138..bb85dcfe 100644 --- a/docs/designs/product/compass-first-turn-delivery/design.md +++ b/docs/designs/product/compass-first-turn-delivery/design.md @@ -7,7 +7,7 @@ Status: Draft Compass carries an `initial_prompt` field through its whole start contract (proto → server → runner → agent SDK → UI → e2e harness), but the Runner never sends it into the container — `(*agentHost).Start` reads only the container -name (`go/internal/runner/host.go:264-265`: `func (h *agentHost) Start(ctx +name (`go/internal/runner/host.go:284-285`: `func (h *agentHost) Start(ctx context.Context, req *compassv1.StartAgentSessionRequest, resumeBody string)` … `name := req.GetContainerName()`; no `GetInitialPrompt` call exists in the file), so every prompt riding the field is silently dropped and the agent @@ -50,14 +50,14 @@ plus a **fresh-start barrier-lift** (gap c) — see PR-A (T-R3, the OQ-A ruling folded below). The three gaps: - **Gap (a) — runner dispatch has no `DeliverControl` arm.** - `go/internal/runner/dispatch.go:243` switches on + `go/internal/runner/dispatch.go:359` switches on Start/Provision/Stop/Remove/Reload/Status/SecretsVersion/ConfigVersion only; a server-pushed `SessionsResponse_DeliverControl` falls to the default and returns `errorResult(id, errors.New("unrecognized session command variant"))` - (`dispatch.go:320-323`). The server DOES send this variant — + (`dispatch.go:446-449`). The server DOES send this variant — `go/internal/runnerhub/dispatch_control.go:44-53` wraps the op in `SessionsResponse_DeliverControl` and `router.send1`s it — and the wire - variant exists (`go/internal/gen/compass/v1/runner.pb.go:599,634`). The send + variant exists (`go/internal/gen/compass/v1/runner.pb.go:618,655`). The send path is live; the receive arm is missing. **T-R1 builds it.** - **Gap (b) — gateway control lane rejects deliver/steer.** `go/internal/runner/gateway/control.go:192-203` `representable()` returns @@ -83,7 +83,7 @@ folded below). The three gaps: The pieces that DO exist, and that PR-A/PR-B compose with: 1. **Fan-out** — the delivery consumer wraps a posted message in the deliver - op: `go/internal/delivery/consumer.go:277-281` — + op: `go/internal/delivery/consumer.go:288-291` — `func deliverOp(msg *compassv1.Message) *compassv1internal.AgentControl` builds `AgentControl_Deliver{Deliver: &DeliverControl{Message: msg}}`. 2. **Dispatch** — `consumer.go:42-43`: `ControlDispatcher` is @@ -165,7 +165,7 @@ cluttering the operator's view) is a KNOWN later problem Matt has deferred | Generated code | `go/gen/compass/v1/compass.pb.go:2767,2885`, `go/internal/gen/compass/v1/agent_gateway.pb.go:595`, `packages/compass-agent/src/gen/compass/v1/{compass_pb.ts:1143,1206, agent_gateway_pb.ts:299}`, `packages/compass-client/src/gen/compass/v1/compass_pb.ts:1143,1206` | Regenerate via the repo's buf lanes (`buf.gen.yaml`, `buf.gen.internal-go.yaml`, `buf.gen.agent-ts.yaml`) — never hand-edit | | Server: agent-spawn leg | `go/server/lifecycle.go:325` — `InitialPrompt: req.GetInitialPrompt(),` inside `hub.Start(ctx, "", &compassv1.StartAgentSessionRequest{...})` | Drop the field from the literal | | Server: composite SpawnAgent | `go/server/spawn.go:135` — `InitialPrompt: msg.GetInitialPrompt(),` in `runSpawn` | Drop the field from the literal | -| Runner | `go/internal/runner/host.go:227-228` — `Start` reads only `req.GetContainerName()`, never the field (the standing bug) | Removing the field makes the drop correct-by-construction (no `host.go` change). NOT "already enrolled for delivery": the Runner deliver-lane is UNBUILT (dispatch has no `SessionsResponse_DeliverControl` arm, `dispatch.go:243/320-323`; `representable()` rejects Deliver/Steer, `gateway/control.go:192-203`) — built additively in PR-A (T-R1/T-R2), NOT in this removal PR | +| Runner | `go/internal/runner/host.go:284-285` — `Start` reads only `req.GetContainerName()`, never the field (the standing bug) | Removing the field makes the drop correct-by-construction (no `host.go` change). NOT "already enrolled for delivery": the Runner deliver-lane is UNBUILT (dispatch has no `SessionsResponse_DeliverControl` arm, `dispatch.go:359/446-449`; `representable()` rejects Deliver/Steer, `gateway/control.go:192-203`) — built additively in PR-A (T-R1/T-R2), NOT in this removal PR | | Agent SDK spawn tool | `packages/compass-agent/src/lifecycle.ts:90-92` — `"initial_prompt?": type("string").describe("Initial prompt to seed the new peer's first turn")`; `:159` `initialPrompt: params.initial_prompt ?? ""`; tool description "optionally a display name and an initial prompt…" (`:146-148`) | Remove the schema key, the request field, and the description clause; describe the replacement ("post the peer's brief to your DM channel with it after spawn") | | UI spawn state | `apps/ui/src/spawn.ts:38` `SessionBinding.initialPrompt`, `:55` `SpawnSpec.initialPrompt`, `:82` capture in `beginSpawn`, `:168` `return b.initialPrompt ? "working" : "idle"` | Remove both fields; `bindingDotState` `running` arm returns `"idle"` unconditionally (every spawn is a start-idle spawn) | | UI start dialog | `apps/ui/src/components/StartAgentDialog.tsx:10-24` — the dialog's only input is the prompt textarea (`spec: Omit`, submit re-attaches `initialPrompt`) | Delete `StartAgentDialog` (+ its test); the board's start affordance calls the spawn action directly with `{agentAccountId, workstreamId}`. The user prompts the agent by posting in its home channel — the exact model this record ratifies | @@ -245,8 +245,8 @@ dogfood-e2e record documents that seam as a primitive Guard note: the "boot-idle READY" premise was an over-claim. On the STREAM the agent emits STARTING at boot (`packages/compass-agent/src/agent.ts:147-149`) - and READY only at `agent_end` (`packages/compass-agent/src/mapping.ts:145-146`); - the READY at `go/internal/runner/host.go:346,480` is the Runner's Status-answer + and READY only at `agent_end` (`packages/compass-agent/src/mapping.ts:115-116`); + the READY at `go/internal/runner/host.go:405,785` is the Runner's Status-answer state, NOT a stream frame. So the guard is stated as "ignore everything until the first WORKING, then return on the next READY" — not a claim about a boot-READY frame appearing on the stream. This also closes the dogfood-e2e @@ -367,13 +367,13 @@ Additive only (no proto removal), independently landable, greens nothing yet. #### T-R1 — Runner dispatch: `DeliverControl` arm (lane: implement-hard, Go runner) Add a `case *compassv1internal.SessionsResponse_DeliverControl:` arm to the -`dispatcher.execute` switch (`go/internal/runner/dispatch.go:243`), mirroring +`dispatcher.execute` switch (`go/internal/runner/dispatch.go:359`), mirroring the existing arms' shape. It unwraps `c.DeliverControl.GetOp()` (the `AgentControl`) and `c.DeliverControl.GetSessionId()` and routes the op to that session's control producer, so a server-pushed deliver is no longer met by the -`:320-323` default's `"unrecognized session command variant"`. The send side is +`:446-449` default's `"unrecognized session command variant"`. The send side is already live (`go/internal/runnerhub/dispatch_control.go:44-53`; -`go/internal/gen/compass/v1/runner.pb.go:599,634`). +`go/internal/gen/compass/v1/runner.pb.go:618,655`). #### T-R2 — gateway control: admit payload-carrying Deliver/Steer (lane: implement, Go runner) @@ -458,7 +458,7 @@ Interfaces: ### T3 — Runner: idle-start assert + gateway test (lane: implement, Go runner) The `initial_prompt` removal itself is behavior-preserving in the runner -(`host.go:227-228` never read the field). The runner's PRODUCTION change for +(`host.go:284-285` never read the field). The runner's PRODUCTION change for first-turn delivery is NOT in this task — it is the additive deliver-lane build in PR-A (T-R1 `dispatch.go` `SessionsResponse_DeliverControl` arm, T-R2 `representable()` fix, T-R3 barrier-lift). This T3 slice is only the @@ -466,7 +466,7 @@ compile-against-regenerated-types proof plus gateway fixture cleanup. Interfaces: -- `go/internal/runner/host.go:227` — signature unchanged: `Start(ctx +- `go/internal/runner/host.go:284` — signature unchanged: `Start(ctx context.Context, req *compassv1.StartAgentSessionRequest, resumeBody string) (string, error)`; compiles against the regenerated type. - `go/internal/runner/gateway/lifecycle_test.go:65` — drop `InitialPrompt: @@ -617,7 +617,7 @@ harness re-model (T1–T7), lands SECOND, greens leg-2. PR-A (Runner deliver-lane + DM auto-provision — additive, independently landable): - [ ] T-R0 — spawn-edge manager↔peer DM auto-provision: get-or-create a per-pair `ChannelKindDM` channel (`store/types.go:74-75`) for the `{manager, peer}` pair via `CreateChannel` + `expandOwnerMembership` (`store/channels.go:72-140`), modeled on the `CoordinationHook` reconcile (`store/coordination.go:10-23`, fired in the CreateAgent tx) but pairwise + idempotent, so the manager can post the brief point-to-point (implement-hard, Go store + comms) -- [ ] T-R1 — runner dispatch: add a `SessionsResponse_DeliverControl` arm in `dispatch.go` (`:243` switch) routing the wrapped op to the container's control producer, so a server-pushed deliver is no longer met by the `:323` "unrecognized session command variant" default (implement-hard, Go runner) +- [ ] T-R1 — runner dispatch: add a `SessionsResponse_DeliverControl` arm in `dispatch.go` (`:359` switch) routing the wrapped op to the container's control producer, so a server-pushed deliver is no longer met by the `:449` "unrecognized session command variant" default (implement-hard, Go runner) - [ ] T-R2 — gateway control: fix `representable()` (`gateway/control.go:192-203`) to admit payload-carrying `Deliver`/`Steer` (they carry a comms `Message`), so the send is no longer dropped as an empty shell (implement, Go runner) - [ ] T-R3 — fresh-start barrier-lift: on a fresh (non-resume) start the Runner sends `AgentControl{replay_complete}` as the first control op after Bind (OQ-A ruling), so the first deliver is not refused-and-stranded by the agent barrier (`control-source.ts:278,373-390`) (implement-hard, Go runner + TS agent) From 0243e199c4924930db04f97729d35993cba541ce Mon Sep 17 00:00:00 2001 From: seal Date: Mon, 10 Aug 2026 16:04:12 -0400 Subject: [PATCH 3/3] =?UTF-8?q?docs(product):=20fold=20Matt's=20OQ-1=20rul?= =?UTF-8?q?ing=20=E2=80=94=20delete=20StartAgentDialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the design-PR gate Matt ruled OQ-1 (the one genuine fork left in the record): StartAgentDialog is DELETED in T5, not reduced to a bare confirm dialog. Removing initial_prompt empties the dialog (its only input was the prompt textarea); the board start affordance invokes the spawn action directly with {agentAccountId, workstreamId} (spawn is already guarded by the DL-164/168 live-session predicate and idempotent under its request id). Ground check at ruling time: StartAgentDialog.tsx + .test.tsx still present on main@origin and no open PR removes them, so the deletion is this design's own, landed by T5 in PR-B after #267/SEA-1932 merges (so DL-185's "Kept" clause exists to be amended). This resolves the record's last open fork; the status-flip is now "one (Matt-ruled)". Settle-primitive naming (AwaitTurnSettled) was already driver-resolved in-record. Refs SEA-1681, SEA-1820 Co-authored-by: Matt Wilkinson --- .../compass-first-turn-delivery/design.md | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/docs/designs/product/compass-first-turn-delivery/design.md b/docs/designs/product/compass-first-turn-delivery/design.md index bb85dcfe..b9edbab1 100644 --- a/docs/designs/product/compass-first-turn-delivery/design.md +++ b/docs/designs/product/compass-first-turn-delivery/design.md @@ -503,11 +503,12 @@ Interfaces: (`:168`) returns `"idle"` unconditionally (live `AgentSessionState` takes over from the first attributed status, per the existing DL-167 reconcile). - `apps/ui/src/components/StartAgentDialog.tsx` + `.test.tsx` — its only input - (the prompt textarea) is gone. Its disposition is OQ-1 (a genuine fork: - DL-185 explicitly KEEPS this dialog), Matt-ruled at this design-PR gate: - either deleted (the board start affordance invokes the spawn action directly - with `SpawnSpec`) or reduced to a bare confirm dialog. T5 implements whichever - Matt rules. + (the prompt textarea) is gone, so the dialog is **deleted** (Matt's + design-PR-gate ruling on OQ-1): the board start affordance invokes the spawn + action directly with `SpawnSpec` (`{agentAccountId, workstreamId}`; spawn is + already guarded by the DL-164/168 live-session predicate and idempotent under + its request id). This amends DL-185's "Kept: `StartAgentDialog`" clause — the + one status-flip, ordered after #267/SEA-1932 merges (see below). - `apps/ui/src/spawn.test.ts:24,38,60,65-67,177,183` — drop prompt fixtures; the two `bindingDotState` running-arm cases collapse to one (`running` → `"idle"`). @@ -576,15 +577,18 @@ path" is a DECISION, but the path's Runner middle leg (and the T-R0 DM auto-provision) is unbuilt today — PR-A (T-R0/T-R1/T-R2/T-R3) builds it. Do NOT land DL-186 as "verified end-to-end": the Runner leg is PR-A, not yet exercised. -**Status flips: one, conditional on OQ-1.** No existing DL row rules on +**Status flips: one (Matt-ruled).** No existing DL row rules on `initial_prompt`, start-idle semantics, or first-turn carriage (verified: `DECISIONS.md` has no occurrence of "initial" or "prompt" in any Decision cell bearing on the prompt field; DL-166/DL-164 rule the composite-spawn and -start-affordance shapes without it). **But DL-185 (SEA-1932, Active) explicitly +start-affordance shapes without it). **DL-185 (SEA-1932, Active) explicitly KEEPS `StartAgentDialog`** as the human's only start affordance — so T5's -disposition of that dialog (OQ-1) amends DL-185's "Kept" clause IF Matt rules -delete. That amendment is NOT applied here: it is Matt's call at this -design-PR gate (OQ-1). If Matt keeps a bare confirm dialog, no DL row flips. +**deletion** of that dialog (OQ-1, Matt-ruled delete at this design-PR gate) +amends DL-185's "Kept" clause. That amendment is NOT applied in this record: T5 +lands it in PR-B, ordered after #267/SEA-1932 merges so the "Kept" clause exists +to be amended. The driver records the flip on DL-185 in the design-ledger-gate +PR at that point, not here. + The superseded content lives in frozen RECORDS, not ledger rows. All six records carrying now-superseded `initial_prompt` content are superseded-in-part by DL-186 (the citable overturn) — three already noted @@ -627,29 +631,29 @@ PR-B (atomic removal + harness re-model — greens leg-2, lands after PR-A): - [ ] T2 — server: drop `InitialPrompt` at `lifecycle.go:325` and `spawn.go:135` + pgtest fixtures (implement) - [ ] T3 — runner: compile against regenerated types; gateway fixture cleanup (no production change beyond PR-A's deliver-lane, which T-R1/T-R2 already landed) (implement) - [ ] T4 — agent SDK: spawn tool loses `initial_prompt?`; tests updated (drop the JSON key as hygiene; NO unknown-key reject assert — see arktype note) (implement) -- [ ] T5 — UI: promptless `SpawnSpec`/binding, `running`→`idle` dot, dispose of `StartAgentDialog` per OQ-1 (delete, or reduce to a bare confirm dialog — Matt's design-gate ruling) (implement) +- [ ] T5 — UI: promptless `SpawnSpec`/binding, `running`→`idle` dot, delete `StartAgentDialog` + test (OQ-1 Matt-ruled delete; board start affordance calls the spawn action directly; amends DL-185's "Kept" clause after #267/SEA-1932 merges) (implement) - [ ] T6 — e2e harness re-model: promptless Start/Resume, home-channel `PostMessage` first turn, split into `OpenSessionTail`(before post) + `AwaitTurnSettled`(WORKING→READY), leg-2/leg-3-4 scenario updates (implement-hard; unblocks SEA-1792 H8 / PR #256) - [ ] T7 — ledger rows DL-186/DL-187/DL-188/DL-189 + `Status:` header; references case-1 follow-up SEA-1820 (no new issue filed) (driver) ## Open Questions -The four design-fork OQs (case-1 scope, deliver-lane carrier, barrier-lift -mechanism, system-sender freeze) are Matt-ruled and folded above -(DL-186..189 + SEA-1820). Two items remain for Matt at this design-PR gate: +All five design-fork OQs are resolved. The four load-bearing ones (case-1 +scope, deliver-lane carrier, barrier-lift mechanism, system-sender freeze) are +Matt-ruled and folded above (DL-186..189 + SEA-1820). OQ-1 (`StartAgentDialog` +disposition) is Matt-ruled at this design-PR gate: -1. **[FORK — intersects an Active decision] `StartAgentDialog` disposition.** +1. **`StartAgentDialog` disposition — RULED: delete (Matt, design-PR gate).** Removing `initial_prompt` empties this dialog (its only input was the prompt - textarea). But **DL-185 (SEA-1932, Active, Matt 2026-08-07) explicitly KEEPS - `StartAgentDialog`** as the human's sole start affordance after the - add-a-workstream surface was dropped — so this is NOT a driver call. - **Recommendation: delete it** and start directly from the board affordance - with `{agentAccountId, workstreamId}` (spawn is already guarded by the - DL-164/168 live-session predicate and idempotent under its request id) — an - empty dialog is pure friction. **Alternative: keep a bare confirm dialog** - ("Start this agent?"). Whichever Matt rules, T5 implements it; a delete - amends DL-185's "Kept" clause (the one conditional status-flip noted above). -2. **[Minor, driver-resolved] Settle-primitive naming → `AwaitTurnSettled`.** + textarea). DL-185 (SEA-1932, Active) explicitly KEEPS it, so this was a fork, + not a driver call. Matt ruled **delete**: the board start affordance invokes + the spawn action directly with `{agentAccountId, workstreamId}` (spawn is + already guarded by the DL-164/168 live-session predicate and idempotent under + its request id). T5 lands the deletion in PR-B, ordered after #267/SEA-1932 + merges; it amends DL-185's "Kept" clause (the one status-flip above). + (Ground check at ruling time: `StartAgentDialog.tsx` + `.test.tsx` still + present on `main@origin`; no open PR removes them — the deletion is this + design's, landed by T5.) +2. **Settle-primitive naming → `AwaitTurnSettled` (driver-resolved).** The new name (WORKING→READY, one settled turn) is the accurate one T6 uses; the old `AwaitSessionSettled` named a session-ready wait the re-model no - longer performs. Pure rename; flip is free if the dogfood-e2e lane prefers - the old name with the new contract. + longer performs. Pure rename, settled in-record.