mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(agents): unify guarded subagent lifecycle cleanup (#119637)
This commit is contained in:
committed by
GitHub
parent
9b2328dc37
commit
40a3bd2e10
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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<typeof callGateway>[0]) => Promise<unknown>;
|
||||
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<SubagentSessionCleanupOutcome> {
|
||||
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) {
|
||||
|
||||
@@ -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<typeof callGateway>[0]) => Promise<unknown>;
|
||||
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<SessionCleanupOutcome> {
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<SubagentBackendState> = {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user