export type PiRuntimeMode = "tui" | "rpc" | "json" | "print"; import type { ReceiverLeaseAcquireResult } from "./receiver-lease.js"; export interface InboxRuntimeStatus { readonly receiver: "dormant" | "send-only" | "receiving"; readonly watch: "stopped" | "recovering" | "watching" | "backoff" | "disabled"; readonly receiverGeneration: string | null; readonly backoffAttempt: number; readonly nextBackoffMs: number | null; readonly lastError: string | null; readonly staged: { readonly urgent: number; readonly normal: number; readonly low: number; }; } export type InboxStatusProjection = | { readonly surface: "tui"; readonly text: string; readonly value: InboxRuntimeStatus } | { readonly surface: "rpc"; readonly value: InboxRuntimeStatus } | { readonly surface: "none" }; export type ReceiverLeaseRuntimeDisposition = | { readonly mode: "receiving"; readonly generation: string } | { readonly mode: "send-only"; readonly diagnostic: string }; export class InboxStatusStore { #status: InboxRuntimeStatus = { receiver: "dormant", watch: "stopped", receiverGeneration: null, backoffAttempt: 0, nextBackoffMs: null, lastError: null, staged: { urgent: 0, normal: 0, low: 0 }, }; readonly #listeners = new Set<(status: InboxRuntimeStatus) => void>(); snapshot(): InboxRuntimeStatus { return cloneStatus(this.#status); } update(update: Partial): void { this.#status = cloneStatus({ ...this.#status, ...update }); for (const listener of this.#listeners) listener(this.snapshot()); } subscribe(listener: (status: InboxRuntimeStatus) => void): () => void { this.#listeners.add(listener); return () => this.#listeners.delete(listener); } } export function projectInboxStatus( mode: PiRuntimeMode, status: InboxRuntimeStatus, ): InboxStatusProjection { if (mode === "print" || mode === "json") return { surface: "none" }; const value = cloneStatus(status); if (mode === "rpc") return { surface: "rpc", value }; if (status.receiver === "dormant") return { surface: "none" }; const parts = ["AMQ"]; if (status.watch === "backoff" || status.watch === "recovering") { parts.push(status.nextBackoffMs === null ? "reconnecting" : `reconnecting in ${status.nextBackoffMs}ms`); } else { parts.push(status.receiver); } const counts = Object.entries(status.staged) .filter(([, count]) => count > 0) .map(([priority, count]) => `${count} ${priority}`); if (counts.length > 0) parts.push(counts.join(", ")); if (status.lastError) parts.push(bounded(status.lastError)); return { surface: "tui", text: parts.join(" · "), value, }; } export async function runInboxForMode( mode: PiRuntimeMode, pump: { run(signal?: AbortSignal): Promise }, status: InboxStatusStore, signal?: AbortSignal, ): Promise<{ readonly kind: "send-only" } | { readonly kind: "completed" }> { if (mode === "print" || mode === "json") { status.update({ receiver: "send-only", watch: "disabled", receiverGeneration: null, nextBackoffMs: null, lastError: null, }); return { kind: "send-only" }; } await pump.run(signal); return { kind: "completed" }; } export function receiverLeaseRuntimeDisposition( result: ReceiverLeaseAcquireResult, ): ReceiverLeaseRuntimeDisposition { if (result.kind === "acquired") { return { mode: "receiving", generation: result.owner.generation }; } return { mode: "send-only", diagnostic: result.kind === "held" ? `Receiver is held by pi session ${result.owner.piSessionId}` : result.diagnostic, }; } function cloneStatus(status: InboxRuntimeStatus): InboxRuntimeStatus { return { ...status, staged: { ...status.staged } }; } function bounded(value: string): string { return value.length <= 160 ? value : `${value.slice(0, 159)}…`; }