Scoped half of boocode_code_review_v2 §1 #10 — publish the agent status BooCoder already observes (the config-injection notify-hook is the documented follow-on, clean-room from superset ELv2). - agent_status_updated WS frame (working|blocked|idle|error), server+web parity. - Published from the dispatcher's turn boundaries (warm-acp/opencode/sdk/pty: working at start, idle/error at end) + the permission flow (blocked/working). Best-effort, never breaks a turn. - Clean-room normalizeAgentEvent helper (superset's vendor-event -> Start/blocked /Stop collapse, event names as facts) + 25 tests — reused by the follow-on. - AgentComposerBar status dot (distinct from the WS-liveness dot), tracked per (chat,agent) by a useAgentStatus map in CoderPane. Built by 2 parallel agents vs a pinned frame contract. Server 545 + coder 294 tests passing (25 new); web tsc + builds clean; ws-frames parity green. Clears the actionable review backlog (#1/#3/#4/#6-#12). Builds on v2.7.5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.0 KiB
Normalized external-agent status (#10, scoped)
Status: in progress (started 2026-06-01)
Source: boocode_code_review_v2.md §1 #10, §5j (superset, Elastic License 2.0 — PATTERN-ONLY,
clean-room; /opt/forks/superset/.../map-event-type.ts, notify-hook.template.sh, agent-setup/*).
Decision (Sam, 2026-06-01): scoped status-publish now; config-injection notify-hook as a follow-on.
Why (corrected premise)
BooCoder already observes agent lifecycle (warm-acp/opencode/SDK backends know active/idle/crashed;
the permission-waiter knows blocked) but never publishes a normalized per-(chat,agent) status to the
UI — so blocked-on-permission is invisible and crash/idle aren't pushed proactively. The AgentComposerBar
dot only shows WS liveness. This batch publishes the status BooCoder already knows; the heavier
config-injection notify-hook (for out-of-band signals) is the documented follow-on.
State model (clean-room from superset's mapEventType)
Superset collapses ~30 vendor event names → 3 signals: Start (working), PermissionRequest
(blocked), Stop (done). BooCoder adds idle (after done) + error (crash/fail). Normalized status:
working | blocked | idle | error.
Pinned frame contract (server + web, byte-identical, parity-tested)
{ type: 'agent_status_updated', chat_id: Uuid, agent: string,
status: 'working' | 'blocked' | 'idle' | 'error', reason?: string, at: IsoTimestamp }
Added to apps/server/src/types/ws-frames.ts AND apps/web/src/api/ws-frames.ts (the ws-frames parity
test), plus the web WsFrame union in apps/web/src/api/types.ts. Published via the coder's
broker.publishFrame (validated against the server WsFrameSchema).
Clean-room normalize helper (built now, reused by the injection follow-on)
apps/coder/src/services/normalize-agent-status.ts:
normalizeAgentEvent(raw: string): 'working' | 'blocked' | 'done' | null — a clean-room reimplementation
of the vendor-event-name → bucket mapping (the event names are facts about each agent's hooks:
SessionStart/UserPromptSubmit/PostToolUse→working; PreToolUse/Notification/PermissionRequest/
exec_approval_request→blocked; Stop/session_end/task_complete→done). The scoped publish points use
BooCoder's own already-normalized turn boundaries; this helper exists so the config-injection follow-on
(which receives raw vendor event names POSTed from agent hooks) reuses it. Unit-tested.
Publish points (BooCoder's existing observation — no per-backend change)
- Dispatcher (
dispatcher.ts) turn boundaries, for every external-agent path (warm-acp/opencode/sdk/pty):workingat turn start,idleon clean completion,erroron failure. - Permission-waiter (
permission-waiter.ts/ thesetPermissionHookspublish inindex.ts):blockedwhen a permission is requested, back toworkingwhen resolved. A smallpublishAgentStatus(broker, chatId, agent, status, reason?)helper centralizes the frame.
Frontend
CoderPane.tsxtracks the latestagent_status_updatedper(chat, agent)(a small live map; reset on chat switch).AgentComposerBar.tsxrenders a normalized status dot beside the existing session chip (reuse theStatusDotvisual language: working=spinner/green, blocked=amber, idle=gray, error=red), distinct from the WS-livenessconnecteddot.
Follow-on (documented, not built): config-injection notify-hook
Clean-room re-derive superset's agent-setup: inject a notify hook into each agent's native config
(claude ~/.claude/settings.json, opencode plugin, codex/gemini templates) that POSTs
{agent, chat_id, eventType} to a new POST /api/coder/agent-status endpoint, which runs
normalizeAgentEvent → publishes the SAME agent_status_updated frame. Reuses everything this batch
builds. Catches out-of-band signals BooCoder's dispatch can't see.
Verify
pnpm -C apps/coder test(+ normalize-agent-status tests) +pnpm -C apps/server test(ws-frames parity)pnpm -C apps/server build && pnpm -C apps/coder build;npx tsc -p apps/web/tsconfig.app.json --noEmit