import { randomUUID } from "node:crypto"; import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent"; import type { IntercomExtensionChannel, IntercomExtensionEvent } from "pi-intercom/extension-api.ts"; export type Role = "worker" | "supervisor"; export interface View { id: string; text: string; reason: string; through?: string; backgroundQuiet: boolean } interface Message { binding: string; role: Role; kind: "hello" | "view" | "steer" | "received"; id: string; text?: string; reason?: string; failure?: string; ready?: boolean; reply?: boolean; through?: string; backgroundQuiet?: boolean } const STATE = "pi-goals-intercom"; export class GoalIntercom { private channel?: IntercomExtensionChannel; private ctx?: ExtensionContext; private stopped = false; private registered = false; private binding = ""; private role: Role = "worker"; private ready = false; private failure?: string; private peer?: string; private peerReady = false; private peerFailure?: string; private pending = new Map(); private received = new Set(); private inbox = new Map(); private deliveryTimer?: ReturnType; private peerRetryTimer?: ReturnType; private peerRetryCount = 0; private idleChecks = 0; private delivering?: string; private compacting = false; private waiters = new Set<(error?: Error) => void>(); latestView?: View; acknowledgedEntry?: string; onView: (view: View) => void = () => {}; onSteer: (text: string) => void = () => {}; onConnectionChange: (ctx: ExtensionContext) => void = () => {}; constructor(private pi: ExtensionAPI) { pi.events.on("intercom:extension-registry-ready", () => this.register()); this.register(); pi.on("session_start", async (event, ctx) => { this.ctx = ctx; if (!this.channel) await this.loadIntercom(event, ctx); }); pi.on("message_start", async event => { if (event.message.role !== "user") return; const content = event.message.content; const text = typeof content === "string" ? content : content.filter(part => part.type === "text").map(part => part.text).join("\n"); for (const message of this.inbox.values()) { if (text !== this.deliveryText(message)) continue; this.inbox.delete(message.id); this.received.add(message.id); this.record("in", message); if (this.connected) this.publish({ binding: this.binding, role: this.role, kind: "received", id: message.id }); this.delivering = undefined; this.idleChecks = 0; this.scheduleDelivery(0); break; } }); pi.on("session_before_compact", async () => { this.compacting = true; }); // Do not append a queued supervision message into compaction. agent_settled releases it // after Pi has finished rebuilding context (including a failed compaction recovery). pi.on("session_compact", async () => { this.compacting = true; }); pi.on("session_compact_failed", async () => { this.compacting = true; }); pi.on("agent_settled", async () => this.resumeDelivery()); pi.on("session_shutdown", async () => { if (this.deliveryTimer) clearTimeout(this.deliveryTimer); this.clearPeerRetry(); this.stopped = true; this.peerReady = false; for (const wake of this.waiters) wake(); }); } configure(binding: string, role: Role, ctx: ExtensionContext, ready = role === "worker"): void { for (const wake of this.waiters) wake(new Error("Supervision readiness wait cancelled by reconfiguration.")); this.binding = binding; this.role = role; this.ctx = ctx; this.ready = ready; this.failure = undefined; this.peer = undefined; this.peerReady = false; this.peerFailure = undefined; this.pending.clear(); this.received.clear(); this.inbox.clear(); this.delivering = undefined; this.idleChecks = 0; if (this.deliveryTimer) clearTimeout(this.deliveryTimer); this.deliveryTimer = undefined; this.clearPeerRetry(); this.latestView = undefined; this.acknowledgedEntry = undefined; for (const entry of ctx.sessionManager.getBranch?.() ?? ctx.sessionManager.getEntries()) { if (entry.type !== "custom" || entry.customType !== STATE) continue; const record = entry.data as { direction: string; message: Message }; const message = record.message; if (message.binding !== binding) continue; if (record.direction === "out" && message.kind === "steer") this.pending.set(message.id, message); if (record.direction === "superseded") this.pending.delete(message.id); if (record.direction === "ack") { this.pending.delete(message.id); if (message.through) this.acknowledgedEntry = message.through; } if (record.direction === "queued") this.inbox.set(message.id, message); if (record.direction === "in") { this.received.add(message.id); this.inbox.delete(message.id); } if (message.kind === "view") this.latestView = { id: message.id, text: message.text!, reason: message.reason!, through: message.through, backgroundQuiet: message.backgroundQuiet === true }; } this.hello(); this.schedulePeerRetry(); this.scheduleDelivery(0); } // End this plan's binding without disposing the session's transport. detach(reason?: string): void { if (this.deliveryTimer) clearTimeout(this.deliveryTimer); this.deliveryTimer = undefined; this.clearPeerRetry(); this.inbox.clear(); this.delivering = undefined; this.ready = false; // Announce on the old binding; a peer failure is never echoed as our own failure. this.failure = reason; this.hello(); this.binding = ""; this.peer = undefined; this.peerReady = false; this.latestView = undefined; this.pending.clear(); for (const wake of this.waiters) wake(new Error("Supervision readiness wait cancelled: plan detached.")); if (this.ctx) this.onConnectionChange(this.ctx); } failReady(reason: string): void { this.failure = reason; this.setReady(false); } markReady(): void { this.failure = undefined; this.setReady(true); this.schedulePeerRetry(); this.resumeDelivery(); } markNotReady(): void { this.setReady(false); } private setReady(ready: boolean): void { if (this.stopped) return; this.ready = ready; this.hello(); if (this.ctx) this.onConnectionChange(this.ctx); } get readinessFailure(): string | undefined { return this.failure ?? this.peerFailure; } get ended(): boolean { return this.stopped; } get bound(): boolean { return !this.stopped && Boolean(this.binding); } get peerPresent(): boolean { return Boolean(this.bound && this.peer && this.channel?.snapshot().connected); } get connected(): boolean { return this.ready && this.peerPresent && this.peerReady; } // Startup can wait for the supervisor while the worker is still in planning/model recovery. async waitReady(timeoutMs = 300_000, { peerOnly = false } = {}): Promise { const ready = () => this.connected || (peerOnly && this.peerPresent && this.peerReady); if (this.peerFailure) throw new Error(this.peerFailure); if (ready()) return; await new Promise((resolve, reject) => { const finish = (error?: Error) => { if (!error && !ready() && !this.stopped) return; clearTimeout(timer); this.waiters.delete(finish); if (error) reject(error); else if (this.stopped) reject(new Error("Session ended while waiting for Intercom readiness.")); else resolve(); }; const timer = setTimeout(() => { this.waiters.delete(finish); reject(new Error("Supervisor did not become ready through pi-intercom; inspect its pane.")); }, timeoutMs); this.waiters.add(finish); finish(); }); } view(text: string, reason: string, through?: string, backgroundQuiet = false): View { if (!this.bound) throw new Error("No active supervision binding for a worker view."); const id = randomUUID(); const message: Message = { binding: this.binding, role: this.role, kind: "view", id, text: `${text}\n\nworker view id: ${id}`, reason, through, backgroundQuiet }; this.record("out", message); this.latestView = { id, text: message.text!, reason, through, backgroundQuiet }; if (this.connected) this.publish(message); return this.latestView; } steer(text: string): { id: string; queued: boolean } { if (!this.bound) throw new Error("Worker pairing is not active; no instruction was retained."); if (this.failure || this.peerFailure) throw new Error(this.failure ?? this.peerFailure); if (!this.connected) { const retained = [...this.pending.values()].find(message => message.text === text); if (retained) return { id: retained.id, queued: true }; for (const pending of this.pending.values()) this.record("superseded", pending); this.pending.clear(); } const message: Message = { binding: this.binding, role: this.role, kind: "steer", id: randomUUID(), text }; this.record("out", message); this.pending.set(message.id, message); if (this.connected) this.publish(message); return { id: message.id, queued: !this.connected }; } // The inbox is persisted before handoff. Receipt means Pi started the user message, not model judgment or execution. private deliveryText(message: Message): string { return message.kind === "view" ? message.text! : `[supervisor] ${message.text!}`; } resumeDelivery(): void { if (!this.ctx?.hasPendingMessages?.()) this.delivering = undefined; this.idleChecks = 0; this.scheduleDelivery(0); // Pi 0.85.1 isIdle includes compaction; check it after the success/failure hook. } private scheduleDelivery(delay: number): void { if (this.deliveryTimer) clearTimeout(this.deliveryTimer); this.deliveryTimer = undefined; if (!this.bound || !this.inbox.size) return; this.deliveryTimer = setTimeout(() => { this.deliveryTimer = undefined; this.deliverNext(); }, delay); } private deliverNext(): void { if (!this.bound || !this.ready || !this.inbox.size) return; // Accepted-but-not-yet-presented messages must not be submitted again behind a running turn. if (this.ctx?.hasPendingMessages?.()) return; if (this.compacting && !this.ctx?.isIdle?.()) { if (++this.idleChecks <= 300) this.scheduleDelivery(1000); else this.ctx?.ui.notify("Supervision message retained while Pi is busy. Use /goals reconnect when ready to retry delivery.", "warning"); return; } if (this.delivering && !this.compacting) return; this.compacting = false; const message = this.inbox.values().next().value!; this.delivering = message.id; try { if (message.kind === "view") this.onView({ id: message.id, text: message.text!, reason: message.reason!, through: message.through, backgroundQuiet: message.backgroundQuiet === true }); else this.onSteer(message.text!); } catch (error) { this.delivering = undefined; this.ctx?.ui.notify(`Supervision message retained: ${String(error)} Use /goals reconnect to retry.`, "warning"); } } private record(direction: string, message: Message): void { this.pi.appendEntry(STATE, { direction, message }); } private publish(message: Message): void { if (this.stopped) throw new Error("Intercom session ended."); if (Buffer.byteLength(JSON.stringify(message)) > 16_000) throw new Error("Supervisor message exceeds the Intercom payload limit."); if (!this.channel?.snapshot().supported) throw new Error("pi-intercom broker does not support extension channels."); this.channel.publish(message, { audience: "capable" }); } private hello(reply = false): void { if (!this.stopped && this.binding && this.channel?.snapshot().connected) this.publish({ binding: this.binding, role: this.role, kind: "hello", id: "hello", ready: this.ready, failure: this.failure, reply }); } private clearPeerRetry(): void { if (this.peerRetryTimer) clearTimeout(this.peerRetryTimer); this.peerRetryTimer = undefined; this.peerRetryCount = 0; } // Reload/reconnect races can lose the initial hello. Retry twice only while no peer exists; // normal readiness timeout and explicit recovery commands remain the authority after that. private schedulePeerRetry(): void { if (this.peerRetryTimer || this.peer || this.failure || !this.bound || !this.channel?.snapshot().connected || this.peerRetryCount >= 2) return; const delay = this.peerRetryCount++ === 0 ? 1_000 : 5_000; this.peerRetryTimer = setTimeout(() => { this.peerRetryTimer = undefined; if (this.peer || this.failure || !this.bound || !this.channel?.snapshot().connected) return; this.hello(); this.schedulePeerRetry(); }, delay); } private receive(event: IntercomExtensionEvent): void { if (this.stopped) return; if (event.type === "connection") { if (!event.connected) { if (this.peerReady) this.ctx?.ui.notify("Goal supervision disconnected from pi-intercom.", "warning"); this.peer = undefined; this.peerReady = false; this.clearPeerRetry(); } else { this.hello(); this.schedulePeerRetry(); } if (this.ctx) this.onConnectionChange(this.ctx); return; } if (event.type === "session_left" && event.sessionId === this.peer) { this.peer = undefined; this.peerReady = false; this.hello(); this.schedulePeerRetry(); this.ctx?.ui.notify("Goal supervision peer disconnected; reconnecting the existing session.", "warning"); if (this.ctx) this.onConnectionChange(this.ctx); return; } if (event.type === "session_joined") { this.hello(); this.schedulePeerRetry(); return; } if (event.type !== "message") return; const message = event.payload as Message; if (!message || message.binding !== this.binding || message.role !== (this.role === "worker" ? "supervisor" : "worker")) return; if (message.kind === "hello") { if (this.peer && this.peer !== event.fromSessionId) throw new Error("Two peers claim this supervision binding. Stop the duplicate session."); const changed = !this.peer || this.peerReady !== Boolean(message.ready) || this.peerFailure !== message.failure; this.peer = event.fromSessionId; this.clearPeerRetry(); this.peerReady = Boolean(message.ready); this.peerFailure = message.failure; if (message.failure) { this.ctx?.ui.notify(message.failure, "error"); for (const wake of this.waiters) wake(new Error(message.failure)); } // Every request gets one reply, even if only the sender forgot its peer. // Replies never elicit hellos; own-ready transitions also trigger replay here. if (!message.reply) this.hello(true); if (this.peerReady && this.ready) { if (this.role === "worker" && this.latestView) this.publish({ binding: this.binding, role: this.role, kind: "view", ...this.latestView }); for (const pending of this.pending.values()) this.publish(pending); } if (this.connected && this.inbox.size && !this.deliveryTimer) this.scheduleDelivery(0); if (changed && this.ctx) this.onConnectionChange(this.ctx); for (const wake of this.waiters) wake(); return; } if (event.fromSessionId !== this.peer || !this.ready) return; if (message.kind === "received") { this.pending.delete(message.id); const through = message.id === this.latestView?.id ? this.latestView.through : undefined; if (through) this.acknowledgedEntry = through; this.record("ack", { ...message, through }); return; } if (this.received.has(message.id)) { if (message.kind === "steer" || (message.kind === "view" && message.reason !== "started")) this.publish({ binding: this.binding, role: this.role, kind: "received", id: message.id }); return; } if (message.kind === "view" && this.role === "supervisor") { this.latestView = { id: message.id, text: message.text!, reason: message.reason!, through: message.through, backgroundQuiet: message.backgroundQuiet === true }; if (message.reason === "started") { this.record("in", message); return; } } else if (message.kind !== "steer" || this.role !== "worker") return; if (this.inbox.has(message.id)) return; if (this.inbox.size >= 64) { this.ctx?.ui.notify("Supervision inbox is full; message was not acknowledged. Use /goals reconnect after pending review finishes.", "error"); return; } this.record("queued", message); this.inbox.set(message.id, message); if (this.compacting) this.ctx?.ui.notify("Supervision message retained during compaction; delivery will retry automatically.", "info"); if (!this.delivering) this.deliverNext(); } private register(): void { if (this.stopped || this.registered) return; this.pi.events.emit("intercom:extension-register", { namespace: "pi-goals", ownerEligible: false, onReady: (channel: IntercomExtensionChannel) => { if (this.stopped) return; this.registered = true; this.channel = channel; this.hello(); this.schedulePeerRetry(); }, onEvent: (event: IntercomExtensionEvent) => { try { this.receive(event); } catch (error) { if (!this.stopped) this.ctx?.ui.notify(`Goal Intercom error: ${String(error)}`, "error"); } }, }); } private async loadIntercom(event: SessionStartEvent, ctx: ExtensionContext): Promise { if (this.pi.getAllTools().some(tool => tool.name === "intercom")) throw new Error("Installed pi-intercom has no extension channel; update it before continuing."); const starts: Array<(event: SessionStartEvent, ctx: ExtensionContext) => unknown> = []; const api = { ...this.pi, on: (name: string, handler: (...args: any[]) => any) => { if (name === "session_start") starts.push(handler); else this.pi.on(name as Parameters[0], handler); } } as ExtensionAPI; const { default: intercom } = await import("pi-intercom"); intercom(api); for (const start of starts) await start(event, ctx); this.register(); } }