4.0 KiB
Normalized external-agent status (#10, scoped)
Status: shipped v2.7.6-agent-status-normalize
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