fix: retire one-shot subagent MCP runtimes

This commit is contained in:
Peter Steinberger
2026-04-23 02:30:46 +01:00
parent dcff528805
commit ccf2e77e8d
8 changed files with 92 additions and 9 deletions

View File

@@ -79,8 +79,8 @@ async function disposeSession(session: BundleMcpSession) {
if (session.transportType === "streamable-http") {
await (session.transport as StreamableHTTPClientTransport).terminateSession().catch(() => {});
}
await session.client.close().catch(() => {});
await session.transport.close().catch(() => {});
await session.client.close().catch(() => {});
}
function createCatalogFingerprint(servers: Record<string, unknown>): string {
@@ -454,6 +454,23 @@ export async function retireSessionMcpRuntime(params: {
}
}
export async function retireSessionMcpRuntimeForSessionKey(params: {
sessionKey?: string | null;
reason: string;
onError?: (error: unknown, sessionId: string, reason: string) => void;
}): Promise<boolean> {
const sessionKey = normalizeOptionalString(params.sessionKey);
if (!sessionKey) {
return false;
}
const sessionId = getSessionMcpRuntimeManager().resolveSessionId(sessionKey);
return await retireSessionMcpRuntime({
sessionId,
reason: params.reason,
onError: params.onError,
});
}
export async function disposeAllSessionMcpRuntimes(): Promise<void> {
await getSessionMcpRuntimeManager().disposeAll();
}

View File

@@ -14,6 +14,7 @@ export {
getOrCreateSessionMcpRuntime,
getSessionMcpRuntimeManager,
retireSessionMcpRuntime,
retireSessionMcpRuntimeForSessionKey,
} from "./pi-bundle-mcp-runtime.js";
export {
createBundleMcpToolRuntime,

View File

@@ -50,7 +50,10 @@ import {
} from "../model-auth.js";
import { normalizeProviderId } from "../model-selection.js";
import { ensureOpenClawModelsJson } from "../models-config.js";
import { retireSessionMcpRuntime } from "../pi-bundle-mcp-tools.js";
import {
retireSessionMcpRuntime,
retireSessionMcpRuntimeForSessionKey,
} from "../pi-bundle-mcp-tools.js";
import {
classifyFailoverReason,
extractObservedOverflowTokenCount,
@@ -2131,15 +2134,23 @@ export async function runEmbeddedPiAgent(
await contextEngine.dispose?.();
stopRuntimeAuthRefreshTimer();
if (params.cleanupBundleMcpOnRunEnd === true) {
await retireSessionMcpRuntime({
sessionId: params.sessionId,
const onError = (error: unknown, sessionId: string) => {
log.warn(
`bundle-mcp cleanup failed after run for ${sessionId}: ${formatErrorMessage(error)}`,
);
};
const retiredBySessionKey = await retireSessionMcpRuntimeForSessionKey({
sessionKey: params.sessionKey,
reason: "embedded-run-end",
onError: (error, sessionId) => {
log.warn(
`bundle-mcp cleanup failed after run for ${sessionId}: ${formatErrorMessage(error)}`,
);
},
onError,
});
if (!retiredBySessionKey) {
await retireSessionMcpRuntime({
sessionId: params.sessionId,
reason: "embedded-run-end",
onError,
});
}
}
}
});

View File

@@ -9,6 +9,7 @@ import {
setDetachedTaskDeliveryStatusByRunId,
} from "../tasks/detached-task-runtime.js";
import { normalizeDeliveryContext } from "../utils/delivery-context.shared.js";
import { retireSessionMcpRuntimeForSessionKey } from "./pi-bundle-mcp-tools.js";
import { type SubagentRunOutcome, withSubagentOutcomeTiming } from "./subagent-announce-output.js";
import {
SUBAGENT_ENDED_REASON_COMPLETE,
@@ -383,6 +384,20 @@ export function createSubagentRegistryLifecycleController(params: {
cleanup: "delete" | "keep";
completedAt: number;
}) => {
if (cleanupParams.entry.spawnMode !== "session") {
void retireSessionMcpRuntimeForSessionKey({
sessionKey: cleanupParams.entry.childSessionKey,
reason: "subagent-run-cleanup",
onError: (error, sessionId) => {
params.warn("failed to retire subagent bundle MCP runtime", {
error: buildSafeLifecycleErrorMeta(error),
sessionId,
runId: maskRunId(cleanupParams.runId),
childSessionKey: maskSessionKey(cleanupParams.entry.childSessionKey),
});
},
});
}
if (cleanupParams.cleanup === "delete") {
params.clearPendingLifecycleError(cleanupParams.runId);
void params.notifyContextEngineSubagentEnded({
@@ -405,6 +420,28 @@ export function createSubagentRegistryLifecycleController(params: {
retryDeferredCompletedAnnounces(cleanupParams.runId);
};
const retireRunModeBundleMcpRuntime = (cleanupParams: {
runId: string;
entry: SubagentRunRecord;
reason: string;
}) => {
if (cleanupParams.entry.spawnMode === "session") {
return;
}
void retireSessionMcpRuntimeForSessionKey({
sessionKey: cleanupParams.entry.childSessionKey,
reason: cleanupParams.reason,
onError: (error, sessionId) => {
params.warn("failed to retire subagent bundle MCP runtime", {
error: buildSafeLifecycleErrorMeta(error),
sessionId,
runId: maskRunId(cleanupParams.runId),
childSessionKey: maskSessionKey(cleanupParams.entry.childSessionKey),
});
},
});
};
const finalizeSubagentCleanup = async (
runId: string,
cleanup: "delete" | "keep",
@@ -689,6 +726,12 @@ export function createSubagentRegistryLifecycleController(params: {
onWarn: (msg) => params.warn(msg, { runId: entry.runId }),
});
retireRunModeBundleMcpRuntime({
runId: completeParams.runId,
entry,
reason: "subagent-run-complete",
});
startSubagentAnnounceCleanupFlow(completeParams.runId, entry);
};

View File

@@ -750,6 +750,7 @@ export async function spawnSubagentDirect(
idempotencyKey: childIdem,
deliver: deliverInitialChildRunDirectly,
lane: AGENT_LANE_SUBAGENT,
cleanupBundleMcpOnRunEnd: spawnMode !== "session",
extraSystemPrompt: childSystemPrompt,
thinking: thinkingOverride,
timeout: runTimeoutSeconds,

View File

@@ -2,6 +2,7 @@ import crypto from "node:crypto";
import { callGateway } from "../../gateway/call.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { resolveNestedAgentLaneForSession } from "../lanes.js";
import { retireSessionMcpRuntimeForSessionKey } from "../pi-bundle-mcp-tools.js";
import { waitForAgentRunAndReadUpdatedAssistantReply } from "../run-wait.js";
export { readLatestAssistantReply } from "../run-wait.js";
@@ -55,6 +56,12 @@ export async function runAgentStep(params: {
sessionKey: params.sessionKey,
timeoutMs: Math.min(params.timeoutMs, 60_000),
});
if (result.status === "ok" || result.status === "error") {
await retireSessionMcpRuntimeForSessionKey({
sessionKey: params.sessionKey,
reason: "nested-agent-step-complete",
});
}
if (result.status !== "ok") {
return undefined;
}

View File

@@ -150,6 +150,7 @@ export const AgentParamsSchema = Type.Object(
timeout: Type.Optional(Type.Integer({ minimum: 0 })),
bestEffortDeliver: Type.Optional(Type.Boolean()),
lane: Type.Optional(Type.String()),
cleanupBundleMcpOnRunEnd: Type.Optional(Type.Boolean()),
extraSystemPrompt: Type.Optional(Type.String()),
bootstrapContextMode: Type.Optional(
Type.Union([Type.Literal("full"), Type.Literal("lightweight")]),

View File

@@ -351,6 +351,7 @@ export const agentHandlers: GatewayRequestHandlers = {
idempotencyKey: string;
timeout?: number;
bestEffortDeliver?: boolean;
cleanupBundleMcpOnRunEnd?: boolean;
label?: string;
inputProvenance?: InputProvenance;
};
@@ -946,6 +947,7 @@ export const agentHandlers: GatewayRequestHandlers = {
messageChannel: originMessageChannel,
runId,
lane: request.lane,
cleanupBundleMcpOnRunEnd: request.cleanupBundleMcpOnRunEnd === true,
extraSystemPrompt: request.extraSystemPrompt,
bootstrapContextMode: request.bootstrapContextMode,
bootstrapContextRunKind: request.bootstrapContextRunKind,