// Parent shutdown policy: the parent-side decision of what happens to // running Subagents when the parent session ends. /reload preserves the // parent-local runtime (Supported reload boundary); a permanent shutdown // cascades the Descendant cancellation cascade, waits for descendant drain, // and only then reclaims the parent surface. See CONTEXT.md // "Descendant cancellation cascade" and "Supported reload boundary". import type { CompletionDeliveryManager } from "./completion-delivery.ts"; import type { NestedLifecycleCoordinator } from "./nested-lifecycle.ts"; import { interruptAndCloseSubagent, noteExternalCancellation, requestSubagentCancellation, type RunningSubagent, } from "./running-registry.ts"; import { sendHerdrAgentEscape } from "./herdr.ts"; /** /reload preserves the parent-local runtime; anything else is final. */ export function shouldPreserveSubagentsOnShutdown(reason: unknown): boolean { return reason === "reload"; } type ShutdownSubagent = Pick & Partial>; /** The shutdown policy needs only iteration and clearing of the collection. */ export interface ShutdownAgentCollection { values(): Iterable; clear(): void; } /** Collaborators the shutdown policy consumes; all cross the seam explicitly. */ export interface ParentShutdownOptions { /** The parent-wide delivery boundary; closed before watchers are aborted. */ deliveryManager: CompletionDeliveryManager; /** Best-effort native surface cleanup for records outside the nested lifecycle. */ cleanupSurface?: (running: Pick) => void; /** Native turn-interrupt used by structured cancellation; Herdr escape in production. */ interruptSurface?: (surface: string) => void; /** Structured nested-lifecycle coordinator owning the Cancellation cutover. */ coordinator: NestedLifecycleCoordinator; } export async function cleanupSubagentsForShutdown( reason: unknown, agents: ShutdownAgentCollection, options: ParentShutdownOptions, ): Promise { const { deliveryManager, cleanupSurface = interruptAndCloseSubagent, interruptSurface = sendHerdrAgentEscape, coordinator, } = options; if (shouldPreserveSubagentsOnShutdown(reason)) return; // The delivery manager owns foreground delivery independently of the child // registry. Close it before aborting watchers so an in-flight destination // cannot accept evidence after shutdown. deliveryManager.shutdown(); const deferredCleanup: ShutdownSubagent[] = []; const cancelAgent = (agent: ShutdownSubagent): void => { if (agent.cancellationRequested) return; // Identified records cancel through the structured path: cancellation // requests a Child-side acknowledgement and keeps both watcher and native // surface alive until that ack proves the descendant tree has drained. if (agent.id && agent.artifacts) { requestSubagentCancellation(agent as RunningSubagent, interruptSurface); return; } // Anonymous collection entries (the shutdown seam's test harness shape) // have no structured counterpart; mark cancellation and defer a // best-effort native surface cleanup until drain completes or the // collection clears. noteExternalCancellation(agent); if (agent.surface) deferredCleanup.push(agent); }; // The coordinator owns the cutover and visits reservations as well as // adopted descendants. Registry entries are deliberately retained until // their Completion watches report terminal drain; this prevents a parent // surface from being reclaimed while a Nested Subagent is still owned. coordinator.requestCancellation((descendant) => { for (const agent of agents.values()) { if (agent.id === descendant.operationId) { cancelAgent(agent); return; } } }); for (const agent of agents.values()) cancelAgent(agent); // Production registries are released by their Completion watch after // descendant termination. A generic collection is cleared only after the // coordinator confirms the descendant drain. if (!coordinator.isDraining()) { flushDeferredCleanup(deferredCleanup, cleanupSurface); agents.clear(); } else { await coordinator.waitForDescendantDrain(); flushDeferredCleanup(deferredCleanup, cleanupSurface); agents.clear(); } } /** Best-effort native surface cleanup; a missing surface must not block shutdown. */ function cleanupOneSurface( agent: ShutdownSubagent, cleanupSurface: (running: Pick) => void, ): void { if (!agent.surface) return; try { cleanupSurface({ surface: agent.surface }); } catch { // Shutdown is best effort; the watcher abort and registry clear continue. } } function flushDeferredCleanup( deferredCleanup: ShutdownSubagent[], cleanupSurface: (running: Pick) => void, ): void { for (const agent of deferredCleanup) cleanupOneSurface(agent, cleanupSurface); }