feat(web): coding-agent storage, the model catalog, and the default model setting
Server-only groundwork for coding agents, on the coding-agents branch. Nothing here reaches the desktop, the Eve coding service, or the Start/list/messages/Stop routes.
Storage
conversations.kindgainscoding_agent. An agent's transcript will be itsmessagesrows and its status its latestturnsrow; a later PR writes those.- New
coding_agenttable (server/db/coding-agent-schema.ts, migration 0061):id,plan_id(cascade),user_id(cascade),conversation_id(cascade),model,effort,plan_snapshot,repository,idempotency_key(unique peruser_id),created_at. No commit column, per Dean. The barrel export puts it underdrizzle-schema.test.ts's drift check, andstorage-schema.test.tslists it. server/hosted/coding-agent-store.ts:createCodingAgent(one transaction under the plan's row lock, so a retry carrying the key of a Start already made answers that agent and writes nothing; opens thecoding_agentconversation beside the row),listCodingAgents(one plan's agents in start order),readCodingAgent(owner only). A plan another account owns starts nothing and reads as none.
The model catalog
server/hosted/model-catalog.tsreads AI Gateway's publicGET /v1/modelsthrough the ambientHttpClient, Schema-decodes it at the boundary, and keeps the Anthropic and OpenAI models taggedtool-useandreasoning; each model's efforts are thevaluesof theeffortentry in itsreasoning_options. A model listing no effort is left out, since no Start could name one for it (judgment call; the published catalog's Claude 4/4.5 Sonnet rows are the ones this drops).ModelCatalogis a service built once per instance inserver/runtime.ts, cached withEffect.cachedWithTTLfor one hour on a success and for no time on a failure, so an outage at the gateway is retried on the next request.GET /api/models(server/models-app.ts,server/routes/models.ts) answers{ models: [{ id, name, provider, efforts }] }to a signed-in bearer; the rewrites inapi-rewrites.jsonandvercel.jsonare regenerated.validateModelChoice(catalog, { model, effort })accepts only an offered model at an effort it lists;acceptedModelChoiceis the same over the service.providerModelOftranslates a catalog id to the provider's own (anthropic/claude-opus-5.5āclaude-opus-5-5; OpenAI ids unchanged after the prefix).
The default model and effort
account_preferencegainscoding_agent_modelandcoding_agent_effort(migration 0062). The same migration makesupdated_atnullable with no default: it is the preferences part's own instant, left null by a write carrying onlycodingAgent, because the desktop reads the answer'supdatedAtas a synced snapshot to apply over its local settings, and a Mac that never synced must not take an empty snapshot over the voice it holds (Devin's first finding)./api/account/preferencesanswerscodingAgent: { model, effort }besidepreferences,anthropic/claude-opus-5.5athighuntil the account chooses. A PUT carriespreferences,codingAgent, or both; a part left out stands as it was, so the desktop's existing settings sync (which sendspreferencesalone and ignores unknown answer keys) never resets the chosen model. AcodingAgentwrite is checked against the catalog: an unknown model or effort isinvalid-request, a catalog the instance cannot read isunavailable.
Goldens
fixtures/account-route/ goldens were regenerated under LUKE_UPDATE_FIXTURES=1: the three existing preferences goldens gain the codingAgent key in their bodies (nothing else moves), and ten new exchanges record the chosen-model read, the coding-agent writes (including a first write on an account that never synced, which answers no updatedAt), the kept-model write, the two refusals, the malformed choice, the catalog outage, and the empty body.
Tests
tests/model-catalog.test.ts (filter, effort list, cache over TestClock, failure not cached, boundary refusal, validation, id translation, the default accepted by the fixture catalog), tests/models-app.test.ts, tests/coding-agent-store.test.ts (idempotent create, order, ownership, cascade), and the extended tests/account-app.test.ts and tests/hosted-account-store.test.ts (round-trip of each part on its own). All hermetic: a fake HttpClient layer, PGlite, TestClock.
deletePlan now reads the plan's agents' conversations before the delete cascades the agent rows, and stamps them deleted beside the planning conversation, so their transcripts reach the thirty-day purge (Devin's second finding; covered in coding-agent-store.test.ts).
Follow-ups noted, not done here
- A stored default whose model later leaves the catalog is answered as stored; the Start route should refuse it and let the menu re-pick.
- The desktop PRs can lift the
/api/modelsanswer schema and thecodingAgentshape into@sidecar/hostedwhen they need a shared wire declaration.
š¤ Generated with Claude Code