# lvmh-agent selftest How the "BULLETPROOF" constraints were verified for `lvmh-agent.ts`. Line numbers refer to `lvmh-agent.ts` in this directory. ## Validation runs (all executed, 2026-08-18) | check | command | result | | --- | --- | --- | | Typecheck (strict, real pi types) | `npx --package typescript@5.9 tsc --noEmit -p .` in `plugin/` | **exit 0**, no errors | | Offline behavior vs mini-daemon | `node plugin/smoke.ts` | **25/25 ok** (run 3× consecutive, all green) | | Real pi end-to-end vs mini-daemon | `node plugin/e2e.ts` | **13/13 ok** (one real LLM call via `-p`) | | Real pi + dead host | `LVMH_URL=ws://127.0.0.1:9/agent/ws LVMH_TOKEN=x pi -e ./plugin/lvmh-agent.ts -p "Reply with exactly: ok"` | replies `ok`, **exit 0**, plugin silently retries (log only, growing backoff), stdout clean | Environment: Node v26.7.0, pi 0.84.1, TypeScript 5.9. ## Constraint checklist ### 1. Never crash pi / never throw from a handler Every `pi.on` subscription goes through `sub()` (L183–204): the handler body runs inside `try/catch`; if it returns a promise, rejections are swallowed via `.then(undefined, …)`; the wrapper itself always returns `undefined`. pi can therefore never observe a throw or an unhandled rejection from this extension, regardless of which of the 13 registered handlers fails (L459–617). Handler-internal risk points and their guards: | code path | guard | | --- | --- | | `log()` (L75–82) | `try/catch` around `fs.appendFile`; callback discards errors | | `nextSeq`/map/emit payload building | pure data code; `mapMessage` (L113) and `textOfContent` (L91) treat every field as `unknown` and type-check before use | | `enqueue`/`flush` (L206–240) | `JSON.stringify` in `try/catch` (circular args → skip frame, L224); `ws.send` in `try/catch` → treated as disconnect + reconnect scheduled (L230–236) | | `connect()` (L288) | constructor in `try/catch` (invalid URL → backoff, L293); all four socket callbacks (`onopen` L297, `onmessage` L312, `onclose` L325) wrap their bodies in `try/catch`; stale-socket identity guard `if (ws !== socket) return` so an old socket's late callbacks can never clobber the new socket's state | | `onMessage` (L382) | `JSON.parse` in `try/catch` (L385); non-object frames, wrong `v`, unknown types ignored | | `deliverPrompt` (L400) | `pi.sendUserMessage(..., {deliverAs:"steer"})` in `try/catch` (L406); empty or non-string message ignored; foreign `sessionId` ignored | | `doAbort` (L411) | `pi.abort` (if present) else `lastCtx.abort()` in `try/catch` | | `buildSnapshot` (L421) | all `sessionManager` reads in `try/catch` with fallbacks (L433: ctx.cwd / Date.now) | | `session_start` (L459) | `getSessionId()` in `try/catch`; no id ⇒ mirroring disabled for that session, no crash | | `session_shutdown` (L490) | `clearTimeout` and bye-send/close both in `try/catch` (L494, L500) | | `onWelcome` (L349) | validates `lastSeq` (`Number.isFinite`, else 0, L355); foreign-session welcome ignored (L350) | That is **14 explicit `try/catch` blocks** plus the universal `sub()` wrapper. Static pass over every function: no code path can throw to pi; nothing `await`s — all I/O is fire-and-forget async (`fs.appendFile`, undici WS), so nothing can block or hang the event loop either. The only synchronous work is JSON serialization of outbound frames (bounded by message size). ### 2. Zero npm dependencies Imports: `node:fs`, `node:os`, `node:path` (L29–31), type-only import of `@earendil-works/pi-coding-agent` (L33, erased at runtime), and the Node ≥ 22 global `WebSocket` (undici). Verified by `grep import` and by the e2e run against real pi. `typeof WebSocket === "undefined"` guard at L156 makes the extension inert (not broken) on older Node. ### 3. Bounded queue + drop-oldest + buffer_overflow - Send queue cap 1000: `enqueue()` L206–215 (`while` shift-oldest, every drop counted and logged). - Replay buffer cap 10000 (persisted-kind events only; `message_update` is excluded per protocol): `emit()` L241–252. - Drops are never silent: `droppedEvents > 0` ⇒ `emit("buffer_overflow", {dropped})` immediately after the next `welcome` is processed (L373–376). - Persisted-kind events dropped from the send queue are still in the replay buffer and get recovered by replay; only `message_update` deltas can be lost. `onWelcome` additionally filters persisted frames out of the stale queue before merging replay (L367–368) so replay + queue can never double-send an event. - Smoke scenario 4 proves it: 10 050 persisted events fired while disconnected → reconnect → `buffer_overflow {dropped: n>0}` observed on the wire, replay delivered, no duplicate seq. ### 4. WS send failures never propagate `flush()` L230–236: `ws.send` in `try/catch` → log, `handleDisconnect()`, `scheduleReconnect()`. Same treatment in `session_shutdown` bye-send (L500). Proven by smoke scenario 4 (server destroys sockets under load) and e2e. ### 5. Reconnect: backoff + jitter, cap 30s, forever, never throws `scheduleReconnect()` L274–286: `min(1000·2^attempt, 30000) + jitter[0,1000)`; attempt counter reset only on a successful `welcome` (L356). Timer is `unref()`'d (L283) so it can never keep pi's event loop alive. Dead-host run above shows pi fully functional with the plugin retrying silently (`~/.pi/lvmh-agent.log`: attempts #0,#1,#2 at ~1s/~2s/~3s spacing). ### 6. Log file only, console clean Single sink `~/.pi/lvmh-agent.log` via `log()` L75–82 (async append, truncated to 500 chars/line, swallows all errors). `grep -n "console\." lvmh-agent.ts` returns nothing; e2e asserts the plugin adds nothing to pi's stdout ("e2e plugin keeps stdout clean"). ### 7. session_shutdown / unload: close, flush nothing, exit fast `session_shutdown` handler L490–519: synchronous, sends best-effort `bye {reason:"shutdown"}` only if the socket is OPEN, `close()`s it, clears the timer, sets `stopped` (blocks `emit`, `connect`, `scheduleReconnect`, and `flush`). No flush, no awaits. Smoke scenario 6: connection count → 0, no reconnect for the following 2.6s. E2E: `bye` frame observed on the wire, no reconnect after. ### 8. Daemon down at startup ⇒ pi starts and runs perfectly Proven twice: (a) smoke scenario 5 — factory loads against a closed port, handlers never throw, event loop stays responsive (100ms timer completes <500ms); (b) real pi run above — prompt answered, exit 0. ### 9. Protocol conformance details - Envelope `v/sessionId/seq/ts/type` on every frame; `hello`/`bye`/`welcome` use `seq: 0` (L339, L505). - Per-session monotonic seq survives reconnects (module-level `seqCounters`, L70); after a process restart, the counter is pushed past `welcome.lastSeq` so seqs are never reused against a warm daemon (L360–364). - Message mapping (`mapMessage` L113): `role`, deterministic `id` (`role-timestamp`), concatenated `text`, `thinking` or null, `toolCalls[{id,name,argsJson}]`, `toolCallId` for toolResults — verified by smoke "2 message_end mapping" and e2e user/assistant message_end. - `message_update` carries only the text delta (diffed against the previously sent prefix, L547–556); non-prefix text (model retry) resends full text. - Truncation to 2000 chars for `partial` and `resultPreview` (L576, L584). - `agent_end.usage` sums `input`/`output`/`cost.total` across the run's assistant messages; omitted entirely if none had usage (L595–613). - Multi-session: extension instance is re-created per session bind; on `session_start` with a new session id the old socket/timers/queues are reset (L475–488) and seq counters stay per-session (module map). ## Known non-issues (documented, not plugin bugs) - **pi print-mode exit hang**: `pi -p` with *any* extension loaded (reproduced with an empty noop extension, no lvmh involvement) sometimes does not exit after answering — a pi quirk, independent of this plugin. The plugin releases all of its resources on `session_shutdown` (proven by `bye`, socket close, and no reconnect in e2e). The e2e harness treats this as informational. - Sync flush of a large replay burst (~10k frames ≈ 1MB) takes single-digit milliseconds of JSON serialization; it cannot hang the loop but is the largest synchronous chunk the plugin can produce. ## Files | file | role | | --- | --- | | `lvmh-agent.ts` | the extension (only file pi loads) | | `README.md` | install / env / behavior | | `tsconfig.json` | strict typecheck against real pi types (`paths`, `typeRoots`) | | `pi-types.d.ts` | commented minimal fallback if pi's dist types move | | `mini-daemon.ts` | dev-only hand-rolled WS daemon for tests | | `smoke.ts` | dev-only offline test (25 checks) | | `e2e.ts` | dev-only real-pi test (13 checks) |