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>
8.3 KiB
8.3 KiB
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 at127.0.0.1:5500. Cross-cutting commands, database, environment, workflow, and cross-app contracts live in the rootCLAUDE.md. This file auto-loads when you read/edit files underapps/coder/.
Probe & provider discovery
services/provider-registry.ts— Static registry of provider metadata (label, transport, model source).PROVIDERSarray,PROVIDERS_BY_NAMEmap. 5 providers: boocode (native), opencode (acp), goose (pty), claude (pty), qwen (pty).PROBED_AGENT_NAMESderives from it — adding/removing providers means editing this file, not the frontend.services/agent-probe.ts— Startup probe via directexec()(not SSH): discovers installed agents, versions, ACP support, models. Qwen models from~/.qwen/settings.json; Claude models static from the registry. Persisted toavailable_agents.routes/providers.ts—GET /api/providersreturns installed providers with models. Transport reflects actual capability (checkssupports_acpfrom DB, not just registry preference). The apps/server side is "Provider picker dispatch" (seeapps/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 / stalePROVIDER_PROBE_TTL_MS24h / dbEmpty; cached). Verify live:curl http://100.114.205.53:9502/api/providers/snapshot— returns providers + models + commands, the exact shapeAgentComposerBarrenders. PATCH /api/providers/configreplaces a provider id's override object wholesale (per-id shallow merge) — to flip one field send{...existing, enabled}, or a custom ACP entry'scommand/labelis wiped and it drops out of the resolved registry.data/coder-providers.jsonis gitignored (live runtime config — the coder reads AND writes it on UI toggles); tracked reference isdata/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: importscreateInferenceRunner,createBroker,ALL_TOOLS,appendMcpToolsfrom the server's compileddist/. apps/server'spackage.jsonhas anexportsmap withtypesconditions 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 atapps/coder/.env.host. Service file at/etc/systemd/system/boocoder.service. - After
pnpm -C apps/coder buildthe host service keeps running the OLD process untilsudo systemctl restart boocoder— a stale process shows new routes 404 with{error:'not found'}while old routes still 200 (the/apinot-found handler shape). Restart, don't re-debug. :9502/api/healthis down ~15–20s 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_pathfromavailable_agents— nospawn('sh', ['-c', ...])(fails under systemd). Paseo's pattern:spawn(fullBinaryPath, argsArray, { cwd }). - systemd hardening: only
NoNewPrivileges=trueis safe.ProtectSystem,ProtectHome,PrivateTmpall break agent dispatch (agents need full filesystem access to read configs, write to worktrees). apps/server/tsconfig.jsonhasdeclaration: trueso.d.tsfiles exist for workspace consumers. The provider'spackage.jsonneedsexportswithtypes+defaultconditions per subpath ("./inference": { "types": "./dist/.../index.d.ts", "default": "./dist/.../index.js" }) — without thetypescondition, NodeNext can't find.d.tsfiles and tsc fails "Cannot find module" here.- Write tools (
edit_file,create_file,delete_file,apply_pending,rewind) queue inpending_changes. Nothing hits disk untilapply_pending.write_guard.tsvalidates 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.ts—opencode serveper BooCoder process, one opencode session per BooCode session, resumed viaagent_sessions). goose/qwen/claude dispatch one-shot ACP/PTY with no ctx/token usage; only nativeboocode(llama-swap) tracks ctx. - opencode SSE (
opencode-server.ts): live streaming issession.next.text.delta/.reasoning.delta/.tool.{called,success,failed}— NOTmessage.part.*(terminal/post-hoc).client.event.subscribe({ directory })MUST pass the session's worktree dir; omit it and opencode scopes events to the serverprocess.cwd()→ zero session events (empty turns, 180s timeout). Each live session owns its own subscribe loop + AbortController (asessionIDdemux guard drops cross-session events when two share a dir). Turn completes onsession.idle;promptAsyncis fire-and-forget (204). - opencode model strings must be provider-prefixed (
llama-swap/<model>) AND exist in~/.config/opencode/opencode.jsonprovider.llama-swap.models— not merely loadable by llama-swap.parseModelinfersllama-swap/for a bare id; the dispatcher coalesces empty→DEFAULT_MODEL then prefixes.agent-probepopulates opencode'savailable_agents.modelsviamergeLlamaSwap(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_idCASCADEs fromchats;session_id/worktree_idare informationalSET NULL. Theworktreestable (one-per-session, survives session delete) supersedes the defangedsession_worktrees.tasks.chat_idthreads the tab id to the dispatcher;runOpenCodeServerTaskresolves-or-creates a chat when null. The@opencode-ai/sdkv2 client takes flattened params ({sessionID, directory, parts, model:{providerID,modelID}}),createOpencodeClientfrom@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 theusercase → a terminaltool_update(completed/failed + output), else the tool_call persistsstatus:'running'and the UI spinner never stops. The dispatcher'stool_updatepath then publishes + persists it. - ACP command discovery is async:
acp-probe.tsmust poll afternewSessionforavailable_commands_update(commands arrive in a later notification; reading synchronously captures 0). PTY providers (claude) discover from disk viaclaude-command-discovery.ts(~/.claude/commands+enabledPlugins, bare names, deduped).AgentCommand.kindtags'command'vs'skill';CoderPane'sslashGroupssplits them into icon'd groups.SlashCommandPicker'sgroups?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 WSsnapshotSELECT (apps/coder/src/routes/ws.ts) — it has its OWN column list and the client'ssnapshothandlersetMessages-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 livemessage_completeWS reducer),CoderMessageWire(CoderMessageList.tsx), andapi/types.ts. The clientmapCoderTimelineRowwhitelists fields — easiest to forget. This bitmodeltwice: 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.