import { access, readFile } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry, } from "@earendil-works/pi-coding-agent"; import { CommandContextFreshSessionCapability, FreshSessionDispatcher, type FreshSessionProjectionEvidence, type FreshSessionReplacementPort, } from "../integration/fresh-session-dispatcher.js"; import { ConfirmedBindOnboarding, parseRosterInput, type ConfirmedBindPreview, } from "../integration/confirmed-bind-onboarding.js"; import { adjacentFleetRecord, FleetListModel, FleetDetailController, MutableFleetProjectionSource, handleFleetInput, handleFleetDetailOverlayInput, projectVisibleFleet, renderFleetDetail, renderFleetDetailOverlay, type FleetAgentRecord, } from "../integration/fleet-view.js"; import { OVERLAY_OPTIONS } from "../integration/tui-overlay.js"; import { AmqCliAdapter } from "../transport/amq-cli-adapter.js"; import { AmqOperations, safeProjection } from "../transport/amq-operations.js"; import { BindingResolver, PI_AMQ_BINDING_ENTRY_TYPE, type BindingProjectionV1, } from "../transport/binding-resolver.js"; import { BridgeJournal, type DiskProjectionEvidence } from "../transport/bridge-journal.js"; import { InboxPump, LeaseBackedInboxAuthority, type SessionProjectionLookup } from "../transport/inbox-pump.js"; import { PresenceProjector } from "../transport/presence-projector.js"; import { PI_AMQ_CUSTOM_TYPE, planPriorityDeliveries, PriorityDeliveryScheduler, type DeliveryRequest, type DeliveryResult, type PiAmqBatchDetailsV1, type PiAmqEnvelopeDetails, } from "../transport/priority-delivery-scheduler.js"; import type { AmqMessage } from "../transport/amq-message.js"; import { LocalProcessIdentityInspector, ReceiverLeaseManager } from "../transport/receiver-lease.js"; import { InboxStatusStore, projectInboxStatus, receiverLeaseRuntimeDisposition, } from "../transport/runtime-status.js"; import { ALL_AMQ_TOOLS, NON_CONSUMING_AMQ_TOOLS, registerAmqTools } from "./tools.js"; import { InstalledPackageGuide, registerGuideTool } from "./guide.js"; import { PiWorkerRuntime } from "./worker-runtime.js"; import { registerWorkerTools } from "./worker-tools.js"; import { parseCleanupAge } from "../workflow/run-cleanup.js"; import { appendAmqMessagingDoctrine, appendWorkerTeamDoctrine, buildBeforeAgentGuidance, buildPendingProjection, } from "./guidance.js"; const PENDING_PROJECTION_TYPE = "pi-amq-pending/v1"; const WIDGET_KEY = "pi-amq-fleet"; interface ReceiverRuntime { readonly lease: ReceiverLeaseManager; readonly generation: string; readonly journal: BridgeJournal; readonly pump: InboxPump; readonly presence: PresenceProjector; abortController: AbortController; pumpTask: Promise; } const FRESH_SESSION_COMMAND = "amq-new-task"; export class PiAmqRuntime { readonly #pi: ExtensionAPI; readonly #workers: PiWorkerRuntime; readonly #freshSessionPort: PiFreshSessionReplacementPort; readonly #guide = new InstalledPackageGuide(); readonly #status = new InboxStatusStore(); readonly #fleet = new MutableFleetProjectionSource([]); #mainFleet: FleetAgentRecord | undefined; #workerFleet: readonly FleetAgentRecord[] = []; #adapter: AmqCliAdapter | undefined; #resolver: BindingResolver | undefined; #binding: BindingProjectionV1 | undefined; #operations: AmqOperations | undefined; #receiver: ReceiverRuntime | undefined; #ctx: ExtensionContext | undefined; #unsubscribeStatus: (() => void) | undefined; #unsubscribeTerminal: (() => void) | undefined; #fleetModel: FleetListModel | undefined; #fleetDetailOwner: symbol | undefined; #started = false; #paused = false; constructor(pi: ExtensionAPI) { this.#pi = pi; this.#freshSessionPort = new PiFreshSessionReplacementPort(pi); this.#workers = new PiWorkerRuntime((records) => { this.#workerFleet = records; this.#commitFleet(); }, () => this.#pi.getFlag("worker-unlimited"), async (projection) => { const operations = this.#operations; const recipients = (process.env.PI_AMQ_LIFECYCLE_TO ?? "") .split(",") .map((item) => item.trim()) .filter(Boolean); if (!operations || recipients.length === 0) return; const result = await operations.send({ to: recipients, body: projection.body, kind: "status", labels: projection.labels, context: projection.context, }); if (!result.ok) { this.#status.update({ lastError: `Worker lifecycle projection failed: ${result.error.message}` }); } }, (notice) => { this.#ctx?.ui.notify(notice.message, notice.level); }); } register(): void { this.#registerFlags(); this.#registerRenderers(); registerAmqTools(this.#pi, { operations: () => this.#operations, canConsume: () => this.#receiver !== undefined && !this.#paused, canSetPresence: () => this.#receiver !== undefined && !this.#paused, }); registerWorkerTools(this.#pi, this.#workers); registerGuideTool(this.#pi, this.#guide); this.#registerCommands(); this.#pi.on("session_start", async (_event, ctx) => { this.#ctx = ctx; await this.#workers.start(ctx); await this.start(ctx); }); this.#pi.on("session_shutdown", async () => { await this.#workers.shutdown(); await this.shutdown(); }); this.#pi.on("agent_start", async (_event, ctx) => { this.#updateMainFleet(ctx, "busy"); await this.#receiver?.presence.busy(ctx.signal); }); this.#pi.on("agent_settled", async (_event, ctx) => { this.#updateMainFleet(ctx, "idle"); await this.#receiver?.presence.idle(ctx.signal); }); this.#pi.on("model_select", async (event, ctx) => { await this.#workers.refreshGuidance(ctx); this.#updateMainFleet(ctx, ctx.isIdle() ? "idle" : "busy", event.model.id); await this.#receiver?.presence.modelNote(event.model.id, ctx.signal); }); this.#pi.on("session_compact", async () => { const records = await this.#receiver?.journal.list() ?? []; this.#pi.appendEntry( PENDING_PROJECTION_TYPE, buildPendingProjection( records.filter((record) => record.phase === "pending").map((record) => record.id), this.#status.snapshot(), ), ); }); this.#pi.on("before_agent_start", (event, ctx) => ({ systemPrompt: this.#workers.appendGuidance( appendWorkerTeamDoctrine(appendAmqMessagingDoctrine(event.systemPrompt)), ), message: { customType: "pi-amq-guidance", display: false, content: buildBeforeAgentGuidance({ ...(this.#binding ? { identity: { handle: this.#binding.handle, project: this.#binding.project, session: this.#binding.session, mode: this.#receiver ? "receiving" : "send-only", }, } : {}), envelopes: collectEnvelopeDetails(ctx), status: this.#status.snapshot(), }), }, })); this.#pi.on("input", (_event, ctx) => { this.#ctx = ctx; }); } async start(ctx: ExtensionContext): Promise { if (this.#started) await this.shutdown(); this.#started = true; this.#ctx = ctx; let binaryPath: string; try { binaryPath = await resolveAmqBinary(process.env.PI_AMQ_BINARY); this.#adapter = new AmqCliAdapter({ binaryPath }); } catch (error) { this.#binding = undefined; this.#operations = undefined; this.#status.update({ receiver: "dormant", watch: "disabled", receiverGeneration: null, lastError: `AMQ binary configuration failed: ${toError(error).message}; /amq bind`, }); this.#activateTools("dormant"); this.#installSurfaces(ctx); this.#updateMainFleet(ctx, ctx.isIdle() ? "idle" : "busy"); return; } this.#resolver = new BindingResolver(this.#adapter); const persisted = latestBinding(ctx); const resolution = await this.#resolver.resolve({ flags: this.#flags(), environment: process.env, ...(persisted ? { persistedBinding: persisted } : {}), }); if (resolution.kind === "dormant") { this.#binding = undefined; this.#operations = undefined; this.#status.update({ receiver: "dormant", watch: "disabled", receiverGeneration: null, lastError: await this.#bindingFailureDiagnostic(resolution.diagnostic), }); this.#activateTools("dormant"); this.#installSurfaces(ctx); this.#updateMainFleet(ctx, ctx.isIdle() ? "idle" : "busy"); return; } await this.#activateBound(ctx, resolution.binding); } async shutdown(): Promise { if (!this.#started) return; this.#started = false; this.#paused = false; const receiver = this.#receiver; this.#receiver = undefined; if (receiver) { receiver.abortController.abort(new Error("pi-amq session shutdown")); await receiver.pumpTask.catch((error) => { this.#status.update({ lastError: `Inbox pump shutdown failed: ${toError(error).message}` }); }); await receiver.presence.offline(); try { const release = await receiver.lease.release(receiver.generation); if (release.kind === "refused") this.#status.update({ lastError: release.diagnostic }); } catch (error) { this.#status.update({ lastError: `Receiver lease release failed: ${toError(error).message}` }); } } this.#unsubscribeStatus?.(); this.#unsubscribeStatus = undefined; this.#unsubscribeTerminal?.(); this.#unsubscribeTerminal = undefined; this.#fleetModel?.dispose(); this.#fleetModel = undefined; this.#ctx?.ui.setStatus("pi-amq", undefined); this.#ctx?.ui.setWidget(WIDGET_KEY, undefined); this.#status.update({ receiver: "dormant", watch: "stopped", receiverGeneration: null }); } async #activateBound(ctx: ExtensionContext, binding: BindingProjectionV1): Promise { if (!this.#adapter || !this.#resolver) throw new Error("AMQ runtime is not initialized"); this.#binding = binding; this.#pi.appendEntry(PI_AMQ_BINDING_ENTRY_TYPE, binding); this.#operations = new AmqOperations({ adapter: this.#adapter, binding: { root: binding.root, handle: binding.handle, project: binding.project, session: binding.session, }, replyRouteLookup: replyRoutes(ctx), }); this.#installSurfaces(ctx); if (ctx.mode === "print" || ctx.mode === "json") { this.#status.update({ receiver: "send-only", watch: "disabled", receiverGeneration: null, lastError: null, }); this.#activateTools("send-only"); this.#updateMainFleet(ctx, "idle"); return; } const sessionFile = ctx.sessionManager.getSessionFile(); if (!sessionFile || !isAbsolute(sessionFile)) { this.#status.update({ receiver: "send-only", watch: "disabled", receiverGeneration: null, lastError: "Pi session file is not disk-visible; receiver activation is deferred", }); this.#activateTools("send-only"); this.#updateMainFleet(ctx, "idle"); return; } const namespaceDirectory = join( binding.root, "agents", binding.handle, "extensions", "pi-amq-extension", ); const lease = new ReceiverLeaseManager({ namespaceDirectory, rootId: binding.rootId, inspector: new LocalProcessIdentityInspector(), }); let acquired; try { acquired = await lease.acquire({ piSessionId: ctx.sessionManager.getSessionId(), piSessionFile: sessionFile, }); } catch (error) { this.#status.update({ receiver: "send-only", watch: "disabled", receiverGeneration: null, lastError: `Receiver lease activation failed: ${toError(error).message}`, }); this.#activateTools("send-only"); this.#updateMainFleet(ctx, "idle"); return; } const leaseDisposition = receiverLeaseRuntimeDisposition(acquired); if (leaseDisposition.mode === "send-only") { this.#status.update({ receiver: "send-only", watch: "disabled", receiverGeneration: null, lastError: leaseDisposition.diagnostic, }); this.#activateTools("send-only"); this.#updateMainFleet(ctx, "idle"); return; } if (acquired.kind !== "acquired") throw new Error("Receiver lease disposition disagrees with acquisition result"); const journal = new BridgeJournal({ namespaceDirectory, leaseManager: lease }); const lookup = new PiSessionProjectionLookup(ctx); const sink = new PiDeliverySink(this.#pi, lookup); const scheduler = new PriorityDeliveryScheduler({ sink }); const authority = new LeaseBackedInboxAuthority({ lease, generation: acquired.owner.generation, verifyBinding: async () => { const current = await this.#resolver!.resolve({ flags: this.#flags(), environment: process.env, persistedBinding: binding, }); return current.kind === "bound" && current.binding.rootId === binding.rootId && current.binding.handle === binding.handle ? { kind: "valid" } : { kind: "invalid", diagnostic: current.kind === "dormant" ? current.diagnostic : "AMQ binding authority changed" }; }, }); const fresh = new FreshSessionDispatcher({ capability: new CommandContextFreshSessionCapability(this.#freshSessionPort), policy: { trustedProject: ctx.isProjectTrusted(), allowlisted: (sender, project) => freshAllowlist().has(`${sender}@${project}`), ...(ctx.hasUI ? { confirm: async (message) => ctx.ui.confirm( "AMQ fresh session", `${message.header.from} [${message.header.fromProject || "unknown project"}] requests replacing the current session: ${message.header.subject || "(no subject)"}`, ), } : {}), canReplaceSession: async () => this.#workers.canReplaceSession(), notifyOnce: (message) => ctx.ui.notify(message, "warning"), }, }); const pump = new InboxPump({ adapter: this.#adapter, binding: { root: binding.root, rootId: binding.rootId, handle: binding.handle, }, receiverGeneration: acquired.owner.generation, targetSessionId: ctx.sessionManager.getSessionId(), authority, journal, scheduler, status: this.#status, projectionLookup: lookup, freshSessionDispatcher: fresh, }); this.#operations = new AmqOperations({ adapter: this.#adapter, binding: { root: binding.root, handle: binding.handle, project: binding.project, session: binding.session, }, claimPort: pump, replyRouteLookup: replyRoutes(ctx), }); const presence = new PresenceProjector({ writer: { setStatus: async (status, note, signal) => { const result = await this.#operations!.setStatus(status, note, signal); return result.ok ? { ok: true } : { ok: false, error: result.error }; }, }, }); const abortController = new AbortController(); const receiver: ReceiverRuntime = { lease, generation: acquired.owner.generation, journal, pump, presence, abortController, pumpTask: Promise.resolve(), }; this.#paused = false; receiver.pumpTask = pump.run(abortController.signal).catch((error) => { this.#status.update({ lastError: `Inbox pump stopped: ${toError(error).message}` }); }); this.#receiver = receiver; this.#status.update({ receiver: "receiving", receiverGeneration: acquired.owner.generation, lastError: null, }); this.#activateTools("receiving"); this.#updateMainFleet(ctx, "idle"); await presence.idle(); } #registerFlags(): void { this.#pi.registerFlag("amq-handle", { type: "string", description: "Explicit AMQ handle" }); this.#pi.registerFlag("amq-root", { type: "string", description: "Explicit physical AMQ root" }); this.#pi.registerFlag("amq-session", { type: "string", description: "Explicit AMQ session" }); this.#pi.registerFlag("amq-mode", { type: "string", description: "Opaque AMQ mode modifier" }); this.#pi.registerFlag("worker-unlimited", { type: "string", description: "Explicit comma-separated unlimited worker-run budget dimensions (or all)", }); } async #bindingFailureDiagnostic(primary: string): Promise { if (!this.#adapter) return `${primary}; /amq bind`; const root = stringFlag(this.#pi.getFlag("amq-root")) ?? process.env.AM_ROOT; const handle = stringFlag(this.#pi.getFlag("amq-handle")) ?? process.env.AM_ME; if (!root || !handle || !isAbsolute(root)) return `${primary}; /amq bind`; return diagnoseBindingFailure(this.#adapter, primary, root, handle); } #registerRenderers(): void { this.#pi.registerMessageRenderer( PI_AMQ_CUSTOM_TYPE, (message, { expanded }, theme) => { const count = message.details?.messageIds.length ?? 0; const header = theme.fg("accent", `[AMQ ${message.details?.priority ?? "mail"} · ${count}]`); const raw = typeof message.content === "string" ? message.content : "[non-text AMQ message]"; const text = expanded ? raw : raw.split("\n").slice(0, 12).join("\n"); return { render: (width: number) => `${header}\n${text}`.split("\n").map((line) => bounded(line, Math.max(1, width))), invalidate: () => undefined, }; }, ); } #registerCommands(): void { this.#pi.registerCommand("amq", { description: "AMQ status, inbox, bind, pause, resume, ingest, takeover, dlq, trace, or doctor", handler: async (args, ctx) => this.#handleCommand(args, ctx), }); this.#pi.registerCommand("pi-amq-guide", { description: "Read installed pi-amq guidance by topic", handler: async (args, ctx) => { try { this.#show(ctx, await this.#guide.read(args)); } catch (error) { this.#show(ctx, { error: { category: "guide", message: toError(error).message } }); } }, }); this.#pi.registerCommand("pi-amq-cleanup", { description: "Dry-run or explicitly delete old terminal workflow runs", handler: async (args, ctx) => { try { const params = parseCleanupCommand(args); this.#show(ctx, await this.#workers.cleanupRuns(params, ctx.signal, ctx)); } catch (error) { this.#show(ctx, { error: { category: "workflow-cleanup", message: toError(error).message } }); } }, }); this.#pi.registerCommand("pi-amq-doctor", { description: "Read-only workflow ledger, process, lease, session, and disk diagnostics", handler: async (_args, ctx) => { try { this.#show(ctx, await this.#workers.doctorWorkflow(ctx.signal, ctx)); } catch (error) { this.#show(ctx, { error: { category: "workflow-doctor", message: toError(error).message } }); } }, }); this.#pi.registerCommand(FRESH_SESSION_COMMAND, { description: "Internal: complete a fresh-session AMQ dispatch (invoked via expandPromptTemplates, not by hand)", handler: async (args, ctx) => this.#freshSessionPort.handleCommand(args, ctx), }); } async #handleCommand(raw: string, ctx: ExtensionCommandContext): Promise { const [command = "status", ...rest] = raw.trim().split(/\s+/).filter(Boolean); switch (command) { case "status": return this.#show(ctx, { binding: this.#binding ?? null, mode: ctx.mode, status: this.#status.snapshot(), presence: this.#receiver?.presence.snapshot() ?? null, lease: this.#receiver ? await this.#receiver.lease.inspectLease() : null, }); case "inbox": { if (!this.#operations) return this.#show(ctx, "pi-amq is dormant; use /amq bind"); const [fresh, current, journal] = await Promise.all([ this.#operations.inbox({ box: "new", limit: 20 }, ctx.signal), this.#operations.inbox({ box: "cur", limit: 20 }, ctx.signal), this.#receiver?.journal.list() ?? [], ]); return this.#show(ctx, { new: fresh, cur: current, bridge: journal }); } case "bind": return this.#bind(ctx); case "pause": return this.#pause(ctx); case "resume": return this.#resume(ctx); case "ingest": { if (!this.#receiver) return this.#show(ctx, "No owning receiver; /amq ingest is unavailable"); if (this.#paused) return this.#show(ctx, "AMQ receiver is paused; resume before ingesting staged mail"); const count = await this.#receiver.pump.ingestStaged(ctx.signal); return this.#show(ctx, `Promoted ${count} staged envelope(s)`); } case "takeover": return this.#takeover(ctx); case "dlq": { if (!this.#operations) return this.#show(ctx, "pi-amq is dormant; use /amq bind"); return this.#show(ctx, await this.#operations.dlqList(20, ctx.signal)); } case "trace": { if (!this.#operations) return this.#show(ctx, "pi-amq is dormant; use /amq bind"); if (!rest[0]) return this.#show(ctx, "Usage: /amq trace "); return this.#show(ctx, await this.#operations.trace(rest[0], ctx.signal)); } case "doctor": { if (!this.#operations) return this.#show(ctx, "pi-amq is dormant; use /amq bind"); const result = await this.#operations.doctor(ctx.signal); return this.#showDoctor(ctx, result); } default: return this.#show(ctx, "Usage: /amq status|inbox|bind|pause|resume|ingest|takeover|dlq|trace |doctor"); } } async #bind(ctx: ExtensionCommandContext): Promise { if (!this.#resolver || !this.#adapter) return this.#show(ctx, "AMQ binary is not initialized; reload the session"); const resolution = await this.#resolver.resolve({ flags: this.#flags(), environment: process.env }); if (resolution.kind === "bound") { if (!ctx.hasUI) return this.#show(ctx, `Binding requires confirmation. Candidate: ${JSON.stringify(resolution.binding)}`); const confirmed = await ctx.ui.confirm( "Bind pi-amq", `Bind ${resolution.binding.handle} to ${resolution.binding.root} (${resolution.binding.project}/${resolution.binding.session || "base"})?`, ); if (!confirmed) return this.#show(ctx, "AMQ bind cancelled; no state changed"); return this.#finishConfirmedBind(ctx, resolution.binding); } const onboarding = new ConfirmedBindOnboarding(this.#adapter, this.#resolver); let preview: ConfirmedBindPreview; try { preview = await this.#onboardingPreview(onboarding, ctx); } catch (error) { return this.#show(ctx, `${resolution.diagnostic}; onboarding refused: ${toError(error).message}`); } if (!ctx.hasUI) { return this.#show( ctx, `Binding requires TUI confirmation; pi-amq remains dormant. Preview: ${renderBindPreview(preview)}`, ); } const confirmed = await ctx.ui.confirm("Provision and bind pi-amq", renderBindPreview(preview)); const result = await onboarding.confirm(preview, confirmed, process.env); if (result.kind !== "bound") return this.#show(ctx, result.diagnostic); return this.#finishConfirmedBind(ctx, result.binding); } async #onboardingPreview( onboarding: ConfirmedBindOnboarding, ctx: ExtensionCommandContext, ): Promise { const defaults = this.#flags(); let root = defaults.root ?? join(ctx.cwd, ".agent-mail"); let handle = defaults.handle ?? process.env.AM_ME ?? "pi-main"; let agents: readonly string[] = parseRosterInput(`user,${handle}`, handle); if (ctx.hasUI) { const selectedRoot = await ctx.ui.input("AMQ physical root", root); if (selectedRoot === undefined) throw new Error("AMQ bind cancelled before root selection; no state changed"); root = selectedRoot; const selectedHandle = await ctx.ui.input("AMQ handle", handle); if (selectedHandle === undefined) throw new Error("AMQ bind cancelled before handle selection; no state changed"); handle = selectedHandle; const selectedAgents = await ctx.ui.input( "Complete AMQ roster (comma-separated, including user)", `user,${handle}`, ); if (selectedAgents === undefined) throw new Error("AMQ bind cancelled before roster selection; no state changed"); agents = parseRosterInput(selectedAgents, handle); } return onboarding.preview({ projectDirectory: ctx.cwd, root, handle, agents, environment: process.env, }); } async #finishConfirmedBind(ctx: ExtensionCommandContext, binding: BindingProjectionV1): Promise { await this.shutdown(); this.#started = true; this.#ctx = ctx; this.#adapter = new AmqCliAdapter({ binaryPath: await resolveAmqBinary(process.env.PI_AMQ_BINARY) }); this.#resolver = new BindingResolver(this.#adapter); await this.#activateBound(ctx, binding); this.#show(ctx, `Bound and verified ${binding.handle}`); } async #pause(ctx: ExtensionCommandContext): Promise { const receiver = this.#receiver; if (!receiver) return this.#show(ctx, "No owning receiver to pause"); this.#paused = true; this.#activateTools("send-only"); receiver.abortController.abort(new Error("pi-amq paused")); await receiver.pumpTask; await receiver.presence.paused(); this.#status.update({ watch: "stopped" }); this.#updateMainFleet(ctx, "paused"); this.#show(ctx, "AMQ receiver paused; lease retained"); } async #resume(ctx: ExtensionCommandContext): Promise { const receiver = this.#receiver; if (!receiver) return this.#show(ctx, "No owning receiver to resume"); if (!receiver.abortController.signal.aborted) return this.#show(ctx, "AMQ receiver is already running"); if (!await this.#bindingIsCurrent()) { return this.#show(ctx, "AMQ binding revalidation failed; resume refused"); } const authority = await receiver.lease.withCurrentLease(receiver.generation, async () => true).catch(() => false); if (!authority) return this.#show(ctx, "Receiver lease revalidation failed; resume refused"); this.#paused = false; this.#activateTools("receiving"); receiver.abortController = new AbortController(); receiver.pumpTask = receiver.pump.run(receiver.abortController.signal).catch((error) => { this.#status.update({ lastError: `Inbox pump stopped: ${toError(error).message}` }); }); await receiver.presence.idle(); this.#updateMainFleet(ctx, "idle"); this.#show(ctx, "AMQ receiver resumed after lease revalidation"); } async #bindingIsCurrent(): Promise { if (!this.#resolver || !this.#binding) return false; const resolution = await this.#resolver.resolve({ flags: this.#flags(), environment: process.env, persistedBinding: this.#binding, }); return resolution.kind === "bound" && resolution.binding.rootId === this.#binding.rootId && resolution.binding.handle === this.#binding.handle; } async #takeover(ctx: ExtensionCommandContext): Promise { if (this.#receiver) return this.#show(ctx, "This session already owns the receiver lease"); if (!this.#binding) return this.#show(ctx, "pi-amq is dormant; bind first"); if (!ctx.hasUI) return this.#show(ctx, "Takeover requires UI confirmation and never forces live or unverifiable ownership"); if (!await ctx.ui.confirm("AMQ takeover", "Re-inspect and quarantine only a proven-stale receiver generation?")) { return this.#show(ctx, "AMQ takeover cancelled"); } await this.start(ctx); this.#show(ctx, this.#receiver ? "Receiver acquired through the ordinary stale-proof path" : this.#status.snapshot().lastError ?? "Takeover refused"); } #installSurfaces(ctx: ExtensionContext): void { this.#unsubscribeStatus?.(); this.#unsubscribeStatus = this.#status.subscribe((status) => { const projected = projectInboxStatus(ctx.mode, status); ctx.ui.setStatus("pi-amq", projected.surface === "tui" ? projected.text : projected.surface === "rpc" ? JSON.stringify(projected.value) : undefined); }); const initial = projectInboxStatus(ctx.mode, this.#status.snapshot()); ctx.ui.setStatus("pi-amq", initial.surface === "tui" ? initial.text : initial.surface === "rpc" ? JSON.stringify(initial.value) : undefined); this.#unsubscribeTerminal?.(); this.#fleetModel?.dispose(); if (ctx.mode === "tui") { ctx.ui.setWidget(WIDGET_KEY, (tui, theme) => { const model = new FleetListModel(this.#fleet, () => tui.requestRender(), { style: { accent: (value) => theme.fg("accent", value), muted: (value) => theme.fg("muted", value), dim: (value) => theme.fg("dim", value), success: (value) => theme.fg("success", value), warning: (value) => theme.fg("warning", value), error: (value) => theme.fg("error", value), bold: (value) => theme.bold(value), }, }); this.#fleetModel = model; return { render: (width: number) => model.render(width), invalidate: () => undefined, dispose: () => model.dispose(), }; }, { placement: "belowEditor" }); this.#unsubscribeTerminal = ctx.ui.onTerminalInput((data) => { const list = this.#fleetModel; if (!list) return undefined; const result = handleFleetInput({ data, editorText: ctx.ui.getEditorText(), list, detailOpen: this.#fleetDetailOwner !== undefined, }); if (!result.consume) return undefined; if (result.action === "detail" && list.selected) void this.#openFleetDetail(ctx, list.selected); return { consume: true }; }); } else if (ctx.mode === "rpc") { ctx.ui.setWidget(WIDGET_KEY, this.#fleet.snapshot().flatMap(renderFleetRow), { placement: "belowEditor" }); } else { ctx.ui.setWidget(WIDGET_KEY, undefined); } } async #openFleetDetail(ctx: ExtensionContext, record: FleetAgentRecord): Promise { if (this.#fleetDetailOwner !== undefined) return; const owner = Symbol("fleet-detail"); this.#fleetDetailOwner = owner; try { await ctx.ui.custom((tui, _theme, _keys, done) => { const unsubscribe = this.#fleet.subscribe(() => tui.requestRender()); const controller = new FleetDetailController({ record, resolve: (recordId) => this.#fleet.snapshot().find((candidate) => candidate.id === recordId), move: (recordId, delta) => adjacentFleetRecord(this.#fleet.snapshot(), recordId, delta), onSelectionChange: (selected) => this.#fleetModel?.select(selected.id), steer: async (workerId, generation, message) => { try { await this.#workers.steerFromFleet(workerId, generation, message); } finally { tui.requestRender(); } }, cancel: async (workerId, generation) => { try { await this.#workers.cancelFromFleet(workerId, generation); } finally { tui.requestRender(); } }, }); return { render: (width: number) => renderFleetDetailOverlay(controller, width), invalidate: () => undefined, handleInput: (data: string) => { handleFleetDetailOverlayInput({ data, controller, close: () => { if (this.#fleetDetailOwner === owner) this.#fleetDetailOwner = undefined; done(undefined); }, invalidate: () => tui.requestRender(), }); }, dispose: unsubscribe, }; }, { overlay: true, overlayOptions: OVERLAY_OPTIONS, onHandle: (handle) => handle.focus(), }); } finally { // A close and immediate reopen can overlap at the Promise boundary. Never let the old // overlay's cleanup release the newer overlay's input ownership. if (this.#fleetDetailOwner === owner) this.#fleetDetailOwner = undefined; } } #updateMainFleet(ctx: ExtensionContext, state: FleetAgentRecord["state"], selectedModel?: string): void { const previous = this.#mainFleet; this.#mainFleet = { id: ctx.sessionManager.getSessionId(), handle: this.#binding?.handle ?? "main", role: "main", model: selectedModel ?? ctx.model?.id ?? previous?.model ?? "unselected", state, spend: null, lastActivity: new Date().toISOString(), mode: "main", }; this.#commitFleet(); } #commitFleet(): void { this.#fleet.update(projectVisibleFleet(this.#mainFleet, this.#workerFleet)); if (this.#ctx?.mode === "rpc") { const rows = this.#fleet.snapshot().flatMap(renderFleetRow); this.#ctx.ui.setWidget( WIDGET_KEY, rows.length > 0 ? rows : undefined, { placement: "belowEditor" }, ); } } #activateTools(mode: "dormant" | "send-only" | "receiving"): void { const existing = this.#pi.getActiveTools().filter((name) => !ALL_AMQ_TOOLS.includes(name as typeof ALL_AMQ_TOOLS[number])); const enabled = mode === "dormant" ? [] : mode === "send-only" ? ["amq_send", "amq_reply", ...NON_CONSUMING_AMQ_TOOLS] : [...ALL_AMQ_TOOLS]; this.#pi.setActiveTools([...existing, ...enabled]); } #flags(): { handle?: string; root?: string; session?: string; mode?: string } { return { ...flag(this.#pi.getFlag("amq-handle"), "handle"), ...flag(this.#pi.getFlag("amq-root"), "root"), ...flag(this.#pi.getFlag("amq-session"), "session"), ...flag(this.#pi.getFlag("amq-mode"), "mode"), }; } #show(ctx: ExtensionContext, value: unknown): void { const text = typeof value === "string" ? value : JSON.stringify(value, null, 2); ctx.ui.notify(bounded(text, 12 * 1024), text.includes("error") ? "warning" : "info"); } #showDoctor(ctx: ExtensionContext, value: unknown): void { if (ctx.mode === "print" || ctx.mode === "json") { return this.#show(ctx, "Doctor UI cards are unavailable in this mode; run `amq doctor --root --ops --json` manually"); } if (typeof value === "object" && value && "ok" in value && "value" in value && (value as { ok: boolean }).ok) { const payload = value.value; const findings = Array.isArray(payload) ? payload : payload && typeof payload === "object" && "checks" in payload && Array.isArray(payload.checks) ? payload.checks : undefined; if (findings) { for (const finding of findings.slice(0, 50)) { const card: Record = finding && typeof finding === "object" ? finding as Record : { message: finding }; const status = typeof card.status === "string" ? card.status : "info"; const name = typeof card.name === "string" ? card.name : "AMQ doctor"; const message = typeof card.message === "string" ? card.message : JSON.stringify(card); ctx.ui.notify(`${name}: ${message}`, status === "error" ? "error" : status === "warn" ? "warning" : "info"); } return; } } this.#show(ctx, value); } } export function parseCleanupCommand(raw: string): { readonly older_than_ms: number; readonly dry_run: boolean; readonly yes: boolean; } { const args = raw.trim().split(/\s+/).filter(Boolean); let age: number | undefined; let dryRun = true; let yes = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]!; switch (arg) { case "--older-than": { const value = args[index + 1]; if (!value) throw new TypeError("--older-than requires a duration"); age = parseCleanupAge(value); index += 1; break; } case "--dry-run": dryRun = true; break; case "--delete": dryRun = false; break; case "--yes": yes = true; break; default: throw new TypeError(`Unknown cleanup option: ${arg}`); } } if (age === undefined) { throw new TypeError("Usage: /pi-amq-cleanup --older-than [--delete] [--yes]"); } return { older_than_ms: age, dry_run: dryRun, yes }; } export async function diagnoseBindingFailure( adapter: Pick, primary: string, root: string, handle: string, ): Promise { const diagnostic = await adapter.runJson([ "route", "explain", "--root", root, "--me", handle, "--to", handle, ]); const projected = diagnostic.ok ? safeProjection(diagnostic.json) : { category: diagnostic.category, message: diagnostic.stderr.trim() || diagnostic.jsonError?.message || diagnostic.processError?.message, }; return `${primary}; /amq bind; route=${JSON.stringify(projected)}`; } class PiSessionProjectionLookup implements SessionProjectionLookup { readonly #ctx: ExtensionContext; constructor(ctx: ExtensionContext) { this.#ctx = ctx; } async find(messageId: string, targetSessionId: string): Promise { if (this.#ctx.sessionManager.getSessionId() !== targetSessionId) return undefined; const sessionFile = this.#ctx.sessionManager.getSessionFile(); if (!sessionFile) return undefined; let contents: string; try { contents = await readFile(sessionFile, "utf8"); } catch { return undefined; } const found = contents.split("\n").some((line) => diskLineContainsMessage(line, messageId)); return found ? { messageId, sessionId: targetSessionId, diskVisible: true } : undefined; } } export function diskLineContainsMessage(line: string, messageId: string): boolean { if (line.length === 0) return false; try { const entry = JSON.parse(line) as Record; if (entry.type !== "custom_message" || entry.customType !== PI_AMQ_CUSTOM_TYPE) return false; const details = entry.details; if (!details || typeof details !== "object" || Array.isArray(details)) return false; const messageIds = (details as Record).messageIds; return Array.isArray(messageIds) && messageIds.includes(messageId); } catch { return false; } } export class PiDeliverySink { readonly #pi: ExtensionAPI; readonly #lookup: SessionProjectionLookup; constructor(pi: ExtensionAPI, lookup: SessionProjectionLookup) { this.#pi = pi; this.#lookup = lookup; } async deliver(request: DeliveryRequest): Promise { try { this.#pi.sendMessage(request.message, request.options); const projections = await Promise.all(request.message.details.messageIds.map(async (messageId) => ( await this.#lookup.find(messageId, request.targetSessionId, request.signal) ?? { messageId, sessionId: request.targetSessionId, diskVisible: false } ))); return { kind: "accepted", projections }; } catch (error) { return { kind: "indeterminate", error: toError(error) }; } } } const FRESH_SESSION_PROVENANCE_TYPE = "pi-amq-fresh-session-provenance/v1"; const DEFAULT_FRESH_SESSION_DISPATCH_TIMEOUT_MS = 30_000; interface FreshSessionHandoff { readonly message: AmqMessage; readonly resolve: (result: { readonly cancelled: boolean; readonly projection?: FreshSessionProjectionEvidence; }) => void; readonly reject: (error: Error) => void; readonly timer: ReturnType; } /** * Bridges the `FreshSessionReplacementPort` contract to real pi command-context session * control (design.md §8.3). `waitForIdle` cannot itself call `ctx.waitForIdle()`: that method * only exists on `ExtensionCommandContext`, which pi hands over solely by dispatching the * `/amq-new-task` command, and dispatch requires the message ID that `waitForIdle` never * receives. So `replace()` fires that dispatch and correlates the eventual command-handler * completion (which performs the real `ctx.waitForIdle()` as its first step, unconditionally) * back to this call via a per-messageId handoff. */ export class PiFreshSessionReplacementPort implements FreshSessionReplacementPort { readonly #pi: ExtensionAPI; readonly #dispatchTimeoutMs: number; readonly #pending = new Map(); constructor(pi: ExtensionAPI, options: { readonly dispatchTimeoutMs?: number } = {}) { this.#pi = pi; this.#dispatchTimeoutMs = options.dispatchTimeoutMs ?? DEFAULT_FRESH_SESSION_DISPATCH_TIMEOUT_MS; } async waitForIdle(signal?: AbortSignal): Promise { if (signal?.aborted) throw abortError(signal); } replace( message: AmqMessage, signal?: AbortSignal, ): Promise<{ readonly cancelled: boolean; readonly projection?: FreshSessionProjectionEvidence }> { const messageId = message.header.id; if (this.#pending.has(messageId)) { return Promise.reject(new Error(`Fresh-session dispatch for ${messageId} is already in flight`)); } return new Promise((resolve, reject) => { const onAbort = (): void => settle.reject(abortError(signal)); const settle: FreshSessionHandoff = { message, resolve: (result) => { this.#finish(messageId, onAbort, signal); resolve(result); }, reject: (error) => { this.#finish(messageId, onAbort, signal); reject(error); }, timer: setTimeout(() => { settle.reject(new Error( `Fresh-session command dispatch for ${messageId} did not reach the /${FRESH_SESSION_COMMAND} ` + `handler within ${this.#dispatchTimeoutMs}ms`, )); }, this.#dispatchTimeoutMs), }; this.#pending.set(messageId, settle); signal?.addEventListener("abort", onAbort, { once: true }); try { this.#pi.sendUserMessage(`/${FRESH_SESSION_COMMAND} ${messageId}`, { expandPromptTemplates: true }); } catch (error) { settle.reject(toError(error)); } }); } #finish(messageId: string, onAbort: () => void, signal: AbortSignal | undefined): void { const handoff = this.#pending.get(messageId); if (!handoff) return; clearTimeout(handoff.timer); signal?.removeEventListener("abort", onAbort); this.#pending.delete(messageId); } /** Invoked by the registered `/amq-new-task` command once pi dispatches it. */ async handleCommand(rawArgs: string, ctx: ExtensionCommandContext): Promise { const messageId = rawArgs.trim(); const handoff = messageId.length > 0 ? this.#pending.get(messageId) : undefined; if (!handoff) { ctx.ui.notify( `AMQ fresh-session dispatch has no matching pending request for "${messageId}"`, "warning", ); return; } try { handoff.resolve(await this.#replaceSession(handoff.message, ctx)); } catch (error) { handoff.reject(toError(error)); } } async #replaceSession( message: AmqMessage, ctx: ExtensionCommandContext, ): Promise<{ readonly cancelled: boolean; readonly projection?: FreshSessionProjectionEvidence }> { await ctx.waitForIdle(); let projection: FreshSessionProjectionEvidence | undefined; const parentSessionFile = ctx.sessionManager.getSessionFile(); const { cancelled } = await ctx.newSession({ ...(parentSessionFile ? { parentSession: parentSessionFile } : {}), setup: async (sessionManager) => { sessionManager.appendCustomEntry(FRESH_SESSION_PROVENANCE_TYPE, { schema: 1, messageId: message.header.id, from: message.header.from, fromProject: message.header.fromProject, parentSessionId: ctx.sessionManager.getSessionId(), }); }, withSession: async (fresh) => { const freshSessionId = fresh.sessionManager.getSessionId(); const [request] = planPriorityDeliveries([message], freshSessionId, { deliveryOverride: { deliverAs: "followUp", triggerTurn: true }, }); if (!request) throw new Error(`Fresh-session kickoff envelope for ${message.header.id} could not be built`); await fresh.sendMessage(request.message, request.options); // Pi persists a fresh session's file lazily: nothing is written to disk until its // first assistant entry arrives (session-manager `_persist`). Immediately after the // kickoff send, the session file may legitimately not exist yet, so a missing disk // projection is not an error here — fall back to the in-memory branch, which pi // always keeps current. Journal reconciliation later promotes this to `injected` // once the projection becomes disk-visible (transport/inbox-pump.ts `#reconcileStartup`). const diskProjection = await new PiSessionProjectionLookup(fresh) .find(message.header.id, freshSessionId); projection = diskProjection ?? ( branchContainsMessage(fresh.sessionManager.getBranch(), message.header.id) ? { messageId: message.header.id, sessionId: freshSessionId, diskVisible: false } : undefined ); }, }); if (cancelled) return { cancelled: true }; if (!projection || projection.messageId !== message.header.id) { throw new Error( `Fresh-session replacement for ${message.header.id} completed without a matching in-memory or ` + "disk-visible projection", ); } return { cancelled: false, projection }; } } function branchContainsMessage(entries: readonly SessionEntry[], messageId: string): boolean { return entries.some((entry) => { if (entry.type !== "custom_message" || entry.customType !== PI_AMQ_CUSTOM_TYPE) return false; const details = entry.details as Partial | undefined; return Array.isArray(details?.messageIds) && details.messageIds.includes(messageId); }); } function abortError(signal: AbortSignal | undefined): Error { return signal?.reason instanceof Error ? signal.reason : new Error("Fresh-session dispatch was cancelled"); } function collectEnvelopeDetails(ctx: ExtensionContext): readonly PiAmqEnvelopeDetails[] { return ctx.sessionManager.getBranch().flatMap((entry) => { if (entry.type !== "custom_message" || entry.customType !== PI_AMQ_CUSTOM_TYPE) return []; const details = entry.details as Partial | undefined; return Array.isArray(details?.envelopes) ? details.envelopes : []; }); } function replyRoutes(ctx: ExtensionContext): { lookup(messageId: string): { readonly to: string; readonly project?: string } | undefined; } { return { lookup(messageId) { const envelope = collectEnvelopeDetails(ctx).find((candidate) => candidate.id === messageId); if (!envelope) return undefined; const to = envelope.replyTo || envelope.from; if (!to) return undefined; const project = envelope.replyProject || envelope.fromProject; return { to, ...(project ? { project } : {}) }; }, }; } function renderBindPreview(preview: ConfirmedBindPreview): string { return [ `kind=${preview.kind}`, `physical-root=${preview.root}`, `session=${preview.session || "base"}`, `project=${preview.project}`, `handle=${preview.handle}`, `roster=${preview.agents.join(",")}`, `command=${preview.commandText}`, ...preview.effects.map((effect) => `effect=${effect}`), ].join("\n"); } function latestBinding(ctx: ExtensionContext): unknown | undefined { const entries = ctx.sessionManager.getEntries(); for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]!; if (entry.type !== "custom" || entry.customType !== PI_AMQ_BINDING_ENTRY_TYPE) continue; return entry.data; } return undefined; } async function resolveAmqBinary(configured: string | undefined): Promise { if (configured) { if (!isAbsolute(configured)) throw new Error("PI_AMQ_BINARY must be an absolute path"); return configured; } for (const candidate of ["/opt/homebrew/bin/amq", "/usr/local/bin/amq", "/usr/bin/amq"]) { try { await access(candidate); return candidate; } catch { // Try the next conventional absolute path. } } return "/usr/local/bin/amq"; } function freshAllowlist(): ReadonlySet { return new Set((process.env.PI_AMQ_FRESH_ALLOWLIST ?? "").split(",").map((item) => item.trim()).filter(Boolean)); } function renderFleetRow(record: FleetAgentRecord): string[] { return [`${record.handle}/${record.role} ${record.model} ${record.state} ${record.lastActivity}`]; } function flag(value: boolean | string | undefined, key: string): Record { return typeof value === "string" && value.length > 0 ? { [key]: value } : {}; } function stringFlag(value: boolean | string | undefined): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } function bounded(value: string, maximum: number): string { return value.length <= maximum ? value : `${value.slice(0, maximum - 1)}…`; } function toError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); }