Files
boocode/openspec/changes/archived/agent-status-normalize/proposal.md
indifferentketchup 2a05d2f9fe docs: archive shipped openspec batches; add feature/plan/research notes
Move 13 shipped openspec change docs under openspec/changes/archived/.
Add docs/features/git-diff-panel, docs/plans/post-review-backlog, and
docs/research/cross-app-contract-ssot.md (the research behind the
@boocode/contracts SSOT work). Update BOOCHAT.md, BOOCODER.md, and
boocode_roadmap.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:20:33 +00:00

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): working at turn start, idle on clean completion, error on failure.
  • Permission-waiter (permission-waiter.ts / the setPermissionHooks publish in index.ts): blocked when a permission is requested, back to working when resolved. A small publishAgentStatus(broker, chatId, agent, status, reason?) helper centralizes the frame.

Frontend

  • CoderPane.tsx tracks the latest agent_status_updated per (chat, agent) (a small live map; reset on chat switch).
  • AgentComposerBar.tsx renders a normalized status dot beside the existing session chip (reuse the StatusDot visual language: working=spinner/green, blocked=amber, idle=gray, error=red), distinct from the WS-liveness connected dot.

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