# lvmh pi plugin A [pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) extension that mirrors live session events to the lvmh daemon over WebSocket and delivers web-side prompts back into the running session. Wire contract: `PROTOCOL.md` (envelope `v: 1`) at the repo root. Single file, zero npm dependencies (Node built-in global `WebSocket` + `node:fs`). TypeScript is loaded directly by pi's extension loader. ## Install Copy `lvmh-agent.ts` into one of pi's auto-discovery locations: ```sh # global (all projects) cp plugin/lvmh-agent.ts ~/.pi/agent/extensions/lvmh-agent.ts # or project-local mkdir -p .pi/extensions cp plugin/lvmh-agent.ts .pi/extensions/lvmh-agent.ts ``` Quick test without installing: ```sh pi -e ./plugin/lvmh-agent.ts ``` ## Environment variables | var | required | meaning | | --- | --- | --- | | `LVMH_URL` | yes | Full agent websocket URL, e.g. `ws://alarm:8686/agent/ws` (`ws://` or `wss://`) | | `LVMH_TOKEN` | yes | Shared bearer secret; sent as `Authorization: Bearer …` on the WS upgrade | | `LVMH_AGENT` | no | Set to `1` by the daemon when spawning container sessions → `session.agent: true` in the hello snapshot | | `LVMH_REPO` | no | `group/project` spawn metadata → `session.repo` in the hello snapshot | If `LVMH_URL` or `LVMH_TOKEN` is unset the extension does nothing at all. ## Behavior - **Handshake** — on `session_start` the plugin connects, sends `hello` with a full session snapshot (id, name, cwd, model, provider, agent, repo, startedAt), waits for `welcome.lastSeq`, then replays every buffered persisted event with `seq > lastSeq` and continues streaming live. - **Event mirroring** — `message_start`, `message_update` (text deltas only), `message_end` (full protocol Message shape incl. `toolCalls[].argsJson`, `thinking`, `toolCallId`), `tool_execution_start/update/end` (`partial`/`resultPreview` truncated to 2000 chars), `agent_start`, `agent_end` (usage: `inputTokens`/`outputTokens`/`totalCost` summed over the run's assistant messages), `agent_settled`, and `session_info` on rename (`session_info_changed`) or model change (`model_select`). - **Seq** — monotonically increasing per session, assigned plugin-side, survives reconnects within the process. Post-restart, the counter is bumped past `welcome.lastSeq` so seq numbers are never reused. - **Inbound** — `prompt` → `pi.sendUserMessage(message, { deliverAs: "steer" })` (TUI-first: web prompts queue like typed-ahead input while you are mid-turn); `abort` → `pi.abort()` (falls back to `ctx.abort()`). Unknown frame types are ignored. - **Reconnect** — exponential backoff 1s → 30s cap (+ up to 1s jitter), forever. Daemon down at startup ⇒ pi runs perfectly, plugin retries silently. - **Bounded queues** — send queue caps at 1000 frames (drop-oldest), replay buffer at 10000 events (drop-oldest). Dropped frames are never silent: a `buffer_overflow` notice (`{dropped: n}`) is emitted right after the next successful handshake. Persisted-kind events dropped from the send queue are recovered via the replay buffer; only unpersisted `message_update` deltas can be lost. - **Shutdown** — `session_shutdown` sends `bye {reason: "shutdown"}` (best effort), closes the socket, cancels all timers. Nothing is flushed; exit is immediate. No reconnect is scheduled afterwards (pi re-instantiates the extension for the next session; seq counters are module-level and carry over). ## Bulletproofing The plugin must never crash, hang, or slow down pi: - every event handler is wrapped by `sub()` (lvmh-agent.ts): sync throws are caught, async rejections are swallowed, handlers always return `undefined`; - all WS I/O is non-blocking; WS send/construct failures are caught and treated as disconnects; - logging is append-only to `~/.pi/lvmh-agent.log` (async, best-effort, never throws); the plugin never writes to stdout/stderr; - no npm dependencies, no timers left running after `session_shutdown` (reconnect timer is `unref()`'d anyway). ## Development ```sh cd plugin npx --package typescript@5.9 --package @types/node tsc --noEmit -p . # typecheck (strict) node smoke.ts # 25 offline checks vs mini-daemon node e2e.ts # real pi vs mini-daemon (1 small LLM call) ``` `tsconfig.json` points `paths`/`typeRoots` at the installed pi package; if that moves, `pi-types.d.ts` contains a commented minimal fallback declaration. `mini-daemon.ts`, `smoke.ts`, `e2e.ts` are dev-only harnesses (hand-rolled WS server, no dependencies) and are not loaded by pi. See `selftest.md` for the full constraint-by-constraint verification.