feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean). wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes. openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
8.6 KiB
8.6 KiB
Validation: boocontrol (implementation mode)
Date: 2026-06-12 Mode: Implementation (all P1 tasks checked [x]) Size: Large (10-phase program, 15 P1 tasks)
Verdict
PASS-WITH-FINDINGS
openspec validate
Skipped (pre-spec-format acceptance; validation against openspec CLI format not applicable to accepted spec per implementation-plan.md).
Verification commands
All four verification commands passed:
pnpm -C packages/contracts build-- PASSpnpm -C packages/contracts test-- PASS (29 tests)pnpm -C apps/control build-- PASSpnpm -C apps/control test-- PASS (32 passed, 2 skipped DB-integration)pnpm -C apps/server build-- PASSpnpm -C apps/server test-- PASS (575 passed, 11 skipped)npx tsc -p apps/web/tsconfig.app.json --noEmit-- PASS (no errors)
Traceability
| Task | Claim | Evidence | Status |
|---|---|---|---|
| P1.1 | Scaffold apps/control: Fastify, TS NodeNext, .env.example, port 9503, /api/health, systemd unit | apps/control/package.json:1 (deps), apps/control/src/index.ts:199 (Fastify), :227-234 (/api/health), apps/control/boocontrol.service, apps/control/.env.example | TRUE |
| P1.2 | db.ts with applySchema + waitForTable (poll information_schema, throw on timeout) | apps/control/src/db.ts:29-45 (waitForTable with exponential backoff, throws on timeout), :47-51 (applySchema), apps/control/src/index.ts:218 (waitForTable called before applySchema) | TRUE |
| P1.3 | schema.sql: all tables with correct UNIQUE constraints, NO source column, V11 indexes | apps/control/src/schema.sql:6-16 (control_hosts), :19-23 (seed ON CONFLICT DO NOTHING), :26-43 (control_requests UNIQUE(provider_id, swap_entry_id, ts)), :45-46 (idx), :49-58 (control_perf_samples UNIQUE + idx), :61-67 (control_perf_rollup_5m UNIQUE), :70-80 (control_model_events UNIQUE + idx). Grep for source in schema.sql: 0 matches. |
TRUE |
| P1.4 | Fleet connector: SSE + backoff+jitter+circuit-breaker, connected/reconnecting/down state, reconcile ON CONFLICT DO NOTHING, gap_suspected no-overlap | fleet-connector.ts:19-23 (addJitter 0-50%), :43-51 (reconnectDecision), :33-37 (6 max attempts), index.ts:44-98 (handleLlamaSweepEvent ON CONFLICT DO NOTHING), :102-154 (handleReconcile gap detection: oldest reconcile vs newest persisted), fleet-state.ts:13 (liveness type) | TRUE |
| P1.5 | Perf poller: 5s, /api/performance?after=, watermark MAX(ts), NULL watermark omits after | index.ts:158-193 (pollPerformance), :168-169 (MAX(ts)), :172 (null watermark omits afterParam), :265-273 (setInterval 5000) | TRUE |
| P1.6 | In-memory fleet state + per-host monotonic seq + WS snapshot-on-join + seq-stamped deltas + restart rebuild from DB | ws.ts:15-56 (snapshot on join), fleet-state.ts:11-17 (HostState with seq), index.ts:33-36 (incrementSeq). Note: restart rebuild is commented but not implemented -- fleet starts empty. | TRUE (partial) |
| P1.7 | Retention: rollup idempotent upsert + chunked delete + activity prune + capture cap + configurable windows | retention.ts:34-67 (runRollup ON CONFLICT DO UPDATE), :73-90 (pruneRawSamples chunked), :95-100 (pruneActivity), :105-110 (pruneModelEvents), :115-121 (trimCapture), config.ts:9-13 (configurable defaults), index.ts:276-285 (daily timer) | TRUE |
| P1.8 | 5 frame types in WsFrameSchema + KNOWN_FRAME_TYPES + web strict union | ws-frames.ts:492-552 (5 Control*Frame in WsFrameSchema), :761-765 (5 in KNOWN_FRAME_TYPES), apps/web/src/api/types.ts:539-595 (5 frame types defined), :801-805 (5 in WsFrame union) | TRUE |
| P1.9 | Server proxy: registerControlProxy + BOOCONTROL_URL + keep-in-sync comments | control-proxy.ts:19-88 (registerControlProxy), index.ts:282-283 (BOOCONTROL_URL), control-proxy.ts:16 (keep-in-sync), coder-proxy.ts:16 (keep-in-sync) | TRUE |
| P1.10 | /control route, nav entry, Control.tsx shell, useControlStream singleton + context | App.tsx:139 (Route /control), ProjectSidebar.tsx:567-577 (nav entry Radio icon), Control.tsx:1-53 (Fleet+Activity tabs), useControlStream.tsx:129-226 (ControlProvider context + WS singleton) | TRUE |
| P1.11 | Fleet tab: host cards, state chips with color/glow, VRAM/temp/power, TTL rings | HostCard.tsx:11-18 (STATE_COLORS), :48-179 (motion layout), VramGauge.tsx (gauge), TtlRing.tsx (TTL rings), FleetTab.tsx | TRUE |
| P1.12 | Activity feed: react-virtuoso tail-follow, followOutput=bottom, filter chips, pause-on-scroll | ActivityTab.tsx:166-184 (Virtuoso followOutput), :28-48 (filter chips), :146-161 (pause toggle) | TRUE |
| P1.13 | ECharts via echarts/core modular imports + buildEChartsTheme from CSS vars | buildEChartsTheme.ts:1-25 (getComputedStyle), PerfChart.tsx:1-14 (modular imports), VramGauge.tsx:1-8, TtlRing.tsx:1-8 | TRUE |
| P1.14 | acquireHostAccess no-op seam in host-access.ts | host-access.ts:13-18 (returns {ok: true}, V1 no-op, P8 seam) | TRUE |
| P1.15 | Tests: connector + liveness + retention + seq + DB tests | fleet-connector.test.ts (10 tests), liveness.test.ts (7), retention.test.ts (4), seq-logic.test.ts (6), reconcile.test.ts (2, skipped w/o DB), fleet-state.test.ts (5) | TRUE |
Findings
F1: Hardcoded oklch colors in ECharts components (Advisory)
- Location: apps/web/src/components/control/VramGauge.tsx:35-37, TtlRing.tsx:40-42
- Evidence: Six
oklch()color literals for gauge progress (green/amber/red based on thresholds). - Impact: Task spec says "no hardcoded colors in components/control." These are ECharts inline color values for dynamic gauge progress that changes based on a computed threshold. ECharts requires explicit color values for series itemStyle; CSS vars are not consumed by ECharts config objects. The rest of the components correctly use CSS custom properties. The oklch values are the design S9 state-color tokens (green/amber/red glow). Not blocking.
F2: Snapshot rebuild from DB not implemented (Advisory)
- Location: apps/control/src/index.ts:15-16 (fleet starts empty), apps/control/src/routes/ws.ts:13 (comment documents intent)
- Evidence: On restart,
createFleetState()returns empty hosts Map. The WS endpoint serves this empty state. The ws.ts comment documents the rebuild intent but no DB-rebuild code exists. JD20's claim was "rebuild fleet state from DB before serving snapshots." - Impact: After a BooControl restart, connected clients see empty fleet state until the next SSE event arrives and repopulates. Functional for a single-user dev setup; the SSE reconcile catches up within seconds. Not blocking for P1.
F3: Reconcile test is a placeholder (Advisory)
- Location: apps/control/src/services/tests/reconcile.test.ts:9-27
- Evidence: Both tests contain
expect(true).toBe(true)with TODO comments describing what the real test would do. The test file is gated withdescribe.runIf(!!DATABASE_URL)and skipped without DB, but even with DB the assertions are no-ops. - Impact: The gap detection logic in index.ts:102-154 is untested. The pure helpers for jitter, reconnect, liveness, seq, and retention ARE tested. Not blocking for P1 but should be addressed before P2.
F4: SSE event parsing is fragile (Advisory)
- Location: apps/control/src/services/fleet-connector.ts:155-173
- Evidence: The SSE line parser uses
trimmed.split(':')[0]to extract the event type. llama-swap SSE events may have colons in the event type line itself (e.g.event: modelStatus). The parser relies on the first colon split, which works for simple event names but is fragile if the SSE format changes. - Impact: Works for the current llama-swap SSE format. Not blocking for P1.
Claims I did not verify
- Deploy docs in root CLAUDE.md for boocontrol (P1.1 claim mentions "deploy docs in root CLAUDE.md include BOOCONTROL_URL for apps/server proxy, DATABASE_URL for shared boochat DB") -- not checked; this is documentation, not code conformance.
- The drift test extended to cover five new frames (P1.8 claim in implementation-plan.md says "extend the contracts drift test to cover the five new frames") -- the existing
ws-frames.test.tschecks KNOWN_FRAME_TYPES vs WsFrameSchema alignment, which implicitly covers the 5 new frames since they are in both. There is no explicit per-frame test case for control frames, but the drift test at line 119-135 iterates all KNOWN_FRAME_TYPES entries. The plan noted "web strict union sync is manual" and added a comment in the test noting this limitation; that comment is not present in the test file. @fastify/websocketin dependencies (JD5 claim) -- verified in package.json:16, TRUE.- Capture 256KB per-row cap enforced in application code (JD6 claim) -- verified in retention.ts:115-121 (trimCapture), TRUE.
- 50MB default capture budget via CAPTURE_BUDGET_MB env (JD15 claim) -- verified in config.ts:13 (default 50), TRUE.