import { Buffer } from "node:buffer"; import { resolveLaunchOptions, type LaunchDefaults, type LaunchOptions } from "./launch-options.js"; import { CAPTURED_TEXT_MAX_BYTES, truncateUtf8 } from "./output.js"; import { BoundedReportIds, isReservedChildShutdownMessage, normalizeReportId, RESERVED_CHILD_SHUTDOWN_MESSAGE, reportRecordBytes, SessionChannelFailureError, TrackedSessionOpenError, type RunningSubagentSession, type SessionEvent, type SessionExit, type SessionResult, type SessionRunner, } from "./session-runner.js"; import { REPORT_INBOX_MAX_BYTES, REPORT_INBOX_MAX_ITEMS, REPORT_MAX_BYTES, RESULT_PREVIEW_MAX_BYTES, SESSION_TOMBSTONE_MAX_ITEMS, STATUS_ACTIVITY_LIMIT, STATUS_ACTIVITY_MAX_BYTES, TASK_MAX_BYTES, type AgentProfile, type JobRequest, type SubagentActivity, type SubagentGeneration, type SubagentReport, type SubagentSession, type UsageStats, type WorkState, } from "./types.js"; const MAX_OPEN_SESSIONS = 8; const SAFE_TOOL_ACTIVITY = /^(Started|Updated|Completed) (read|grep|find|ls|bash|edit|write|subagent_report|tool)$/u; const SESSION_FAILURE_ID_MAX_BYTES = 512; const stripActivityControls = (text: string): string => { let safe = ""; for (let index = 0; index < text.length;) { const code = text.charCodeAt(index); const escapeSequence = code === 0x1b; const osc = code === 0x9d || (escapeSequence && text[index + 1] === "]"); if (osc) { index += code === 0x9d ? 1 : 2; while ( index < text.length && text.charCodeAt(index) !== 0x07 && text.charCodeAt(index) !== 0x9c && !(text.charCodeAt(index) === 0x1b && text[index + 1] === "\\") ) index += 1; if (text.charCodeAt(index) === 0x1b) index += 2; else if (index < text.length) index += 1; continue; } const csi = code === 0x9b || (escapeSequence && text[index + 1] === "["); if (csi) { index += code === 0x9b ? 1 : 2; while (index < text.length && !(text.charCodeAt(index) >= 0x40 && text.charCodeAt(index) <= 0x7e)) index += 1; if (index < text.length) index += 1; continue; } if (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) { index += 1; continue; } safe += text[index] ?? ""; index += 1; } return safe.replace(/\s+/gu, " ").trim(); }; const safeFailureSessionId = (id: string): string => truncateUtf8(stripActivityControls(id), SESSION_FAILURE_ID_MAX_BYTES).text || "unknown"; interface Deferred { promise: Promise; resolve: (value: T | PromiseLike) => void; reject: (reason?: unknown) => void; } const deferred = (): Deferred => { let resolve: Deferred["resolve"] | undefined; let reject: Deferred["reject"] | undefined; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); if (resolve === undefined || reject === undefined) throw new Error("Deferred promise initialization failed"); return { promise, resolve, reject }; }; const withoutResultPreview = (generation: SubagentGeneration): SubagentGeneration => { const { resultPreview: _resultPreview, ...withoutPreview } = generation; return withoutPreview; }; const safeResultPreview = (output: string): string => { let safe = ""; for (let index = 0; index < output.length;) { const code = output.charCodeAt(index); const escapeSequence = code === 0x1b; const osc = code === 0x9d || (escapeSequence && output[index + 1] === "]"); if (osc) { index += code === 0x9d ? 1 : 2; while ( index < output.length && output.charCodeAt(index) !== 0x07 && output.charCodeAt(index) !== 0x9c && !(output.charCodeAt(index) === 0x1b && output[index + 1] === "\\") ) index += 1; if (output.charCodeAt(index) === 0x1b) index += 2; else if (index < output.length) index += 1; continue; } const csi = code === 0x9b || (escapeSequence && output[index + 1] === "["); if (csi) { index += code === 0x9b ? 1 : 2; while (index < output.length && !(output.charCodeAt(index) >= 0x40 && output.charCodeAt(index) <= 0x7e)) index += 1; if (index < output.length) index += 1; continue; } if (code === 0x0a) safe += "\n"; else if (code === 0x09) safe += " "; else if (code > 0x1f && code !== 0x7f && !(code >= 0x80 && code <= 0x9f)) safe += output[index] ?? ""; index += 1; } return truncateUtf8(safe, RESULT_PREVIEW_MAX_BYTES).text; }; const safeActivity = (event: Extract): SubagentActivity => { const text = stripActivityControls(truncateUtf8(event.text, STATUS_ACTIVITY_MAX_BYTES).text); if (text === "Model reasoning") return { type: "model", text, timestamp: event.timestamp }; if (SAFE_TOOL_ACTIVITY.test(text)) return { type: "tool", text, timestamp: event.timestamp }; return { type: "diagnostic", text: "Subagent activity", timestamp: event.timestamp }; }; const redactLaunchProfileValues = (text: string, profile: AgentProfile): string => { const privateValues = new Set([ profile.description, profile.systemPrompt, ...(profile.tools ?? []), profile.model, profile.filePath, ].filter((value): value is string => value !== undefined && value.length > 0)); const longestFirst = [...privateValues].sort((left, right) => right.length - left.length); return longestFirst.reduce((safe, value) => safe.split(value).join("[private profile data]"), text); }; interface InternalSession { session: SubagentSession; launchProfile?: AgentProfile; launchOptions: LaunchOptions; childPotentiallyLive: boolean; running?: RunningSubagentSession; queuedFollowUp?: string; helpReply?: string; helpReportId?: string; helpQuestion?: string; helpReport?: SubagentReport; helpSelectedGeneration?: number; helpResumeUsage?: UsageStats; generationPrompt?: string; partialResult?: SessionResult; cancellation?: Promise; closePromise?: Promise; resolveClose?: (session: SubagentSessionSnapshot) => void; rejectClose?: (error: unknown) => void; closeStarted?: boolean; reportIds: BoundedReportIds; sessionFailureEmitted: boolean; channelFailed?: boolean; } export interface SubagentGenerationSnapshot extends Omit {} export interface SubagentSessionSnapshot extends Omit { generation: SubagentGenerationSnapshot; queuedFollowUp: boolean; blockedByResult: boolean; cancellable: boolean; } export type SubagentManagerEvent = | { type: "report_added"; report: SubagentReport } | { type: "help_waiting"; report: SubagentReport } | { type: "session_failed"; sessionId: string; generation: number; reason: "unexpected_child_exit"; partialResultReady: boolean; }; export type WaitUntil = "any" | "all"; export type WaitOutcome = "completed" | "timed_out" | "aborted"; export interface WaitWorkStatus { id: string; generation: number; state: WorkState; resultReady: boolean; queuedFollowUp: boolean; blockedByResult: boolean; } export interface WaitResult { operation: "wait"; outcome: WaitOutcome; until: WaitUntil; timeoutMs: number; elapsedMs: number; jobs: WaitWorkStatus[]; } export interface WaitForOptions { ids: readonly string[]; until: WaitUntil; timeoutMs: number; signal?: AbortSignal; } export interface ReadInboxLimits { maxReports?: number; maxMessageBytes?: number; } export class SubagentManager { private readonly runner: SessionRunner; private readonly now: () => number; private readonly setTimer: (callback: () => void, delay: number) => unknown; private readonly clearTimer: (timer: unknown) => void; private readonly sessions = new Map(); private readonly subscribers = new Set<(sessions: readonly SubagentSessionSnapshot[]) => void>(); private readonly eventSubscribers = new Set<(event: SubagentManagerEvent) => void>(); private readonly tombstoneProtections = new Map(); private readonly readyQueue: string[] = []; private readonly followUpQueue: string[] = []; private readonly activeIds = new Set(); private nextId = 1; private nextReadyId = 1; private pumping = false; private closeAllPromise?: Promise; private shutdownPromise?: Promise; private closingAll = false; private notifying = false; private notificationPending = false; private pruning = false; private prunePending = false; constructor(options: { runner: SessionRunner; now?: () => number; setTimer?: (callback: () => void, delay: number) => unknown; clearTimer?: (timer: unknown) => void; }) { this.runner = options.runner; this.now = options.now ?? Date.now; this.setTimer = options.setTimer ?? ((callback, delay) => setTimeout(callback, delay)); this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer as NodeJS.Timeout)); } async enqueue( requests: JobRequest[], profiles: ReadonlyMap, defaults: LaunchDefaults & { cwd: string }, ): Promise { if (this.closingAll || this.shutdownPromise) throw new Error("Subagent manager is shutting down and cannot enqueue sessions"); if (requests.length === 0) throw new Error("Enqueue requires at least one session"); if (requests.some((request) => Buffer.byteLength(request.task, "utf8") > TASK_MAX_BYTES)) { throw new Error(`Task exceeds ${TASK_MAX_BYTES} UTF-8 bytes.`); } if (requests.some((request) => isReservedChildShutdownMessage(request.task))) { throw new Error(RESERVED_CHILD_SHUTDOWN_MESSAGE); } const selected = requests.map((request) => { const profile = profiles.get(request.agent); if (!profile) throw new Error(`Unknown agent profile: ${request.agent}`); const launchOptions = resolveLaunchOptions(request, profile, defaults); return { request, profile, launchOptions }; }); const diagnostic = selected.flatMap((entry) => entry.launchOptions.diagnostics)[0]; if (diagnostic) throw new Error(diagnostic); if (this.openCapacity() + selected.length > MAX_OPEN_SESSIONS) { throw new Error("Enqueue would exceed the limit of eight open sessions"); } const createdAt = this.now(); const added = selected.map(({ request, profile, launchOptions }) => { const session: SubagentSession = { id: `job-${this.nextId++}`, request: structuredClone(request), profile: { name: profile.name }, state: "opening", createdAt, reports: [], reportBytes: 0, omittedReports: 0, generation: { number: 1, state: "queued", resultState: "none", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }, activity: [], createdAt, }, launchModel: launchOptions.launchModel, launchThinkingLevel: launchOptions.launchThinkingLevel, launchThinkingSource: launchOptions.launchThinkingSource, }; const entry: InternalSession = { session, launchProfile: structuredClone(profile), launchOptions: structuredClone(launchOptions), childPotentiallyLive: true, reportIds: new BoundedReportIds(), sessionFailureEmitted: false, }; this.sessions.set(session.id, entry); return entry; }); this.notify(); await Promise.all(added.map((entry) => this.open(entry, defaults))); return added.map((entry) => this.snapshot(entry)); } list(): readonly SubagentSessionSnapshot[] { return [...this.sessions.values()].map((entry) => this.snapshot(entry)); } /** Returns true while any child is opening or has not confirmed that it closed. */ hasOpenChildren(): boolean { for (const entry of this.sessions.values()) { if (entry.childPotentiallyLive) return true; } return false; } get(id: string): SubagentSessionSnapshot | undefined { const entry = this.sessions.get(id); return entry ? this.snapshot(entry) : undefined; } readInbox(id?: string, limits?: ReadInboxLimits): SubagentReport[] { const entries = id === undefined ? [...this.sessions.values()] : [this.requireSession(id)]; const candidates = entries.flatMap((entry, sessionIndex) => entry.session.reports.map((report, reportIndex) => ({ entry, report, sessionIndex, reportIndex })), ).sort((left, right) => left.report.timestamp - right.report.timestamp || left.sessionIndex - right.sessionIndex || left.reportIndex - right.reportIndex, ); const maxReports = limits?.maxReports ?? Number.POSITIVE_INFINITY; const maxMessageBytes = limits?.maxMessageBytes ?? Number.POSITIVE_INFINITY; const selected: typeof candidates = []; let selectedMessageBytes = 0; for (const candidate of candidates) { if (selected.length >= maxReports) break; const messageBytes = Buffer.byteLength(candidate.report.message, "utf8"); if (selectedMessageBytes + messageBytes > maxMessageBytes) break; selected.push(candidate); selectedMessageBytes += messageBytes; } const selectedByEntry = new Map>(); for (const { entry, report } of selected) { const reports = selectedByEntry.get(entry) ?? new Set(); reports.add(report); selectedByEntry.set(entry, reports); } for (const [entry, reports] of selectedByEntry) { entry.session.reports = entry.session.reports.filter((report) => !reports.has(report)); entry.session.reportBytes = entry.session.reports.reduce( (bytes, report) => bytes + reportRecordBytes(report), 0, ); } if (selected.length > 0) this.notify(); const reports = selected.map(({ report }) => structuredClone(report)); this.requestTombstonePrune(); return reports; } close(id: string): Promise { const entry = this.sessions.get(id); if (!entry) return Promise.reject(new Error(`Unknown subagent session: ${id}`)); if (entry.session.state === "closed") { const closed = this.snapshot(entry); this.requestTombstonePrune(); return Promise.resolve(closed); } if (entry.closePromise) return entry.closePromise; if (entry.session.state === "failed") return Promise.reject(new Error(`Session ${id} is failed and cannot be closed`)); entry.closePromise = new Promise((resolve, reject) => { entry.resolveClose = resolve; entry.rejectClose = reject; }); entry.session.state = "closing"; this.clearQueuedWorkForClose(entry); this.notify(); this.startClose(entry); return entry.closePromise; } closeAll(): Promise { if (this.closeAllPromise) return this.closeAllPromise; const deferredOperation = deferred(); const operation = deferredOperation.promise; this.closeAllPromise = operation; this.closingAll = true; const closes = [...this.sessions.values()].map((entry) => this.close(entry.session.id)); void Promise.allSettled(closes).then(() => { deferredOperation.resolve(); }); void operation.then(() => { if (this.closeAllPromise !== operation) return; this.closingAll = false; this.closeAllPromise = undefined; this.pump(); }); return operation; } shutdown(): Promise { if (this.shutdownPromise) return this.shutdownPromise; const deferredOperation = deferred(); const operation = deferredOperation.promise; this.shutdownPromise = operation; try { void this.closeAll().then( () => { void this.waitForNoOpenChildren().then(deferredOperation.resolve, deferredOperation.reject); }, deferredOperation.reject, ); } catch (error) { deferredOperation.reject(error); } const clearOperation = () => { if (this.shutdownPromise === operation) this.shutdownPromise = undefined; }; void operation.then(clearOperation, clearOperation); return operation; } async cancel(id: string): Promise { const entry = this.sessions.get(id); if (!entry) throw new Error(`Unknown subagent session: ${id}`); if (entry.session.state === "closing" || entry.session.state === "closed" || entry.session.state === "failed") { throw new Error(`Session ${id} is ${entry.session.state} and cannot be cancelled`); } const generation = entry.session.generation; if (generation.state === "cancelling") { await entry.cancellation; return this.snapshot(entry); } if (this.activeIds.has(id)) { if (generation.state !== "running" || !entry.running) { throw new Error(`Session ${id} has no cancellable running child`); } generation.state = "cancelling"; this.notify(); const cancellation = entry.running.abort().catch((error: unknown) => { // A fatal channel failure leaves command acceptance unknown until child exit confirms it. if (error instanceof SessionChannelFailureError) { this.retainFatalChannelFailure(entry, error); throw error; } queueMicrotask(() => { if (entry.session.state !== "open" || entry.session.generation.state !== "cancelling") return; entry.session.generation.state = "running"; entry.session.errorMessage = this.normalizeFailure(error).text; this.notify(); }); throw error; }); entry.cancellation = cancellation; try { await cancellation; } finally { if (entry.cancellation === cancellation) entry.cancellation = undefined; } return this.snapshot(entry); } if (entry.helpReply !== undefined && generation.state === "waiting_for_parent") { entry.helpReply = undefined; this.removeQueuedId(this.readyQueue, id); this.notify(); return this.snapshot(entry); } if (generation.state === "queued") { entry.generationPrompt = undefined; generation.state = "cancelled"; generation.finishedAt = this.now(); this.removeQueuedId(this.readyQueue, id); // Pumping may advance the preserved follow-up to a new current generation, so return the cancelled one. const cancelled = this.snapshot(entry); this.pump(); this.notify(); return cancelled; } if (entry.queuedFollowUp !== undefined) { entry.queuedFollowUp = undefined; this.removeQueuedId(this.followUpQueue, id); this.notify(); return this.snapshot(entry); } throw new Error(`Session ${id} has no cancellable work`); } /** * Internal collection seam. Callers must format this deep clone synchronously with a pure * formatter, then immediately call collect(). With no await between those calls, JavaScript * run-to-completion keeps both validations on the same current ready-result barrier. */ peekReadyResult(id: string): SubagentSession { return structuredClone(this.requireReadyResult(id, "collect").session); } collect(id: string): SubagentSession { const entry = this.requireReadyResult(id, "collect"); const result = entry.session.generation.result; if (!result) throw new Error(`Session ${id} no longer has a ready result to collect`); const preview = safeResultPreview(result.output); // Build the complete return value before crossing the irreversible result-release barrier. const collected = structuredClone({ ...entry.session, generation: { ...entry.session.generation, resultState: "collected" as const, ...(preview ? { resultPreview: preview } : {}), result, }, }); entry.session.generation.resultState = "collected"; if (preview) entry.session.generation.resultPreview = preview; else entry.session.generation = withoutResultPreview(entry.session.generation); entry.session.generation.result = undefined; entry.partialResult = undefined; this.pump(); this.notify(); this.requestTombstonePrune(); return collected; } discard(id: string): SubagentSessionSnapshot { const entry = this.requireReadyResult(id, "discard"); entry.session.generation.resultState = "discarded"; entry.session.generation = withoutResultPreview(entry.session.generation); entry.session.generation.result = undefined; entry.partialResult = undefined; const discarded = this.snapshot(entry); this.pump(); this.notify(); this.requestTombstonePrune(); return discarded; } subscribe(listener: (sessions: readonly SubagentSessionSnapshot[]) => void): () => void { this.subscribers.add(listener); try { listener(this.list()); } catch { // A listener cannot disrupt the manager. } return () => this.subscribers.delete(listener); } subscribeEvents(listener: (event: SubagentManagerEvent) => void): () => void { this.eventSubscribers.add(listener); return () => this.eventSubscribers.delete(listener); } async waitFor(options: WaitForOptions): Promise { if (options.ids.length === 0) throw new Error("Wait requires at least one session ID"); const seen = new Set(); for (const id of options.ids) { if (seen.has(id)) throw new Error(`Duplicate session ID: ${id}`); seen.add(id); this.requireSession(id); } const releaseTombstones = this.protectTombstones(options.ids); const startedAt = this.now(); const work = this.waitSnapshots(options.ids); if (this.waitSatisfied(work, options.until)) { const result = this.waitResult("completed", options, startedAt, work); releaseTombstones(); return result; } if (options.signal?.aborted) { const result = this.waitResult("aborted", options, startedAt, work); releaseTombstones(); return result; } return new Promise((resolve, reject) => { let settled = false; let timer: unknown; let hasTimer = false; let unsubscribe: (() => void) | undefined; let abortSubscribed = false; const settle = (outcome: WaitOutcome, observed?: readonly SubagentSessionSnapshot[]): void => { if (settled) return; const current = this.waitSnapshots(options.ids, observed); if (outcome === "completed" && !this.waitSatisfied(current, options.until)) return; settled = true; cleanup(); resolve(this.waitResult(outcome, options, startedAt, current)); }; const onAbort = () => settle("aborted"); const cleanup = () => { const stop = unsubscribe; unsubscribe = undefined; stop?.(); if (hasTimer) { hasTimer = false; this.clearTimer(timer); } if (abortSubscribed) { abortSubscribed = false; options.signal?.removeEventListener("abort", onAbort); } releaseTombstones(); }; try { unsubscribe = this.subscribe((sessions) => settle("completed", sessions)); if (settled) { cleanup(); return; } if (options.signal?.aborted) { settle("aborted"); return; } if (options.signal) { abortSubscribed = true; options.signal.addEventListener("abort", onAbort, { once: true }); } if (settled) { cleanup(); return; } timer = this.setTimer(() => settle("timed_out"), options.timeoutMs); hasTimer = true; if (settled) cleanup(); } catch (error) { cleanup(); reject(error); } }); } async send(id: string, message: string, delivery: "follow_up" | "redirect" = "follow_up"): Promise { if (isReservedChildShutdownMessage(message)) throw new Error(RESERVED_CHILD_SHUTDOWN_MESSAGE); const entry = this.sessions.get(id); if (!entry) throw new Error(`Unknown subagent session: ${id}`); if (entry.session.state !== "open") { throw new Error(`Session ${id} is ${entry.session.state} and cannot receive messages`); } if (entry.session.generation.state === "waiting_for_parent" && (delivery === "follow_up" || delivery === "redirect")) { if (entry.helpReply !== undefined) { throw new Error(`Session ${id} already has a queued help reply`); } entry.helpReply = message; this.readyQueue.push(id); this.pump(); this.notify(); return; } if (delivery === "follow_up") { if (entry.queuedFollowUp !== undefined) { throw new Error(`Session ${id} already has a queued follow-up`); } entry.queuedFollowUp = message; this.followUpQueue.push(id); this.pump(); this.notify(); return; } if (delivery !== "redirect") throw new Error(`Unknown delivery: ${delivery}`); if (entry.session.generation.state !== "running" || !this.activeIds.has(id)) { throw new Error(`Session ${id} is not running; use follow_up instead`); } if (!entry.running) throw new Error(`Session ${id} has no running child`); try { await entry.running.steer(message); } catch (error) { if (error instanceof SessionChannelFailureError) this.retainFatalChannelFailure(entry, error); throw error; } } private async open(entry: InternalSession, defaults: LaunchDefaults & { cwd: string }): Promise { try { if (!entry.launchProfile) throw new Error("Subagent launch profile is unavailable"); entry.running = await this.runner.open({ cwd: entry.session.request.cwd ?? defaults.cwd, profile: structuredClone(entry.launchProfile), accessMode: entry.session.request.writeAccess ? "write" : "read-only", launchOptions: structuredClone(entry.launchOptions), }); entry.childPotentiallyLive = true; void entry.running.closed.then( (exit) => this.handleClosed(entry, exit), (error: unknown) => this.handleClosedRejection(entry, error), ); entry.running.subscribe((event) => this.handleEvent(entry, event)); if (entry.session.state === "closing") { this.startClose(entry); } else { entry.session.state = "open"; entry.session.openedAt = this.now(); } } catch (error) { if (error instanceof TrackedSessionOpenError) { entry.childPotentiallyLive = true; void error.processExit.then( () => { entry.childPotentiallyLive = false; this.notify(); this.requestTombstonePrune(); }, () => { // A rejected observation cannot prove that the child exited. }, ); } else if (!entry.running) { entry.childPotentiallyLive = false; } const failure = this.normalizeFailure(error, entry.launchProfile); entry.session.state = "failed"; entry.session.failedAt = this.now(); entry.session.generation.state = "failed"; entry.session.errorMessage = failure.text; entry.rejectClose?.(new Error(failure.text)); } finally { entry.launchProfile = undefined; } this.pump(); this.notify(); this.requestTombstonePrune(); } private pump(): void { if (this.pumping || this.closingAll) return; this.pumping = true; try { this.moveOpenedSessionsToReadyQueue(); this.scheduleEligibleFollowUps(); while (this.activeIds.size < 4) { const id = this.readyQueue[0]; if (!id) return; const entry = this.sessions.get(id); if (!entry || entry.session.state === "failed") { this.readyQueue.shift(); continue; } if (entry.session.state !== "open" || !entry.running) return; this.readyQueue.shift(); this.activeIds.add(id); void this.promptGeneration(entry); } } finally { this.pumping = false; } } private async promptGeneration(entry: InternalSession): Promise { const { session, running } = entry; if (!running) return; const generation = session.generation; const resumingHelp = generation.state === "waiting_for_parent" && entry.helpReply !== undefined; const answeredHelpReportId = resumingHelp ? session.pendingHelpReportId : undefined; const answeredHelpQuestion = resumingHelp ? session.pendingHelpQuestion : undefined; const prompt = entry.helpReply ?? entry.generationPrompt ?? session.request.task; entry.generationPrompt = undefined; if (resumingHelp && entry.helpResumeUsage === undefined) { entry.helpResumeUsage = structuredClone(generation.usage); } if (entry.helpReportId === answeredHelpReportId) { entry.helpReportId = undefined; entry.helpQuestion = undefined; entry.helpReport = undefined; } // A parent reply beginning resumed execution starts a new run within this generation. // Reports already in the inbox stay there; only help newly reported during this run may be selected. if (resumingHelp) entry.helpSelectedGeneration = undefined; generation.state = "running"; generation.startedAt ??= this.now(); // The runner resets prompt-local captures, but a help reply resumes this manager generation. // Keep the paused result as the fallback until the resumed prompt supplies authoritative data. entry.partialResult ??= this.emptyPartialResult(); if (!resumingHelp) entry.partialResult = this.emptyPartialResult(); if (resumingHelp && entry.helpResumeUsage) { entry.partialResult.usage = structuredClone(entry.helpResumeUsage); } this.notify(); try { await running.prompt(prompt, !resumingHelp); if (resumingHelp) { entry.helpReply = undefined; if (session.pendingHelpReportId === answeredHelpReportId) { session.pendingHelpReportId = undefined; session.pendingHelpQuestion = undefined; } session.errorMessage = undefined; this.notify(); } } catch (error) { if (error instanceof SessionChannelFailureError) { this.retainFatalChannelFailure(entry, error); return; } queueMicrotask(() => { if (session.state !== "open" || session.generation !== generation || generation.state !== "running" || !this.releaseActiveSlot(entry)) return; if (resumingHelp) { entry.helpReply = undefined; if (session.pendingHelpReportId === answeredHelpReportId && entry.helpReportId === undefined) { entry.helpReportId = answeredHelpReportId; entry.helpQuestion = answeredHelpQuestion; } generation.state = "waiting_for_parent"; session.errorMessage = undefined; } else { generation.state = "failed"; session.errorMessage = this.normalizeFailure(error).text; } this.pump(); this.notify(); }); } } private handleEvent(entry: InternalSession, event: SessionEvent): void { if (entry.channelFailed) return; if (entry.session.state !== "open" || (entry.session.generation.state !== "running" && entry.session.generation.state !== "cancelling")) return; if (event.type === "report") { this.addReport(entry, event); return; } if (event.type === "progress") { entry.session.generation.activity.push(safeActivity(event)); entry.session.generation.activity.splice(0, Math.max(0, entry.session.generation.activity.length - STATUS_ACTIVITY_LIMIT)); this.notify(); return; } if (event.type === "output") { const output = truncateUtf8(event.text, CAPTURED_TEXT_MAX_BYTES); const partial = entry.partialResult ?? this.emptyPartialResult(); partial.output = output.text; partial.outputTruncation = output.truncation ?? event.truncation; entry.partialResult = partial; return; } if (event.type === "telemetry") { const partial = entry.partialResult ?? this.emptyPartialResult(); partial.usage = this.totalHelpResumeUsage(entry, event.usage); partial.model = event.model === undefined ? undefined : truncateUtf8(event.model, CAPTURED_TEXT_MAX_BYTES).text; entry.session.generation.usage = structuredClone(partial.usage); entry.session.generation.reportedModel = partial.model; entry.partialResult = partial; this.notify(); return; } if (event.type !== "settled" || !this.releaseActiveSlot(entry)) return; const settledResult = structuredClone({ ...event.result, usage: this.totalHelpResumeUsage(entry, event.result.usage), }); if (entry.helpReportId !== undefined && entry.session.generation.state !== "cancelling") { entry.session.generation.usage = structuredClone(settledResult.usage); entry.session.generation.reportedModel = settledResult.model === undefined ? undefined : truncateUtf8(settledResult.model, CAPTURED_TEXT_MAX_BYTES).text; entry.partialResult = settledResult; entry.helpResumeUsage = undefined; entry.session.pendingHelpReportId = entry.helpReportId; entry.session.pendingHelpQuestion = entry.helpQuestion; entry.session.generation.state = "waiting_for_parent"; entry.session.generation.result = undefined; entry.session.generation.resultState = "none"; const helpReport = entry.helpReport; if (helpReport !== undefined) this.emitEvent({ type: "help_waiting", report: helpReport }); this.pump(); this.notify(); return; } entry.helpReportId = undefined; entry.helpQuestion = undefined; entry.helpReport = undefined; entry.session.generation.result = settledResult; entry.helpResumeUsage = undefined; entry.session.generation.usage = structuredClone(entry.session.generation.result.usage); entry.session.generation.reportedModel = entry.session.generation.result.model === undefined ? undefined : truncateUtf8(entry.session.generation.result.model, CAPTURED_TEXT_MAX_BYTES).text; entry.session.generation.state = entry.session.generation.state === "cancelling" ? "cancelled" : "completed"; entry.session.generation.resultState = "ready"; entry.session.generation.finishedAt = this.now(); this.pump(); this.notify(); } private handleClosed(entry: InternalSession, exit: SessionExit): void { entry.childPotentiallyLive = false; if (entry.session.state === "failed" && !exit.expected) { this.releaseActiveSlot(entry); this.pump(); this.notify(); this.requestTombstonePrune(); return; } if (entry.session.state === "closing") { this.finishClose(entry); return; } if (exit.expected) { entry.session.state = "closing"; this.clearQueuedWorkForClose(entry); this.finishClose(entry); return; } this.failUnexpectedChild(entry, this.exitFailure(exit), exit); } private handleClosedRejection(entry: InternalSession, error: unknown): void { if (entry.session.state === "closing") { this.failClose(entry, error); return; } this.failUnexpectedChild(entry, this.normalizeFailure(error)); } private emptyPartialResult(): SessionResult { return { output: "", stderr: "", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }, malformedEventCount: 0, }; } private totalHelpResumeUsage(entry: InternalSession, promptUsage: UsageStats): UsageStats { const baseline = entry.helpResumeUsage; if (!baseline) return structuredClone(promptUsage); return { input: baseline.input + promptUsage.input, output: baseline.output + promptUsage.output, cacheRead: baseline.cacheRead + promptUsage.cacheRead, cacheWrite: baseline.cacheWrite + promptUsage.cacheWrite, cost: baseline.cost + promptUsage.cost, turns: baseline.turns + promptUsage.turns, }; } private retainFatalChannelFailure(entry: InternalSession, error: SessionChannelFailureError): void { if (entry.session.state !== "open" || entry.channelFailed) return; entry.channelFailed = true; entry.session.errorMessage = this.normalizeFailure(error).text; this.notify(); } private normalizeFailure(error: unknown, launchProfile?: AgentProfile): ReturnType { let message = "Child session failed"; if (error instanceof Error) message = error.message; else if (typeof error === "string") message = error; return truncateUtf8( launchProfile ? redactLaunchProfileValues(message, launchProfile) : message, CAPTURED_TEXT_MAX_BYTES, ); } private exitFailure(exit: SessionExit): ReturnType { if (exit.error) { if (exit.errorTruncation) return { text: exit.error, truncation: exit.errorTruncation }; return truncateUtf8(exit.error, CAPTURED_TEXT_MAX_BYTES); } const reason = exit.signal ? `Child session exited with signal ${exit.signal}` : `Child session exited with code ${exit.exitCode}`; return truncateUtf8(reason, CAPTURED_TEXT_MAX_BYTES); } private failUnexpectedChild( entry: InternalSession, failure: ReturnType, exit?: SessionExit, ): void { if (entry.session.state === "failed" || entry.session.state === "closed") return; const { generation } = entry.session; entry.session.state = "failed"; entry.session.failedAt = this.now(); entry.session.errorMessage = failure.text; entry.queuedFollowUp = undefined; this.clearHelp(entry); entry.generationPrompt = undefined; this.removeQueuedId(this.followUpQueue, entry.session.id); this.removeQueuedId(this.readyQueue, entry.session.id); if (generation.resultState === "none") { generation.state = "failed"; generation.finishedAt = this.now(); const partial = entry.partialResult ?? this.emptyPartialResult(); const { stderr: _stderr, errorMessage: _errorMessage, errorTruncation: _errorTruncation, stderrTruncation: _stderrTruncation, ...fallback } = partial; let stderr: ReturnType = { text: partial.stderr, truncation: partial.stderrTruncation }; if (exit && (exit.stderr.length > 0 || exit.stderrTruncation)) { stderr = exit.stderrTruncation ? { text: exit.stderr, truncation: exit.stderrTruncation } : truncateUtf8(exit.stderr, CAPTURED_TEXT_MAX_BYTES); } let error = failure; if (exit?.error !== undefined) error = this.exitFailure(exit); else if (partial.errorMessage !== undefined) { error = { text: partial.errorMessage, truncation: partial.errorTruncation }; } generation.result = { ...fallback, stderr: stderr.text, errorMessage: error.text, ...(error.truncation ? { errorTruncation: error.truncation } : {}), ...(stderr.truncation ? { stderrTruncation: stderr.truncation } : {}), }; generation.resultState = "ready"; generation.usage = structuredClone(generation.result.usage); generation.reportedModel = generation.result.model === undefined ? undefined : truncateUtf8(generation.result.model, CAPTURED_TEXT_MAX_BYTES).text; } this.emitSessionFailed(entry); this.releaseActiveSlot(entry); this.pump(); this.notify(); this.requestTombstonePrune(); } private emitSessionFailed(entry: InternalSession): void { if (entry.sessionFailureEmitted) return; entry.sessionFailureEmitted = true; const generation = entry.session.generation.number; this.emitEvent({ type: "session_failed", sessionId: safeFailureSessionId(entry.session.id), generation: Number.isSafeInteger(generation) && generation >= 0 ? generation : 0, reason: "unexpected_child_exit", partialResultReady: entry.session.generation.resultState === "ready", }); } private clearQueuedWorkForClose(entry: InternalSession): void { entry.queuedFollowUp = undefined; this.clearHelp(entry); entry.generationPrompt = undefined; this.removeQueuedId(this.followUpQueue, entry.session.id); this.removeQueuedId(this.readyQueue, entry.session.id); const generation = entry.session.generation; if (generation.resultState === "ready") { generation.resultState = "discarded"; entry.session.generation = withoutResultPreview(generation); } entry.session.generation.result = undefined; entry.partialResult = undefined; } private startClose(entry: InternalSession): void { if (entry.closeStarted || !entry.running) return; entry.closeStarted = true; const { generation, id } = entry.session; if (this.activeIds.has(id) && generation.state === "running") { generation.state = "cancelling"; try { void entry.running.abort().catch((error: unknown) => { if (entry.session.state === "closing") { entry.session.errorMessage = this.normalizeFailure(error).text; this.notify(); } }); } catch (error) { entry.session.errorMessage = this.normalizeFailure(error).text; } } try { void entry.running.close().then( () => this.finishClose(entry), (error: unknown) => this.failClose(entry, error), ); } catch (error) { this.failClose(entry, error); } } private finishClose(entry: InternalSession): void { entry.childPotentiallyLive = false; if (entry.session.state !== "closing") return; const generation = entry.session.generation; if ( generation.state === "queued" || generation.state === "running" || generation.state === "cancelling" || generation.state === "waiting_for_parent" ) { generation.state = "cancelled"; generation.finishedAt = this.now(); } if (generation.resultState === "ready") { generation.resultState = "discarded"; entry.session.generation = withoutResultPreview(generation); } entry.session.generation.result = undefined; entry.session.state = "closed"; this.releaseActiveSlot(entry); const closed = this.snapshot(entry); this.pump(); this.notify(); this.requestTombstonePrune(); entry.resolveClose?.(closed); } private failClose(entry: InternalSession, error: unknown): void { if (entry.session.state !== "closing") return; const generation = entry.session.generation; if ( generation.state === "queued" || generation.state === "running" || generation.state === "cancelling" || generation.state === "waiting_for_parent" ) { generation.state = "cancelled"; generation.finishedAt = this.now(); } if (generation.resultState === "ready") { generation.resultState = "discarded"; entry.session.generation = withoutResultPreview(generation); } entry.session.generation.result = undefined; entry.session.state = "failed"; entry.session.failedAt = this.now(); entry.session.errorMessage = this.normalizeFailure(error).text; if (!entry.childPotentiallyLive) this.releaseActiveSlot(entry); this.pump(); this.notify(); this.requestTombstonePrune(); entry.rejectClose?.(error); } private clearHelp(entry: InternalSession): void { entry.helpReply = undefined; entry.helpReportId = undefined; entry.helpResumeUsage = undefined; entry.helpQuestion = undefined; entry.helpReport = undefined; entry.helpSelectedGeneration = undefined; entry.session.pendingHelpReportId = undefined; entry.session.pendingHelpQuestion = undefined; } private releaseActiveSlot(entry: InternalSession): boolean { return this.activeIds.delete(entry.session.id); } private scheduleEligibleFollowUps(): void { const queuedIds = this.followUpQueue.splice(0); for (const id of queuedIds) { const entry = this.sessions.get(id); if (!entry || entry.queuedFollowUp === undefined || entry.session.state !== "open") continue; const generation = entry.session.generation; if ( generation.resultState === "ready" || (generation.state !== "completed" && generation.state !== "failed" && generation.state !== "cancelled") ) { this.followUpQueue.push(id); continue; } entry.reportIds.clear(); entry.session.generation = { number: generation.number + 1, state: "queued", resultState: "none", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }, activity: [], createdAt: this.now(), }; entry.partialResult = undefined; entry.helpResumeUsage = undefined; entry.helpSelectedGeneration = undefined; entry.session.errorMessage = undefined; entry.generationPrompt = entry.queuedFollowUp; entry.queuedFollowUp = undefined; this.readyQueue.push(id); } } private moveOpenedSessionsToReadyQueue(): void { while (this.nextReadyId < this.nextId) { const entry = this.sessions.get(`job-${this.nextReadyId}`); if (entry?.session.state === "opening") return; this.nextReadyId += 1; if (entry?.session.state === "open" && entry.session.generation.state === "queued") { this.readyQueue.push(entry.session.id); } } } private removeQueuedId(queue: string[], id: string): void { for (let index = queue.length - 1; index >= 0; index -= 1) { if (queue[index] === id) queue.splice(index, 1); } } private openCapacity(): number { let count = 0; for (const entry of this.sessions.values()) { if (entry.session.state === "opening" || entry.session.state === "closing" || entry.childPotentiallyLive) count += 1; } return count; } private protectTombstones(ids: readonly string[]): () => void { for (const id of ids) { this.tombstoneProtections.set(id, (this.tombstoneProtections.get(id) ?? 0) + 1); } let released = false; return () => { if (released) return; released = true; for (const id of ids) { const count = this.tombstoneProtections.get(id) ?? 0; if (count <= 1) this.tombstoneProtections.delete(id); else this.tombstoneProtections.set(id, count - 1); } this.requestTombstonePrune(); }; } private requestTombstonePrune(): void { this.prunePending = true; this.flushTombstonePrune(); } private flushTombstonePrune(): void { if (this.notifying || this.pruning) return; this.pruning = true; try { while (this.prunePending) { this.prunePending = false; const tombstones = [...this.sessions.values()].filter((entry) => !entry.childPotentiallyLive && (entry.session.state === "closed" || entry.session.state === "failed") ); let excess = tombstones.length - SESSION_TOMBSTONE_MAX_ITEMS; let removed = false; for (const entry of tombstones) { if (excess <= 0) break; if (this.tombstoneProtections.has(entry.session.id)) continue; this.sessions.delete(entry.session.id); excess -= 1; removed = true; } if (removed) this.notify(); } } finally { this.pruning = false; } } private waitForNoOpenChildren(): Promise { if (!this.hasOpenChildren()) return Promise.resolve(); return new Promise((resolve) => { let settled = false; let unsubscribe: (() => void) | undefined; const settle = () => { if (settled || this.hasOpenChildren()) return; settled = true; const stop = unsubscribe; unsubscribe = undefined; stop?.(); resolve(); }; unsubscribe = this.subscribe(settle); if (settled) { const stop = unsubscribe; unsubscribe = undefined; stop(); } }); } private waitResult( outcome: WaitOutcome, options: WaitForOptions, startedAt: number, jobs: WaitWorkStatus[], ): WaitResult { return { operation: "wait", outcome, until: options.until, timeoutMs: options.timeoutMs, elapsedMs: this.now() - startedAt, jobs, }; } private waitSnapshots( ids: readonly string[], observed?: readonly SubagentSessionSnapshot[], ): WaitWorkStatus[] { const sessions = observed ?? ids.map((id) => this.snapshot(this.requireSession(id))); const byId = new Map(sessions.map((session) => [session.id, session])); return ids.map((id) => { const session = byId.get(id); if (!session) throw new Error(`Unknown subagent session: ${id}`); return { id: session.id, generation: session.generation.number, state: session.generation.state, resultReady: session.generation.resultState === "ready", queuedFollowUp: session.queuedFollowUp, blockedByResult: session.blockedByResult, }; }); } private waitSatisfied(jobs: readonly WaitWorkStatus[], until: WaitUntil): boolean { const isSatisfied = (state: WorkState): boolean => state === "waiting_for_parent" || state === "completed" || state === "failed" || state === "cancelled"; return until === "any" ? jobs.some((job) => isSatisfied(job.state)) : jobs.every((job) => isSatisfied(job.state)); } private requireReadyResult(id: string, action: "collect" | "discard"): InternalSession { const entry = this.sessions.get(id); if (!entry) throw new Error(`Unknown subagent session: ${id}`); const generation = entry.session.generation; if (generation.resultState !== "ready" || !generation.result) { throw new Error(`Session ${id} generation ${generation.number} has no ready result to ${action}`); } return entry; } private requireSession(id: string): InternalSession { const entry = this.sessions.get(id); if (!entry) throw new Error(`Unknown subagent session: ${id}`); return entry; } private addReport(entry: InternalSession, event: Extract): void { const reportId = normalizeReportId(event.reportId); if (!entry.reportIds.add(reportId)) return; const message = truncateUtf8(event.message, REPORT_MAX_BYTES).text; const report: SubagentReport = { sessionId: entry.session.id, generation: entry.session.generation.number, reportId, kind: event.kind, message, timestamp: event.timestamp, }; const recordBytes = reportRecordBytes(report); if ( event.kind === "help_request" && entry.helpSelectedGeneration !== entry.session.generation.number && entry.helpReportId === undefined ) { entry.helpSelectedGeneration = entry.session.generation.number; entry.helpReportId = report.reportId; entry.helpQuestion = report.message; entry.helpReport = report; } const protectedHelpIds = new Set([entry.helpReportId, entry.session.pendingHelpReportId]); while ( entry.session.reports.length >= REPORT_INBOX_MAX_ITEMS || entry.session.reportBytes + recordBytes > REPORT_INBOX_MAX_BYTES ) { const progressIndex = entry.session.reports.findIndex((candidate) => candidate.kind === "progress"); const index = progressIndex >= 0 ? progressIndex : entry.session.reports.findIndex((candidate) => !protectedHelpIds.has(candidate.reportId)); if (index < 0) break; const [evicted] = entry.session.reports.splice(index, 1); if (!evicted) break; entry.session.reportBytes -= reportRecordBytes(evicted); entry.session.omittedReports += 1; } entry.session.reports.push(report); entry.session.reportBytes += recordBytes; this.emitEvent({ type: "report_added", report }); this.notify(); } private emitEvent(event: SubagentManagerEvent): void { for (const listener of this.eventSubscribers) { try { listener(structuredClone(event)); } catch { // An event listener cannot disrupt the manager or later listeners. } } } private notify(): void { this.notificationPending = true; if (this.notifying) return; this.notifying = true; try { while (this.notificationPending) { this.notificationPending = false; const sessions = this.list(); for (const listener of this.subscribers) { try { listener(structuredClone(sessions)); } catch { // A listener cannot disrupt the manager or later listeners. } } } } finally { this.notifying = false; } this.flushTombstonePrune(); } private cancellable(entry: InternalSession): boolean { if (entry.session.state !== "opening" && entry.session.state !== "open") return false; const generation = entry.session.generation; if (generation.state === "cancelling") return true; if (this.activeIds.has(entry.session.id)) return generation.state === "running" && entry.running !== undefined; if (entry.helpReply !== undefined && generation.state === "waiting_for_parent") return true; if (entry.queuedFollowUp !== undefined) return true; return generation.state === "queued"; } private snapshot(entry: InternalSession): SubagentSessionSnapshot { const { result: _result, ...generation } = entry.session.generation; return structuredClone({ ...entry.session, generation, queuedFollowUp: entry.queuedFollowUp !== undefined, blockedByResult: entry.queuedFollowUp !== undefined && entry.session.generation.resultState === "ready", cancellable: this.cancellable(entry), }); } }