feat(hosted): a coding agent carries its rollout role, position, and base branch (rollout 4/15)

CH
5d ago
+9364-12

PR 4 of 15 in the rollout stack (base: #2227, luke-rollout/1-rollout-field).

What

A coding agent started for a plan's rollout carries its role there: one builder per pull request, at its position, and one keeper per rollout, at no position, which owns the stack's git structure. The plan's rollout is the graph; these rows are its state. No rollout table.

  • coding_agent gains rollout_position (nullable int), base_branch (nullable text), and rollout_role (nullable text, ROLLOUT_ROLE = builder | keeper) in migration 0097_coding_agent_rollout.sql, ADD COLUMN IF NOT EXISTS like PR 1's 0096, so a Preview database that ran it under an earlier number takes it again unchanged.
  • coding-agent-store.ts:
    • NewCodingAgent.role? / position? / baseBranch?; CodingAgent carries role, position, baseBranch.
    • listCodingAgents lists builders first by position, then the rest in start order (main's archived filter kept).
    • liveWorkerAtPosition(userId, planId, position) and liveKeeperOf(userId, planId) each answer the newest such row, not archived, whose newest turn is not failed or cancelled (isLiveWorkerTurn).
    • createCodingAgent refuses a second live keeper with KeeperTaken (tagged error, beside PlanArchived), under the plan's row lock. A retry under the same key still answers the agent its Start made.
  • coding-agent-wire.ts: ROLLOUT_ROLE, RolloutRole, rolloutRoleSchema; codingAgentSummarySchema gains position and role, both optionalKey and absent for an agent outside a rollout. Goldens regenerated (LUKE_UPDATE_FIXTURES=1).
  • coder-host/start.ts names no role yet, so a KeeperTaken there is a defect (Effect.die). PR 5/14 maps it to a refusal once a Start can name a keeper.
  • apps/web/server/README.md names the columns and the rules.

Decisions (agreed with the orchestrator)

  1. Live = the newest row whose status (codingAgentStatusOf, over its newest turn and awaitingLinesOf) reads neither failed nor cancelled, so the place and the status never disagree. A running one and a settled one hold their place. One with no turn yet holds it through CODER.STARTING_GRACE, or past it while a first line still waits on its turn (status running); with nothing waiting past the grace its status reads failed and the place is free. A running turn carrying a Stop reads cancelled and frees it too. A failed or stopped one frees its place for a restart, and so does one the developer archived, which holds it again once brought back. The store still reaches turns and messages only through message-reads.ts (store-writer-boundary.test.ts).
  2. position and role on the summary are optionalKey, absent for a plain agent.
  3. Roles: a builder keeps rollout_position; the keeper has none. The store does not cross-check role against position; the Start that names them does.

Tests

  • coding-agent-store.test.ts:
    • A Start at a position holds role, position, and base, and builders list by position before plain agents.
    • The live builder across no turn, settled, failed, running, cancelled, restart, and archived and back, plus another position and another account.
    • A second live keeper is refused with KeeperTaken and writes nothing, while a builder starts beside it; a failed or a stopped keeper frees the place, and a settled one holds it.
    • One account reads no keeper of another's plan, and its own keeper is never refused for another's.
    • A builder and a keeper with no turn hold their places through the starting grace (a second keeper is still refused) and free them one second past it, under TestClock; a replacement then holds each.
    • Past the grace, a builder and a keeper whose first line still waits read running and hold their places (a second keeper is still refused), while a builder with nothing waiting frees its place.
  • coding-agent-rollout-migration.test.ts (PR 1's pattern): an agent stored before 0097 reads with no role, position, or base; running 0097 again over a database that already has the columns keeps a builder's role, position, and base (this case fails without IF NOT EXISTS).
  • coding-agents-app.test.ts: the list route's summary carries position and role for a builder and neither for a plain agent.
  • drizzle-schema.test.ts holds the module to the migrated database.

Every new test fails on the base. ./scripts/check.sh exits 0 locally, and pnpm --filter @luke/web test:store passes on a local Postgres 17 (69 files, 740 tests); CI's store job runs it on Postgres 18.

No Mac or UI change.

šŸ¤– Generated with Claude Code


Open workspace in Conductor