- CHANGELOG: v2.8.0-fork-lifts entry covering all 8 integrations - Roadmap: update shipped header through v2.8.0, bump last-updated date - CURRENT.md: reflect fork-lifts as last-shipped batch - apps/coder/CLAUDE.md: document edit-guards behavior and API
10 KiB
10 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 packages/contracts build && 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.
Orchestrator (v2.7.17)
- In-app multi-agent conductor:
services/flow-runner.tsruns a flow by inserting each step as atasksrow (the existing dispatcher runs it) and advancing on a newonTaskTerminaldispatcher-deps hook; persisted inflow_runs/flow_steps(resumed at startup viainitResume). The 22 conductor flow defs + Spine factory are re-homed undersrc/conductor/. Pure scheduler/resume helpers inflow-runner-decisions.ts. Full design:openspec/changes/archived/orchestrator/. - Read-only is load-bearing — don't add a dispatch path that bypasses it. Every step dispatches
agent='qwen', mode_id='plan';dispatcher.tsforce-routes qwen+plan to the PTY--approval-mode plangate and HARD-FAILS the task (never falls to write-capable native inference) when qwen is unavailable (shouldFailOnMissingAgent).BOOCODE_TOOLSgates BooChat's NATIVE inference tools only — it does NOT govern an external CLI agent (qwen/opencode bring their own write tools); read-only for a dispatched agent is the agent-layer mode (PTY--approval-mode plan; ACPsetSessionModeis fail-OPEN by default, fail-CLOSED forplanviaREAD_ONLY_MODE_IDSinacp-dispatch.ts).
Edit safety guards (v2.8)
services/edit-guards.ts—validateEditResult(original, updated, filePath)runs inpending_changes.tsimmediately beforewriteFileAtomic. Rejects catastrophic truncation (>60% char loss AND >50% line loss). Throws aformatGuardErrormessage that percolates to the agent as a visible error.services/edit-guards-imports.ts—checkDroppedImports(original, updated, filePath)detects removed import/require lines. Called alongside the truncation guard.- Both guards run on the
/applypath only (not on queue). Re-queued identical edits re-validate at apply time. - Guard functions are pure — no DB or filesystem access. Easy to unit-test.