import { SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_CONTROL_EVENT, } from "../shared/types.ts"; const DEFAULT_SOURCE = "pi-subagents:herdr"; const DEFAULT_TTL_MS = 120_000; const DEFAULT_REFRESH_MS = 45_000; let metadataReportSeq = Date.now() * 1000; function nextMetadataReportSeq(): number { metadataReportSeq = Math.max(metadataReportSeq + 1, Date.now() * 1000); return metadataReportSeq; } export interface HerdrStatusBridgeEvents { on(event: string, handler: (data: unknown) => void): (() => void) | void; emit(event: string, data: unknown): void; } export interface HerdrStatusRun { id: string; agent?: string; agents?: string[]; needsAttention?: boolean; attentionLabel?: string; } export interface HerdrStatusBridgeOptions { events: HerdrStatusBridgeEvents; env?: Record; /** Current authoritative active-run projection, used before TTL refresh. */ getRuns?: () => Iterable; runHerdr: (args: readonly string[]) => void | Promise; ttlMs?: number; refreshMs?: number; timers?: { setInterval: typeof setInterval; clearInterval: typeof clearInterval; }; } export interface HerdrStatusBridge { /** * Binds the pane owner. Only the root interactive session may publish pane * metadata: headless parents (print/json), non-UI harnesses, and child * runtimes must never fight the pane's lifecycle authority over display * state. Also re-syncs runs that survived a reload/resume. */ sessionStarted(input: { hasUI: boolean; runs: Iterable }): void; agentStarted(): void; flush(): Promise; dispose(): void; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function startedRun(data: unknown): HerdrStatusRun | undefined { if (!isRecord(data) || typeof data.id !== "string" || !data.id) return undefined; return { id: data.id, ...(typeof data.agent === "string" ? { agent: data.agent } : {}), ...(Array.isArray(data.agents) && data.agents.every((agent) => typeof agent === "string") ? { agents: data.agents as string[] } : {}), }; } function completedRunId(data: unknown): string | undefined { if (!isRecord(data)) return undefined; const id = typeof data.runId === "string" ? data.runId : data.id; return typeof id === "string" && id.length > 0 ? id : undefined; } function attentionNotice(data: unknown): { runId: string; label: string } | undefined { if (!isRecord(data) || data.source !== "async" || !isRecord(data.event)) return undefined; if (data.event.type !== "needs_attention" || typeof data.event.runId !== "string" || !data.event.runId) return undefined; const label = typeof data.noticeText === "string" && data.noticeText ? data.noticeText : typeof data.event.message === "string" && data.event.message ? data.event.message : "subagent needs attention"; return { runId: data.event.runId, label }; } export function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): HerdrStatusBridge { const env = options.env ?? process.env; const paneId = env.HERDR_PANE_ID; const enabled = env.HERDR_ENV === "1" && typeof paneId === "string" && paneId.length > 0; const runHerdr = options.runHerdr; const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; const refreshMs = options.refreshMs ?? DEFAULT_REFRESH_MS; const timers = options.timers ?? { setInterval, clearInterval }; const runs = new Map(); const attentionLabels = new Map(); const acknowledgedAttention = new Set(); const subscriptions: Array<() => void> = []; let rootSession = false; let published = false; let busyRaised = false; let busyLabel: string | undefined; let blockedRaised = false; let blockedLabel: string | undefined; let disposed = false; let pendingReport: readonly string[] | undefined; let draining = false; let drainPromise = Promise.resolve(); let refreshTimer: ReturnType | undefined; const label = (): string => { const agents = [...new Set([...runs.values()].flatMap((run) => run.agents?.length ? run.agents : run.agent ? [run.agent] : []))]; const who = agents.length > 0 ? ` (${agents.slice(0, 3).join(", ")}${agents.length > 3 ? ", …" : ""})` : ""; return `⏳ ${runs.size} subagent${runs.size === 1 ? "" : "s"}${who}`; }; const enqueue = (args: readonly string[]): void => { pendingReport = args; if (draining) return; draining = true; drainPromise = (async () => { while (pendingReport) { const next = pendingReport; pendingReport = undefined; try { await runHerdr(next); } catch { // Herdr integration is best effort; a later state transition or TTL // refresh retries with the newest desired snapshot. } } })().finally(() => { draining = false; }); }; const publish = (): void => { if (!enabled || !rootSession || disposed || !paneId) return; if (runs.size === 0 && !published) return; const seq = String(nextMetadataReportSeq()); if (runs.size === 0) { published = false; enqueue([ "pane", "report-metadata", paneId, "--source", DEFAULT_SOURCE, "--agent", "pi", "--applies-to-source", "herdr:pi", "--clear-state-labels", "--clear-token", "summary", "--seq", seq, ]); return; } const text = label(); published = true; enqueue([ "pane", "report-metadata", paneId, "--source", DEFAULT_SOURCE, "--agent", "pi", "--applies-to-source", "herdr:pi", "--state-label", `idle=${text}`, "--state-label", `done=${text}`, "--state-label", `working=${text}`, "--token", `summary=${text}`, "--ttl-ms", String(ttlMs), "--seq", seq, ]); }; const syncRefreshTimer = (): void => { if (runs.size > 0 && refreshMs > 0 && !refreshTimer) { refreshTimer = timers.setInterval(() => refresh(), refreshMs); refreshTimer.unref?.(); } else if ((runs.size === 0 || refreshMs <= 0) && refreshTimer) { timers.clearInterval(refreshTimer); refreshTimer = undefined; } }; const syncBusy = (): void => { if (!enabled || !rootSession || disposed) return; if (runs.size > 0) { const text = label(); if (busyRaised && busyLabel === text) return; if (busyRaised) options.events.emit("herdr:busy", { active: false }); busyRaised = true; busyLabel = text; options.events.emit("herdr:busy", { active: true, label: text }); return; } if (busyRaised) { busyRaised = false; busyLabel = undefined; options.events.emit("herdr:busy", { active: false }); } }; const syncBlocked = (): void => { if (!enabled || !rootSession || disposed) return; const nextLabel = [...attentionLabels.values()].at(-1); if (nextLabel !== undefined) { if (blockedRaised && blockedLabel === nextLabel) return; // Herdr's sibling overlay contract is counted. Lower before changing // the active label so this bridge continues to own exactly one count. if (blockedRaised) options.events.emit("herdr:blocked", { active: false }); blockedRaised = true; blockedLabel = nextLabel; options.events.emit("herdr:blocked", { active: true, label: nextLabel }); return; } if (blockedRaised) { blockedRaised = false; blockedLabel = undefined; options.events.emit("herdr:blocked", { active: false }); } }; const clearAttention = (): void => { attentionLabels.clear(); syncBlocked(); }; const raiseAttention = (runId: string, labelText: string): void => { if (!rootSession || attentionLabels.has(runId)) return; acknowledgedAttention.delete(runId); attentionLabels.set(runId, labelText); syncBlocked(); }; const replaceRuns = (nextRuns: Iterable): void => { const nextAttention = new Map(); const activeIds = new Set(); runs.clear(); for (const run of nextRuns) { if (!run || typeof run.id !== "string" || !run.id) continue; activeIds.add(run.id); runs.set(run.id, { ...run }); if (!run.needsAttention) { acknowledgedAttention.delete(run.id); } else if (!acknowledgedAttention.has(run.id)) { nextAttention.set(run.id, run.attentionLabel || attentionLabels.get(run.id) || "subagent needs attention"); } } for (const id of acknowledgedAttention) { if (!activeIds.has(id)) acknowledgedAttention.delete(id); } attentionLabels.clear(); for (const [id, labelText] of nextAttention) attentionLabels.set(id, labelText); syncBusy(); syncRefreshTimer(); syncBlocked(); publish(); }; const refresh = (): void => { if (!options.getRuns) { publish(); return; } try { replaceRuns(options.getRuns()); } catch { // Keep the last known active projection and retry on the next refresh. publish(); } }; const subscribe = (event: string, handler: (data: unknown) => void): void => { const unsubscribe = options.events.on(event, handler); if (typeof unsubscribe === "function") subscriptions.push(unsubscribe); }; if (enabled) { subscribe(SUBAGENT_ASYNC_STARTED_EVENT, (data) => { if (!rootSession) return; const run = startedRun(data); if (!run) return; acknowledgedAttention.delete(run.id); runs.set(run.id, run); syncBusy(); syncRefreshTimer(); publish(); }); subscribe(SUBAGENT_ASYNC_COMPLETE_EVENT, (data) => { if (!rootSession) return; const id = completedRunId(data); if (!id || !runs.delete(id)) return; acknowledgedAttention.delete(id); if (attentionLabels.delete(id)) syncBlocked(); syncBusy(); syncRefreshTimer(); publish(); }); subscribe(SUBAGENT_CONTROL_EVENT, (data) => { if (!rootSession) return; const notice = attentionNotice(data); if (!notice || !runs.has(notice.runId)) return; raiseAttention(notice.runId, notice.label); }); } return { agentStarted() { for (const id of attentionLabels.keys()) acknowledgedAttention.add(id); clearAttention(); }, sessionStarted({ hasUI, runs: restoredRuns }) { if (!enabled || disposed || hasUI !== true) return; rootSession = true; replaceRuns(restoredRuns); }, async flush() { while (draining || pendingReport) await drainPromise; }, dispose() { if (disposed) return; clearAttention(); acknowledgedAttention.clear(); runs.clear(); syncBusy(); syncRefreshTimer(); publish(); for (const unsubscribe of subscriptions) unsubscribe(); disposed = true; }, }; }