Files
boocode/apps/coder/CLAUDE.md
indifferentketchup e5ce01ae72 fix(coder): include model in WS snapshot SELECT so the attribution chip survives refresh
CoderPane hydrates from the HTTP listMessages fetch (SELECT has model) AND the WS snapshot frame, and the snapshot handler setMessages-overwrites the HTTP load. The snapshot query in apps/coder/src/routes/ws.ts had its own column list that omitted model, so on coder refresh the chip's model was lost (it showed live via the message_complete frame). One-column fix: add model to that SELECT. CLAUDE.md mapper-chain note updated to list the WS snapshot SELECT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 18:03:10 +00:00

8.3 KiB
Raw Blame History

apps/coder — BooCoder (deep reference)

Per-app engineering notes for apps/coder/src/. BooCoder runs as a systemd service on the host (boocoder.service), NOT in Docker — Fastify at port 9502, postgres at 127.0.0.1:5500. Cross-cutting commands, database, environment, workflow, and cross-app contracts live in the root CLAUDE.md. This file auto-loads when you read/edit files under apps/coder/.

Probe & provider discovery

  • services/provider-registry.ts — Static registry of provider metadata (label, transport, model source). PROVIDERS array, PROVIDERS_BY_NAME map. 5 providers: boocode (native), opencode (acp), goose (pty), claude (pty), qwen (pty). PROBED_AGENT_NAMES derives from it — adding/removing providers means editing this file, not the frontend.
  • services/agent-probe.ts — Startup probe via direct exec() (not SSH): discovers installed agents, versions, ACP support, models. Qwen models from ~/.qwen/settings.json; Claude models static from the registry. Persisted to available_agents.
  • routes/providers.tsGET /api/providers returns installed providers with models. Transport reflects actual capability (checks supports_acp from DB, not just registry preference). The apps/server side is "Provider picker dispatch" (see apps/server/CLAUDE.md).
  • Provider snapshot lifecycle (services/): provider-config.ts (Zod config, never-throws) → provider-config-registry.ts (buildResolvedRegistry, singleton) → provider-snapshot.ts (two-tier probe: tier-1 fast presence, tier-2 cold ACP probe skipped unless force / stale PROVIDER_PROBE_TTL_MS 24h / dbEmpty; cached). Verify live: curl http://100.114.205.53:9502/api/providers/snapshot — returns providers + models + commands, the exact shape AgentComposerBar renders.
  • PATCH /api/providers/config replaces a provider id's override object wholesale (per-id shallow merge) — to flip one field send {...existing, enabled}, or a custom ACP entry's command/label is wiped and it drops out of the resolved registry. data/coder-providers.json is gitignored (live runtime config — the coder reads AND writes it on UI toggles); tracked reference is data/coder-providers.example.json. The loader falls back to {providers:{}} (built-ins only) when absent, so a fresh checkout needs no copy.

Build, deploy, dispatch

  • Workspace dependency on @boocode/server: imports createInferenceRunner, createBroker, ALL_TOOLS, appendMcpTools from the server's compiled dist/. apps/server's package.json has an exports map with types conditions for NodeNext resolution. apps/server must build FIRST.
  • Build + deploy: pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder. Env file at apps/coder/.env.host. Service file at /etc/systemd/system/boocoder.service.
  • After pnpm -C apps/coder build the host service keeps running the OLD process until sudo systemctl restart boocoder — a stale process shows new routes 404 with {error:'not found'} while old routes still 200 (the /api not-found handler shape). Restart, don't re-debug.
  • :9502/api/health is down ~1520s after a boocoder restart while the startup agent-probe scan runs — retry; an early connection-refused is not a failed deploy.
  • Agent dispatch spawns binaries directly using install_path from available_agents — no spawn('sh', ['-c', ...]) (fails under systemd). Paseo's pattern: spawn(fullBinaryPath, argsArray, { cwd }).
  • systemd hardening: only NoNewPrivileges=true is safe. ProtectSystem, ProtectHome, PrivateTmp all break agent dispatch (agents need full filesystem access to read configs, write to worktrees).
  • apps/server/tsconfig.json has declaration: true so .d.ts files exist for workspace consumers. The provider's package.json needs exports with types + default conditions per subpath ("./inference": { "types": "./dist/.../index.d.ts", "default": "./dist/.../index.js" }) — without the types condition, NodeNext can't find .d.ts files and tsc fails "Cannot find module" here.
  • Write tools (edit_file, create_file, delete_file, apply_pending, rewind) queue in pending_changes. Nothing hits disk until apply_pending. write_guard.ts validates paths (resolve + prefix-check, no realpath since files may not exist for creates).

Backends

Behavioral overview + flows + data model: see /docs/coder-backends.md. The notes below are the deep per-fact reference.

  • opencode runs as a warm HTTP server (services/backends/opencode-server.tsopencode serve per BooCoder process, one opencode session per BooCode session, resumed via agent_sessions). goose/qwen/claude dispatch one-shot ACP/PTY with no ctx/token usage; only native boocode (llama-swap) tracks ctx.
  • opencode SSE (opencode-server.ts): live streaming is session.next.text.delta / .reasoning.delta / .tool.{called,success,failed} — NOT message.part.* (terminal/post-hoc). client.event.subscribe({ directory }) MUST pass the session's worktree dir; omit it and opencode scopes events to the server process.cwd() → zero session events (empty turns, 180s timeout). Each live session owns its own subscribe loop + AbortController (a sessionID demux guard drops cross-session events when two share a dir). Turn completes on session.idle; promptAsync is fire-and-forget (204).
  • opencode model strings must be provider-prefixed (llama-swap/<model>) AND exist in ~/.config/opencode/opencode.json provider.llama-swap.models — not merely loadable by llama-swap. parseModel infers llama-swap/ for a bare id; the dispatcher coalesces empty→DEFAULT_MODEL then prefixes. agent-probe populates opencode's available_agents.models via mergeLlamaSwap (fetches /v1/models); empty model list → frontend sends '' → no inference (empty turn).
  • agent_sessions resume: config_hash = sha256('opencode_server|<model>') — must NOT include the server port (random per boot; breaks cross-restart resume). Keyed (chat_id, agent) — the tab/chat is the context unit (two opencode tabs = two contexts sharing one worktree). chat_id CASCADEs from chats; session_id/worktree_id are informational SET NULL. The worktrees table (one-per-session, survives session delete) supersedes the defanged session_worktrees. tasks.chat_id threads the tab id to the dispatcher; runOpenCodeServerTask resolves-or-creates a chat when null. The @opencode-ai/sdk v2 client takes flattened params ({sessionID, directory, parts, model:{providerID,modelID}}), createOpencodeClient from @opencode-ai/sdk/v2/client.
  • Claude SDK backend tool RESULTS arrive as type:'user' SDK messages (tool_result content blocks): mapSdkMessage (claude-sdk-map.ts) MUST map the user case → a terminal tool_update (completed/failed + output), else the tool_call persists status:'running' and the UI spinner never stops. The dispatcher's tool_update path then publishes + persists it.
  • ACP command discovery is async: acp-probe.ts must poll after newSession for available_commands_update (commands arrive in a later notification; reading synchronously captures 0). PTY providers (claude) discover from disk via claude-command-discovery.ts (~/.claude/commands + enabledPlugins, bare names, deduped). AgentCommand.kind tags 'command' vs 'skill'; CoderPane's slashGroups splits them into icon'd groups. SlashCommandPicker's groups? prop is opt-in.
  • A new per-message coder field silently drops unless you update every mapper: the HTTP read SELECT + mapCoderMessageRow (apps/coder/src/routes/messages.ts), the WS snapshot SELECT (apps/coder/src/routes/ws.ts) — it has its OWN column list and the client's snapshot handler setMessages-overwrites the HTTP load, so a field present in the HTTP route but absent here shows live yet vanishes on refresh — CoderPane.tsx (RawCoderMessage/CoderMessage/mapCoderTimelineRow + the live message_complete WS reducer), CoderMessageWire (CoderMessageList.tsx), and api/types.ts. The client mapCoderTimelineRow whitelists fields — easiest to forget. This bit model twice: the client chain (v2.7.9) and then the WS snapshot SELECT (v2.7.11) — the chip showed live but vanished on coder refresh until both were fixed.