feat(hosted): a coding agent carries its rollout role, position, and base branch (rollout 4/15)
Ā
CH
5d ago+9364+9364+0123456789012345678901234567890123456789-12-12-01234567890123456789
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_agentgainsrollout_position(nullable int),base_branch(nullable text), androllout_role(nullable text,ROLLOUT_ROLE=builder|keeper) in migration0097_coding_agent_rollout.sql,ADD COLUMN IF NOT EXISTSlike 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?;CodingAgentcarriesrole,position,baseBranch.listCodingAgentslists builders first by position, then the rest in start order (main's archived filter kept).liveWorkerAtPosition(userId, planId, position)andliveKeeperOf(userId, planId)each answer the newest such row, not archived, whose newest turn is not failed or cancelled (isLiveWorkerTurn).createCodingAgentrefuses a second live keeper withKeeperTaken(tagged error, besidePlanArchived), 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;codingAgentSummarySchemagainspositionandrole, bothoptionalKeyand absent for an agent outside a rollout. Goldens regenerated (LUKE_UPDATE_FIXTURES=1).coder-host/start.tsnames no role yet, so aKeeperTakenthere is a defect (Effect.die). PR 5/14 maps it to a refusal once a Start can name a keeper.apps/web/server/README.mdnames the columns and the rules.
Decisions (agreed with the orchestrator)
- Live = the newest row whose status (
codingAgentStatusOf, over its newest turn andawaitingLinesOf) reads neitherfailednorcancelled, 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 throughCODER.STARTING_GRACE, or past it while a first line still waits on its turn (statusrunning); 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 reachesturnsandmessagesonly throughmessage-reads.ts(store-writer-boundary.test.ts). positionandroleon the summary areoptionalKey, absent for a plain agent.- 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
KeeperTakenand 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
runningand 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 withoutIF NOT EXISTS).coding-agents-app.test.ts: the list route's summary carriespositionandrolefor a builder and neither for a plain agent.drizzle-schema.test.tsholds 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