lvmh pi plugin
A pi 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:
# 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:
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_startthe plugin connects, sendshellowith a full session snapshot (id, name, cwd, model, provider, agent, repo, startedAt), waits forwelcome.lastSeq, then replays every buffered persisted event withseq > lastSeqand 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/resultPreviewtruncated to 2000 chars),agent_start,agent_end(usage:inputTokens/outputTokens/totalCostsummed over the run's assistant messages),agent_settled, andsession_infoon 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.lastSeqso 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 toctx.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_overflownotice ({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 unpersistedmessage_updatedeltas can be lost. - Shutdown —
session_shutdownsendsbye {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 returnundefined; - 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 isunref()'d anyway).
Development
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.