feat(database): an agent may have no repository and no first message, and a call names its agent

Merged
DA
5d ago
+15129-110

PR 1 of 7 in the Unified agents rollout. A voice chat is becoming an agent with a call attached. It can live on a task with no repository and start blank, and a call has to name the agent it speaks into. This PR changes the schema and makes every reader type-safe. No service, voice or UI behaviour changes, and today's Start still requires a repository and still writes one.

Schema (migration 20261020180912_coding_agent_optional_repository_voice_sessions_agent)

  • coding_agent.repository drops NOT NULL. repositories stays a list and is written [] for an agent with none.
  • voice_sessions.agent_id is new: a nullable uuid FK to coding_agent.id, ON DELETE SET NULL, indexed (voice_sessions_agent). Nothing writes it yet, and task_id stays.
  • The migration only widens: ADD COLUMN IF NOT EXISTS, DROP NOT NULL, CREATE INDEX IF NOT EXISTS, and the FK added after a DROP CONSTRAINT IF EXISTS. Existing agent_id values are all null, so the FK can't break any row. The previous deployment keeps working, and there is no backfill. drizzle-kit check passes.

"Exactly one of plan snapshot and prompt"

There is no check constraint for this. The rule lives in the store's CodingAgentBriefText type, which now has a third variant with neither (an agent started blank). The coder host's Start path (start.ts) keeps the old pair through HandedPlace, so it still hands the plan or a prompt and never nothing.

Readers

  • The store gains WithRepository<A> and hasRepository(agent).
  • Coder host: one guard at its door. agentOf refuses an agent with no repository with CODER_REFUSAL.NO_REPOSITORY, and its seams (checkout, prompt, skills, pull-request, recordContext) take WithRepository<CodingAgent>. A coding session is repository work all the way through (clone, firewall token, instructions naming the repo). The orchestrator agreed that running a session with no repository belongs to PR 2, which lifts this guard.
  • Everywhere else, an agent with no repository skips the repository work:
    • The Pull request tab and overview, Changes, and chapters answer no pull request. Writes answer not found. Review options answer repository-not-reachable.
    • Published reads answer nothing published, and GitHub images answer not found.
    • The terminal does not reach the agent, and steer skips the GitHub reach check.
    • Replay lineage, source copy, GitHub wakes, factory world and automation runs simply don't match a repository.
    • Mention notes and agent introductions leave out "working in …".
  • Wire: codingAgentPageSchema.repository (and the taskless agent line built from it) and codingAgentPullRequestAnswerSchema.repository become NullOr. The panel and host readers handle null: no compare link, no taskless subject. The admin account page's agent repository becomes nullable as well.
  • Taskless inbox items keep repository: string on the wire. The two reads now leave out an agent with neither a task nor a repository, which no code path can create. Widening that wire would have reached the inbox UI, which is outside this PR.

Tests

  • packages/database/src/models/__tests__/coding-agent-store.test.ts: an agent on a task with no repository, handed nothing, is stored and read back by a teammate with no repository, no repositories, and neither plan nor prompt (in both the full read and the line). A voice session naming it outlives the agent's delete with agent_id set to null. This test fails without the change.
  • coder-host.test.ts: the host refuses an agent with no repository with NO_REPOSITORY.
  • The coder-host tests that pass an agent to the checkout seams narrow it with assert.ok(hasRepository(agent)).
  • The JSON Schema goldens were re-recorded with LUKE_UPDATE_FIXTURES=1: 7 files under packages/features/fixtures/json-schema/. The only change in each is repository going from a string to string-or-null on the agent page, the taskless agent line and the published answer, plus the view schemas and the task list built from them.

Checks run locally

  • Typecheck is clean in every workspace. This sandbox has 8 GB, which isn't enough to run all of them in parallel, so they ran one at a time.
  • The affected tests all pass: drizzle-kit check, contractions, biome and oxlint, and discarded-effect.
  • The one exception was desktop's app-menu.test.ts, which failed only because the sandbox had no Electron binary; that is being re-run now.

Made with Luke


Devin Review