Files

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_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 mirroringmessage_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.
  • Inboundpromptpi.sendUserMessage(message, { deliverAs: "steer" }) (TUI-first: web prompts queue like typed-ahead input while you are mid-turn); abortpi.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.
  • Shutdownsession_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

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.