import * as fs from "node:fs"; import * as path from "node:path"; import { randomUUID } from "node:crypto"; import type { ExtensionAPI } from "@selesai/code"; import { listAsyncRuns } from "./async-status.ts"; import { writeAtomicJson } from "../../shared/atomic-json.ts"; import { DIRS, INTERCOM_DETACH_REQUEST_EVENT, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, SUBAGENT_RESULT_INTERCOM_EVENT, type SubagentState, type WaitSubscriptionRecord, } from "../../shared/types.ts"; const SUBSCRIPTION_VERSION = 1; const RECONCILE_INTERVAL_MS = 1_000; export interface ArmWaitSubscriptionInput { targetKind: "async" | "foreground"; runId: string; requestedId: string; timeoutMs: number; } export interface WaitSubscriptionManager { arm(input: ArmWaitSubscriptionInput): WaitSubscriptionRecord; restore(): void; reconcile(): void; dispose(): void; } interface WaitSubscriptionManagerOptions { asyncDirRoot?: string; resultsDir?: string; subscriptionsDir?: string; now?: () => number; pollIntervalMs?: number; kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean; } function isNotFound(error: unknown): boolean { return typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"; } function parseRecord(value: unknown): WaitSubscriptionRecord | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; const record = value as Partial; if (record.version !== SUBSCRIPTION_VERSION || typeof record.token !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(record.token) || typeof record.sessionId !== "string" || (record.targetKind !== "async" && record.targetKind !== "foreground") || typeof record.runId !== "string" || typeof record.requestedId !== "string" || typeof record.createdAt !== "number" || typeof record.expiresAt !== "number") return undefined; return record as WaitSubscriptionRecord; } function needsAttention(run: ReturnType[number]): boolean { return run.activityState === "needs_attention" || run.steps.some((step) => step.activityState === "needs_attention"); } function subscriptionFile(dir: string, token: string): string { return path.join(dir, `${token}.json`); } export function formatWaitSubscriptions(state: Pick, now = Date.now()): string | undefined { const subscriptions = [...(state.waitSubscriptions?.values() ?? [])].sort((left, right) => left.createdAt - right.createdAt); if (subscriptions.length === 0) return undefined; const lines = [`Armed wait subscriptions (${subscriptions.length}):`]; for (const record of subscriptions) { lines.push(`- ${record.token}: ${record.targetKind} run ${record.runId}, timeout in ${Math.max(0, record.expiresAt - now)}ms`); } return lines.join("\n"); } export function createWaitSubscriptionManager( pi: Pick, state: SubagentState, options: WaitSubscriptionManagerOptions = {}, ): WaitSubscriptionManager { const asyncDirRoot = options.asyncDirRoot ?? DIRS.async; const resultsDir = options.resultsDir ?? DIRS.results; const subscriptionsDir = options.subscriptionsDir ?? path.join(path.dirname(asyncDirRoot), "wait-subscriptions"); const now = options.now ?? Date.now; const subscriptions = state.waitSubscriptions ?? new Map(); state.waitSubscriptions = subscriptions; const unresolvedRestoredForegroundTokens = new Set(); let disposed = false; const remove = (record: WaitSubscriptionRecord) => { try { fs.unlinkSync(subscriptionFile(subscriptionsDir, record.token)); } catch (error) { if (!isNotFound(error)) throw error; } subscriptions.delete(record.token); unresolvedRestoredForegroundTokens.delete(record.token); }; const settle = (record: WaitSubscriptionRecord, outcome: string, detail: string) => { if (disposed || state.currentSessionId !== record.sessionId) return; try { remove(record); } catch (error) { console.error(`Failed to clear wait subscription '${record.token}'; it remains armed:`, error); return; } try { pi.sendMessage({ customType: "subagent-wait-subscription", content: `Wait subscription ${record.token} fired for run ${record.runId}: ${outcome}. ${detail}`, display: true, details: { token: record.token, runId: record.runId, outcome }, }, { triggerTurn: true }); } catch (error) { console.error(`Failed to deliver wait subscription '${record.token}' after clearing it:`, error); } }; const reconcileRecord = (record: WaitSubscriptionRecord) => { if (record.sessionId !== state.currentSessionId) return; if (now() >= record.expiresAt) { settle(record, "timed out", "The targeted run may still be active; inspect its status before taking follow-up action."); return; } if (record.targetKind === "foreground") { const run = state.foregroundRuns?.get(record.runId); if (!run && unresolvedRestoredForegroundTokens.has(record.token)) return; if (run) unresolvedRestoredForegroundTokens.delete(record.token); if (!run || run.sessionId !== record.sessionId) { settle(record, "could not be reconciled", "The remembered foreground run disappeared before completion was confirmed."); return; } const detached = run.children.filter((child) => child.status === "detached"); if (detached.some((child) => child.activityState === "needs_attention")) { settle(record, "needs attention", "Reply to the pending supervisor request or inspect the run status."); return; } if (detached.length === 0) { const failed = run.children.some((child) => child.status === "failed"); settle(record, failed ? "failed" : "completed", "Inspect the run status for its final output."); } return; } const runs = listAsyncRuns(asyncDirRoot, { sessionId: record.sessionId, runId: record.runId, resultsDir, kill: options.kill, now, }); const run = runs.find((candidate) => candidate.id === record.runId); if (!run) { settle(record, "could not be reconciled", "The exact async run disappeared before a terminal result was confirmed."); return; } if (needsAttention(run)) { settle(record, "needs attention", "Inspect the run status and answer any pending supervisor request."); return; } if (run.state !== "queued" && run.state !== "running") { settle(record, run.state === "complete" ? "completed" : run.state, "Inspect the run status for its final output."); } }; const reconcile = () => { if (disposed || !state.currentSessionId) return; for (const record of [...subscriptions.values()]) { try { reconcileRecord(record); } catch (error) { console.error(`Failed to reconcile wait subscription '${record.token}':`, error); settle(record, "reconciliation failed", "The targeted run could not be reconciled. Inspect its status before taking follow-up action."); } } }; const wakeChannels = [ INTERCOM_DETACH_REQUEST_EVENT, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, SUBAGENT_RESULT_INTERCOM_EVENT, ]; const unsubscribes = wakeChannels.map((channel) => pi.events.on(channel, reconcile)); const interval = setInterval(reconcile, options.pollIntervalMs ?? RECONCILE_INTERVAL_MS); interval.unref?.(); return { arm(input) { const sessionId = state.currentSessionId; if (!sessionId) throw new Error("A wait subscription requires an active session identity."); const createdAt = now(); const record: WaitSubscriptionRecord = { version: SUBSCRIPTION_VERSION, token: randomUUID(), sessionId, targetKind: input.targetKind, runId: input.runId, requestedId: input.requestedId, createdAt, expiresAt: createdAt + input.timeoutMs, }; fs.mkdirSync(subscriptionsDir, { recursive: true }); writeAtomicJson(subscriptionFile(subscriptionsDir, record.token), record); subscriptions.set(record.token, record); return record; }, restore() { subscriptions.clear(); unresolvedRestoredForegroundTokens.clear(); if (!state.currentSessionId) return; let files: string[]; try { files = fs.readdirSync(subscriptionsDir).filter((file) => file.endsWith(".json")); } catch (error) { if (isNotFound(error)) return; throw error; } for (const file of files) { try { const record = parseRecord(JSON.parse(fs.readFileSync(path.join(subscriptionsDir, file), "utf-8"))); if (!record || file !== `${record.token}.json`) continue; if (record.sessionId === state.currentSessionId) { subscriptions.set(record.token, record); if (record.targetKind === "foreground" && !state.foregroundRuns?.has(record.runId)) unresolvedRestoredForegroundTokens.add(record.token); } } catch (error) { console.error(`Ignoring invalid wait subscription '${path.join(subscriptionsDir, file)}':`, error); } } reconcile(); }, reconcile, dispose() { if (disposed) return; disposed = true; clearInterval(interval); for (const unsubscribe of unsubscribes) { try { unsubscribe(); } catch { /* best effort */ } } subscriptions.clear(); }, }; }