diff --git a/src/agents/subagent-control.ts b/src/agents/subagent-control.ts index 59bfec09231d..bcff33d21646 100644 --- a/src/agents/subagent-control.ts +++ b/src/agents/subagent-control.ts @@ -340,17 +340,19 @@ async function killSubagentRun(params: { targetState?: SubagentKillTargetState; error?: string; }> { + const markKilledBestEffort = () => + markSubagentRunTerminatedBestEffort({ + runId: params.entry.runId, + reason: "killed", + suppressTaskDelivery: params.suppressTaskDelivery, + }); const initialTargetState = resolveSubagentKillTargetState(params.entry); if (initialTargetState) { if ( params.entry.endedReason === SUBAGENT_ENDED_REASON_KILLED && params.entry.suppressAnnounceReason !== "steer-restart" ) { - markSubagentRunTerminatedBestEffort({ - runId: params.entry.runId, - reason: "killed", - suppressTaskDelivery: params.suppressTaskDelivery, - }); + markKilledBestEffort(); } return { killed: false, targetState: initialTargetState }; } @@ -399,11 +401,7 @@ async function killSubagentRun(params: { params.entry.endedReason === SUBAGENT_ENDED_REASON_KILLED && params.entry.suppressAnnounceReason !== "steer-restart" ) { - markSubagentRunTerminatedBestEffort({ - runId: params.entry.runId, - reason: "killed", - suppressTaskDelivery: params.suppressTaskDelivery, - }); + markKilledBestEffort(); } return { killed: false, sessionId, targetState: targetStateAfterRuntimeLoad }; } @@ -535,11 +533,7 @@ async function killSubagentRun(params: { targetState.task.status === "cancelled" && targetState.task.error === SUBAGENT_KILL_TASK_ERROR; if (killedTarget) { - markSubagentRunTerminatedBestEffort({ - runId: params.entry.runId, - reason: "killed", - suppressTaskDelivery: params.suppressTaskDelivery, - }); + markKilledBestEffort(); } else { try { releaseSubagentRunKillClaim({ @@ -1166,8 +1160,8 @@ export async function steerControlledSubagentRun(params: { // chat.abort remains the primary cleanup; exact session deletion is only // the fallback when the accepted session row can be resolved. } - if (!isAgentEventLifecycleGenerationCurrent(steerLifecycleGeneration)) { - await terminateAcceptedCollectorRun({ + const terminateUnownedSteer = () => + terminateAcceptedCollectorRun({ childSessionKey: params.entry.childSessionKey, gatewayRunId: runId, expectedSessionId: acceptedSessionEntry?.sessionId, @@ -1175,6 +1169,8 @@ export async function steerControlledSubagentRun(params: { callGateway: subagentControlDeps.callGateway, timeoutMs: 10_000, }); + if (!isAgentEventLifecycleGenerationCurrent(steerLifecycleGeneration)) { + await terminateUnownedSteer(); clearSubagentRunSteerRestart(params.entry.runId, currentEntry); return { status: "error", @@ -1197,14 +1193,7 @@ export async function steerControlledSubagentRun(params: { task: params.message, }); if (!replaced) { - await terminateAcceptedCollectorRun({ - childSessionKey: params.entry.childSessionKey, - gatewayRunId: runId, - expectedSessionId: acceptedSessionEntry?.sessionId, - expectedLifecycleRevision: acceptedSessionEntry?.lifecycleRevision, - callGateway: subagentControlDeps.callGateway, - timeoutMs: 10_000, - }); + await terminateUnownedSteer(); clearSubagentRunSteerRestart(params.entry.runId, currentEntry); return { status: "error", diff --git a/src/agents/subagent-registry-lifecycle-cleanup.ts b/src/agents/subagent-registry-lifecycle-cleanup.ts index 51b6abd39845..01291cca0bf5 100644 --- a/src/agents/subagent-registry-lifecycle-cleanup.ts +++ b/src/agents/subagent-registry-lifecycle-cleanup.ts @@ -73,60 +73,75 @@ export function createSubagentRegistryLifecycleCleanup( entry.endedReason === SUBAGENT_ENDED_REASON_COMPLETE && entry.execution.outcome?.status === "ok"; - const finalizeResumedAnnounceGiveUp = async (giveUpParams: { + const finalizeAnnounceGiveUp = async (giveUpParams: { runId: string; entry: SubagentRunRecord; reason: "expiry" | "permanent_failure"; + cleanup?: "delete" | "keep"; + cleanupGeneration?: number; + retryCount?: number; + completedAt?: number; }) => { - if (shouldSuspendPendingFinalDelivery(giveUpParams.entry)) { + const { runId, entry, reason, cleanup, cleanupGeneration, retryCount, completedAt } = + giveUpParams; + if (shouldSuspendPendingFinalDelivery(entry)) { suspendPendingFinalDelivery({ - runId: giveUpParams.runId, - entry: giveUpParams.entry, - reason: giveUpParams.reason, - error: getDeliveryLastError(giveUpParams.entry), + runId, + entry, + reason, + error: getDeliveryLastError(entry), }); return; } - const deliveryError = getDeliveryLastError(giveUpParams.entry) ?? giveUpParams.reason; - clearPendingFinalDelivery(giveUpParams.entry); - const failedDelivery = ensureDeliveryState(giveUpParams.entry); + const deliveryError = getDeliveryLastError(entry) ?? reason; + clearPendingFinalDelivery(entry); + const failedDelivery = ensureDeliveryState(entry); failedDelivery.status = "failed"; failedDelivery.lastError = deliveryError; + if (retryCount != null) { + failedDelivery.attemptCount = retryCount; + failedDelivery.lastAttemptAt = completedAt ?? Date.now(); + } safeSetSubagentTaskDeliveryStatus({ - entry: giveUpParams.entry, + entry, deliveryStatus: "failed", deliveryError, }); safeMarkRequiredCompletionDeliveryBlocked({ - entry: giveUpParams.entry, + entry, reason: deliveryError, }); - giveUpParams.entry.wakeOnDescendantSettle = undefined; - const completion = ensureCompletionState(giveUpParams.entry); + entry.wakeOnDescendantSettle = undefined; + const completion = ensureCompletionState(entry); completion.fallbackResultText = undefined; completion.fallbackCapturedAt = undefined; - const shouldDeleteAttachments = - giveUpParams.entry.cleanup === "delete" || !giveUpParams.entry.retainAttachmentsOnKeep; - if (shouldDeleteAttachments) { - await safeRemoveAttachmentsDir(giveUpParams.entry); + if ((cleanup ?? entry.cleanup) === "delete" || !entry.retainAttachmentsOnKeep) { + await safeRemoveAttachmentsDir(entry); } - const completionReason = resolveCleanupCompletionReason(giveUpParams.entry); - logAnnounceGiveUp(giveUpParams.entry, giveUpParams.reason); + if ( + cleanupGeneration !== undefined && + !isCleanupAttemptCurrent(runId, entry, cleanupGeneration) + ) { + await retireSupersededCleanupIfNeeded(runId, entry, cleanupGeneration); + return; + } + const completionReason = resolveCleanupCompletionReason(entry); + logAnnounceGiveUp(entry, reason); // Retry-limit / expiry give-up should not leave cleanup stuck behind the // best-effort ended hook. Mark the run cleaned first, then fire the hook. completeCleanupBookkeeping({ - runId: giveUpParams.runId, - entry: giveUpParams.entry, - cleanup: giveUpParams.entry.cleanup, - completedAt: Date.now(), + runId, + entry, + cleanup: cleanup ?? entry.cleanup, + completedAt: completedAt ?? Date.now(), }); - if (!shouldSuppressSubagentRecoverySessionEffects(giveUpParams.entry)) { + if (!shouldSuppressSubagentRecoverySessionEffects(entry)) { await emitCompletionEndedHookIfNeeded( - giveUpParams.entry, + entry, completionReason, () => - isEndedHookOwnerCurrent(giveUpParams.runId, giveUpParams.entry) && - !shouldSuppressSubagentRecoverySessionEffects(giveUpParams.entry), + isEndedHookOwnerCurrent(runId, entry) && + !shouldSuppressSubagentRecoverySessionEffects(entry), ); } }; @@ -159,7 +174,7 @@ export function createSubagentRegistryLifecycleCleanup( entry, cleanupGeneration: cleanupGenerations.get(entry)!, run: async () => { - await finalizeResumedAnnounceGiveUp({ + await finalizeAnnounceGiveUp({ runId, entry, reason: "expiry", @@ -326,64 +341,15 @@ export function createSubagentRegistryLifecycleCleanup( } if (deferredDecision.kind === "give-up") { - if (shouldSuspendPendingFinalDelivery(entry)) { - suspendPendingFinalDelivery({ - runId, - entry, - reason: deferredDecision.reason, - error: getDeliveryLastError(entry), - }); - return; - } - const deliveryError = getDeliveryLastError(entry) ?? deferredDecision.reason; - clearPendingFinalDelivery(entry); - const failedDelivery = ensureDeliveryState(entry); - failedDelivery.status = "failed"; - failedDelivery.lastError = deliveryError; - if (deferredDecision.retryCount != null) { - failedDelivery.attemptCount = deferredDecision.retryCount; - failedDelivery.lastAttemptAt = now; - } - safeSetSubagentTaskDeliveryStatus({ - entry, - deliveryStatus: "failed", - deliveryError, - }); - safeMarkRequiredCompletionDeliveryBlocked({ - entry, - reason: deliveryError, - }); - entry.wakeOnDescendantSettle = undefined; - const completion = ensureCompletionState(entry); - completion.fallbackResultText = undefined; - completion.fallbackCapturedAt = undefined; - const shouldDeleteAttachments = cleanup === "delete" || !entry.retainAttachmentsOnKeep; - if (shouldDeleteAttachments) { - await safeRemoveAttachmentsDir(entry); - } - if (!isCleanupAttemptCurrent(runId, entry, cleanupGeneration)) { - await retireSupersededCleanupIfNeeded(runId, entry, cleanupGeneration); - return; - } - const completionReason = resolveCleanupCompletionReason(entry); - logAnnounceGiveUp(entry, deferredDecision.reason); - // Giving up on announce delivery is terminal for cleanup even if the - // best-effort hook is still resolving. - completeCleanupBookkeeping({ + await finalizeAnnounceGiveUp({ runId, entry, + reason: deferredDecision.reason, cleanup, + cleanupGeneration, + retryCount: deferredDecision.retryCount, completedAt: now, }); - if (!shouldSuppressSubagentRecoverySessionEffects(entry)) { - await emitCompletionEndedHookIfNeeded( - entry, - completionReason, - () => - isEndedHookOwnerCurrent(runId, entry) && - !shouldSuppressSubagentRecoverySessionEffects(entry), - ); - } return; } @@ -642,7 +608,7 @@ export function createSubagentRegistryLifecycleCleanup( return { completeCleanupBookkeeping, - finalizeResumedAnnounceGiveUp, + finalizeResumedAnnounceGiveUp: finalizeAnnounceGiveUp, retireRunModeBundleMcpRuntime, startSubagentAnnounceCleanupFlow, }; diff --git a/src/agents/subagent-registry-restore.ts b/src/agents/subagent-registry-restore.ts index 82a4f8e3920d..3fdcdf76cb3f 100644 --- a/src/agents/subagent-registry-restore.ts +++ b/src/agents/subagent-registry-restore.ts @@ -16,7 +16,7 @@ import { } from "./subagent-registry-helpers.js"; import type { createSubagentRegistryLifecycleController } from "./subagent-registry-lifecycle.js"; import type { SubagentRunRecord } from "./subagent-registry.types.js"; -import { isSessionLifecycleChangedGatewayError } from "./subagent-session-cleanup.js"; +import { deleteSubagentSessionForCleanup } from "./subagent-session-cleanup.js"; import { loadSubagentSessionEntry, type SubagentSessionStoreCache, @@ -327,27 +327,18 @@ export function createSubagentRegistryRestorer(config: { if (!ownsCleanup()) { return false; } - try { - await deps().callGateway({ - method: "sessions.delete", - params: { - key: entry.childSessionKey, - deleteTranscript: true, - expectedSessionId, - expectedLifecycleRevision, - emitLifecycleHooks: false, - }, - timeoutMs: 10_000, - }); - sessionDeleted = true; - return true; - } catch (cleanupError) { - if (isSessionLifecycleChangedGatewayError(cleanupError)) { - sessionOwnershipChanged = true; - return true; - } - throw cleanupError; - } + const outcome = await deleteSubagentSessionForCleanup({ + callGateway: deps().callGateway, + childSessionKey: entry.childSessionKey, + expectedSessionId, + expectedLifecycleRevision, + onError: (cleanupError) => { + throw cleanupError; + }, + }); + sessionDeleted = outcome === "deleted"; + sessionOwnershipChanged = outcome === "changed"; + return outcome !== "failed"; }, { shouldRetry: () => !launchTerminationConfirmed && ownsCleanup(), diff --git a/src/agents/subagent-registry-sweeper.ts b/src/agents/subagent-registry-sweeper.ts index 4adf2720b8cd..7b202391c67d 100644 --- a/src/agents/subagent-registry-sweeper.ts +++ b/src/agents/subagent-registry-sweeper.ts @@ -31,7 +31,7 @@ import type { SubagentRunRecord, } from "./subagent-registry.types.js"; import { isStaleUnendedSubagentRun } from "./subagent-run-liveness.js"; -import { isSessionLifecycleChangedGatewayError } from "./subagent-session-cleanup.js"; +import { deleteSubagentSessionForCleanup } from "./subagent-session-cleanup.js"; import { loadSubagentSessionEntry, resolveCompletionFromSessionEntry, @@ -210,25 +210,20 @@ export function createSubagentRegistrySweeper(params: { childSessionKey: string, identity: FrozenSessionIdentity, ): Promise<"deleted" | "changed"> { - try { - await params.callGateway({ - method: "sessions.delete", - params: { - key: childSessionKey, - deleteTranscript: true, - emitLifecycleHooks: false, - expectedSessionId: identity.sessionId, - expectedLifecycleRevision: identity.lifecycleRevision, - }, - timeoutMs: 10_000, - }); - return "deleted"; - } catch (error) { - if (isSessionLifecycleChangedGatewayError(error)) { - return "changed"; - } - throw error; + let failure: unknown; + const outcome = await deleteSubagentSessionForCleanup({ + callGateway: params.callGateway, + childSessionKey, + expectedSessionId: identity.sessionId, + expectedLifecycleRevision: identity.lifecycleRevision, + onError: (error) => { + failure = error; + }, + }); + if (outcome === "failed") { + throw failure; } + return outcome; } const sweptContext = (entry: SubagentRunRecord) => ({ diff --git a/src/agents/subagent-session-cleanup.ts b/src/agents/subagent-session-cleanup.ts index e17b86a60856..6ff5ea48000f 100644 --- a/src/agents/subagent-session-cleanup.ts +++ b/src/agents/subagent-session-cleanup.ts @@ -3,13 +3,13 @@ * the gateway and preserves lifecycle-hook behavior for session-mode spawns. */ import { SESSION_LIFECYCLE_CHANGED_ERROR_REASON } from "../config/sessions/lifecycle.js"; -import type { callGateway as defaultCallGateway } from "../gateway/call.js"; +import type { callGateway } from "../gateway/call.js"; import type { SpawnSubagentMode } from "./subagent-spawn.types.js"; -type CallGateway = typeof defaultCallGateway; +type CallGateway = (options: Parameters[0]) => Promise; type SubagentSessionCleanupOutcome = "deleted" | "changed" | "failed"; -export function isSessionLifecycleChangedGatewayError(error: unknown): boolean { +function isSessionLifecycleChangedGatewayError(error: unknown): boolean { if (!(error instanceof Error) || error.name !== "GatewayClientRequestError") { return false; } @@ -28,8 +28,11 @@ export async function deleteSubagentSessionForCleanup(params: { callGateway: CallGateway; childSessionKey: string; spawnMode?: SpawnSubagentMode; + emitLifecycleHooks?: boolean; + deleteTranscript?: boolean; expectedSessionId?: string; expectedLifecycleRevision?: string; + timeoutMs?: number; onError?: (error: unknown) => void; }): Promise { if (!params.expectedSessionId || !params.expectedLifecycleRevision) { @@ -40,14 +43,12 @@ export async function deleteSubagentSessionForCleanup(params: { method: "sessions.delete", params: { key: params.childSessionKey, - deleteTranscript: true, - emitLifecycleHooks: params.spawnMode === "session", - ...(params.expectedSessionId ? { expectedSessionId: params.expectedSessionId } : {}), - ...(params.expectedLifecycleRevision - ? { expectedLifecycleRevision: params.expectedLifecycleRevision } - : {}), + deleteTranscript: params.deleteTranscript ?? true, + emitLifecycleHooks: params.emitLifecycleHooks ?? params.spawnMode === "session", + expectedSessionId: params.expectedSessionId, + expectedLifecycleRevision: params.expectedLifecycleRevision, }, - timeoutMs: 10_000, + timeoutMs: params.timeoutMs ?? 10_000, }); return "deleted"; } catch (error) { diff --git a/src/agents/subagent-spawn-cleanup.ts b/src/agents/subagent-spawn-cleanup.ts index 511d8dadd558..b01a5d57a1c8 100644 --- a/src/agents/subagent-spawn-cleanup.ts +++ b/src/agents/subagent-spawn-cleanup.ts @@ -1,27 +1,11 @@ import { promises as fs } from "node:fs"; -import { SESSION_LIFECYCLE_CHANGED_ERROR_REASON } from "../config/sessions/lifecycle.js"; import type { callGateway } from "../gateway/call.js"; import { isFastTestRuntimeEnv } from "../infra/env.js"; +import { deleteSubagentSessionForCleanup } from "./subagent-session-cleanup.js"; import { callSubagentGateway } from "./subagent-spawn-gateway.js"; const SUBAGENT_CONTROL_GATEWAY_TIMEOUT_MS = 60_000; type GatewayCall = (options: Parameters[0]) => Promise; -type SessionCleanupOutcome = "deleted" | "changed" | "failed"; - -function isSessionLifecycleChangedGatewayError(error: unknown): boolean { - if (!(error instanceof Error) || error.name !== "GatewayClientRequestError") { - return false; - } - const requestError = error as Error & { gatewayCode?: unknown; details?: unknown }; - const details = requestError.details; - return ( - requestError.gatewayCode === "INVALID_REQUEST" && - typeof details === "object" && - details !== null && - (details as { reason?: unknown }).reason === SESSION_LIFECYCLE_CHANGED_ERROR_REASON - ); -} - function isMatchingAbortResponse(response: unknown, gatewayRunId: string): boolean { if (!response || typeof response !== "object") { return false; @@ -65,35 +49,17 @@ type SessionCleanupOptions = { timeoutMs?: number; }; -async function requestProvisionalSessionCleanup( +function requestProvisionalSessionCleanup( childSessionKey: string, options?: SessionCleanupOptions, -): Promise { - if (!options?.expectedSessionId || !options.expectedLifecycleRevision) { - return "failed"; - } - try { - await (options?.callGateway ?? callSubagentGateway)({ - method: "sessions.delete", - params: { - key: childSessionKey, - emitLifecycleHooks: options?.emitLifecycleHooks === true, - deleteTranscript: options?.deleteTranscript === true, - ...(options?.expectedSessionId ? { expectedSessionId: options.expectedSessionId } : {}), - ...(options?.expectedLifecycleRevision - ? { expectedLifecycleRevision: options.expectedLifecycleRevision } - : {}), - }, - timeoutMs: options?.timeoutMs ?? SUBAGENT_CONTROL_GATEWAY_TIMEOUT_MS, - }); - return "deleted"; - } catch (error) { - if (isSessionLifecycleChangedGatewayError(error)) { - return "changed"; - } - // Best-effort cleanup only. - return "failed"; - } +) { + return deleteSubagentSessionForCleanup({ + ...options, + childSessionKey, + callGateway: options?.callGateway ?? callSubagentGateway, + deleteTranscript: options?.deleteTranscript === true, + timeoutMs: options?.timeoutMs ?? SUBAGENT_CONTROL_GATEWAY_TIMEOUT_MS, + }); } export async function cleanupProvisionalSession( @@ -125,30 +91,21 @@ export async function cleanupFailedSpawnBeforeAgentStart(params: { expectedSessionId?: string; expectedLifecycleRevision?: string; }): Promise<{ attachmentsRemoved: boolean; sessionDeleted: boolean }> { + const { childSessionKey, attachmentAbsDir, waitForSessionDeletion, ...sessionCleanupOptions } = + params; let attachmentsRemoved = true; - if (params.attachmentAbsDir) { + if (attachmentAbsDir) { try { - await fs.rm(params.attachmentAbsDir, { recursive: true, force: true }); + await fs.rm(attachmentAbsDir, { recursive: true, force: true }); } catch { attachmentsRemoved = false; } } - const sessionCleanupOptions = { - emitLifecycleHooks: params.emitLifecycleHooks, - deleteTranscript: params.deleteTranscript, - expectedSessionId: params.expectedSessionId, - expectedLifecycleRevision: params.expectedLifecycleRevision, - }; - if (params.waitForSessionDeletion) { - const sessionDeleted = await waitForProvisionalSessionDeletion( - params.childSessionKey, - sessionCleanupOptions, - ); - return { attachmentsRemoved, sessionDeleted }; - } return { attachmentsRemoved, - sessionDeleted: await cleanupProvisionalSession(params.childSessionKey, sessionCleanupOptions), + sessionDeleted: await ( + waitForSessionDeletion ? waitForProvisionalSessionDeletion : cleanupProvisionalSession + )(childSessionKey, sessionCleanupOptions), }; } diff --git a/src/agents/subagent-spawn.ts b/src/agents/subagent-spawn.ts index 0785e7b88099..2f3f597354a3 100644 --- a/src/agents/subagent-spawn.ts +++ b/src/agents/subagent-spawn.ts @@ -202,6 +202,12 @@ export async function spawnSubagentDirect( expectedSessionId: initialSession.entry?.sessionId, expectedLifecycleRevision: initialSession.entry?.lifecycleRevision, }; + const cleanupCreatedSession = (emitLifecycleHooks = false) => + cleanupProvisionalSession(childSessionKey, { + emitLifecycleHooks, + deleteTranscript: true, + ...provisionalSessionIdentity, + }); const preparedSpawnContext = await prepareSubagentSessionContext({ cfg, contextMode, @@ -211,11 +217,7 @@ export async function spawnSubagentDirect( childSessionKey, }); if (preparedSpawnContext.status === "error") { - await cleanupProvisionalSession(childSessionKey, { - emitLifecycleHooks: false, - deleteTranscript: true, - ...provisionalSessionIdentity, - }); + await cleanupCreatedSession(); return { status: "error", error: preparedSpawnContext.error, @@ -229,11 +231,7 @@ export async function spawnSubagentDirect( resolvedModel, }); if (runtimeModelPersistError) { - await cleanupProvisionalSession(childSessionKey, { - emitLifecycleHooks: false, - deleteTranscript: true, - ...provisionalSessionIdentity, - }); + await cleanupCreatedSession(); return { status: "error", error: runtimeModelPersistError, @@ -258,11 +256,7 @@ export async function spawnSubagentDirect( }, }); if (bindResult.status === "error") { - await cleanupProvisionalSession(childSessionKey, { - emitLifecycleHooks: false, - deleteTranscript: true, - ...provisionalSessionIdentity, - }); + await cleanupCreatedSession(); return { status: "error", error: bindResult.error, @@ -316,11 +310,7 @@ export async function spawnSubagentDirect( mountPathHint, }); if (materializedAttachments && materializedAttachments.status !== "ok") { - await cleanupProvisionalSession(childSessionKey, { - emitLifecycleHooks: threadBindingReady, - deleteTranscript: true, - ...provisionalSessionIdentity, - }); + await cleanupCreatedSession(threadBindingReady); return { status: materializedAttachments.status, error: materializedAttachments.error, @@ -400,6 +390,15 @@ export async function spawnSubagentDirect( spawnMode, resolvedModelMetadata, }); + const cleanupFailedSpawn = (waitForSessionDeletion?: boolean) => + cleanupFailedSpawnBeforeAgentStart({ + childSessionKey, + attachmentAbsDir, + emitLifecycleHooks: threadBindingReady, + deleteTranscript: true, + ...provisionalSessionIdentity, + waitForSessionDeletion, + }); type SubagentBackendState = { contextEnginePreparation?: SubagentSpawnPreparation }; const adapter: SpawnBackendAdapter = { async initialize() { @@ -427,13 +426,7 @@ export async function spawnSubagentDirect( }, async cleanupOnFailure({ phase, state }) { if (phase === "initialize") { - await cleanupFailedSpawnBeforeAgentStart({ - childSessionKey, - attachmentAbsDir, - emitLifecycleHooks: threadBindingReady, - deleteTranscript: true, - ...provisionalSessionIdentity, - }); + await cleanupFailedSpawn(); return; } await rollbackPreparedContextEngine(state?.contextEnginePreparation); @@ -473,11 +466,7 @@ export async function spawnSubagentDirect( } emitLifecycleHooks = !endedHookEmitted; } - await cleanupProvisionalSession(childSessionKey, { - emitLifecycleHooks, - deleteTranscript: true, - ...provisionalSessionIdentity, - }); + await cleanupCreatedSession(emitLifecycleHooks); }, }; const pipelineResult = await runSpawnPipeline({ @@ -584,16 +573,11 @@ export async function spawnSubagentDirect( const launchError = summarizeSpawnError(error); const [contextRollback, sessionCleanup] = await Promise.allSettled([ rollbackPreparedContextEngine(pipelineResult.state.contextEnginePreparation), - cleanupFailedSpawnBeforeAgentStart({ - childSessionKey, - attachmentAbsDir, - emitLifecycleHooks: threadBindingReady, - deleteTranscript: true, - ...provisionalSessionIdentity, + cleanupFailedSpawn( // A launch RPC can fail after acceptance. Keep the FIFO slot until // deleting the child session proves no accepted run remains active. - waitForSessionDeletion: !launchTerminationConfirmed, - }), + !launchTerminationConfirmed, + ), ]); await retrySubagentCleanup(async () => { settleFailedQueuedSubagentLaunch(childRunId, launchError);