mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(cron): retire bundled mcp runtimes
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Config/gateway: recover configs accidentally prefixed with non-JSON output during gateway startup or `openclaw doctor --fix`, preserving the clobbered file as a backup while leaving normal config reads read-only.
|
||||
- Pi embedded runs: pass real built-in tools into Pi session creation and then narrow active tool names after custom tool registration, so the runner and compaction paths compile cleanly and keep OpenClaw-managed custom tool allowlists without feeding string arrays into `createAgentSession`. Thanks @vincentkoc.
|
||||
- Agents/OpenAI websocket: route native OpenAI websocket metadata and session-header decisions through the shared endpoint classifier so local mocks and custom `models.providers.openai.baseUrl` endpoints stay out of the native OpenAI path consistently across embedded-runner and websocket transport code. Thanks @vincentkoc.
|
||||
- Cron/MCP: retire bundled MCP runtimes through one shared cleanup path for isolated cron run ends, persistent cron session rollover, and direct cron `deleteAfterRun` fallback cleanup. Fixes #69145, #68623, and #68827.
|
||||
- MCP/gateway: tear down stdio MCP process trees on transport close and dispose bundled MCP runtimes during session delete/reset, preventing orphaned wrapper/server processes from accumulating. Fixes #68809 and #69465.
|
||||
- Config: render validation warnings with real line breaks instead of a literal `\n` sequence in CLI/audit output. Fixes #70140.
|
||||
- Cron/doctor: repair malformed persisted cron job IDs through `openclaw doctor`, including legacy `jobId`, non-string `id`, and missing `id` rows, so `cron list` no longer needs display-layer coercion for corrupt store data. Fixes #70128.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanupBundleMcpHarness } from "./pi-bundle-mcp-test-harness.js";
|
||||
import { __testing, materializeBundleMcpToolsForRun } from "./pi-bundle-mcp-tools.js";
|
||||
import {
|
||||
__testing,
|
||||
getOrCreateSessionMcpRuntime,
|
||||
materializeBundleMcpToolsForRun,
|
||||
retireSessionMcpRuntime,
|
||||
} from "./pi-bundle-mcp-tools.js";
|
||||
import type { SessionMcpRuntime } from "./pi-bundle-mcp-types.js";
|
||||
|
||||
vi.mock("./embedded-pi-mcp.js", () => ({
|
||||
@@ -298,4 +303,20 @@ describe("session MCP runtime", () => {
|
||||
expect((result.error as Error).message).toMatch(/disposed/);
|
||||
expect(manager.listSessionIds()).not.toContain("session-d");
|
||||
});
|
||||
|
||||
it("retires global session runtimes and ignores missing ids", async () => {
|
||||
await getOrCreateSessionMcpRuntime({
|
||||
sessionId: "session-retire",
|
||||
sessionKey: "agent:test:session-retire",
|
||||
workspaceDir: "/workspace",
|
||||
});
|
||||
expect(__testing.getCachedSessionIds()).toContain("session-retire");
|
||||
|
||||
await expect(
|
||||
retireSessionMcpRuntime({ sessionId: " session-retire ", reason: "test" }),
|
||||
).resolves.toBe(true);
|
||||
expect(__testing.getCachedSessionIds()).not.toContain("session-retire");
|
||||
|
||||
await expect(retireSessionMcpRuntime({ sessionId: " ", reason: "test" })).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -436,6 +436,24 @@ export async function disposeSessionMcpRuntime(sessionId: string): Promise<void>
|
||||
await getSessionMcpRuntimeManager().disposeSession(sessionId);
|
||||
}
|
||||
|
||||
export async function retireSessionMcpRuntime(params: {
|
||||
sessionId?: string | null;
|
||||
reason: string;
|
||||
onError?: (error: unknown, sessionId: string, reason: string) => void;
|
||||
}): Promise<boolean> {
|
||||
const sessionId = normalizeOptionalString(params.sessionId);
|
||||
if (!sessionId) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await disposeSessionMcpRuntime(sessionId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
params.onError?.(error, sessionId, params.reason);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function disposeAllSessionMcpRuntimes(): Promise<void> {
|
||||
await getSessionMcpRuntimeManager().disposeAll();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export {
|
||||
disposeSessionMcpRuntime,
|
||||
getOrCreateSessionMcpRuntime,
|
||||
getSessionMcpRuntimeManager,
|
||||
retireSessionMcpRuntime,
|
||||
} from "./pi-bundle-mcp-runtime.js";
|
||||
export {
|
||||
createBundleMcpToolRuntime,
|
||||
|
||||
@@ -33,6 +33,7 @@ let streamCallCount = 0;
|
||||
let observedContexts: Array<Array<{ role?: string; content?: unknown }>> = [];
|
||||
|
||||
vi.mock("./pi-bundle-mcp-tools.js", () => ({
|
||||
retireSessionMcpRuntime: vi.fn(async () => true),
|
||||
getOrCreateSessionMcpRuntime: async () => ({
|
||||
sessionId: "bundle-mcp-runtime",
|
||||
sessionKey: "agent:test:bundle-mcp-e2e",
|
||||
|
||||
@@ -111,6 +111,8 @@ const installRunEmbeddedMocks = () => {
|
||||
}));
|
||||
vi.doMock("./pi-bundle-mcp-tools.js", () => ({
|
||||
disposeSessionMcpRuntime: (sessionId: string) => disposeSessionMcpRuntimeMock(sessionId),
|
||||
retireSessionMcpRuntime: ({ sessionId }: { sessionId?: string | null }) =>
|
||||
sessionId ? disposeSessionMcpRuntimeMock(sessionId) : Promise.resolve(false),
|
||||
}));
|
||||
vi.doMock("./pi-embedded-runner/model.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./pi-embedded-runner/model.js")>(
|
||||
|
||||
@@ -336,6 +336,7 @@ export async function loadCompactHooksHarness(): Promise<{
|
||||
}));
|
||||
|
||||
vi.doMock("../pi-bundle-mcp-tools.js", () => ({
|
||||
retireSessionMcpRuntime: vi.fn(async () => true),
|
||||
createBundleMcpToolRuntime: vi.fn(async () => ({
|
||||
tools: [],
|
||||
dispose: vi.fn(async () => {}),
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
} from "../model-auth.js";
|
||||
import { normalizeProviderId } from "../model-selection.js";
|
||||
import { ensureOpenClawModelsJson } from "../models-config.js";
|
||||
import { disposeSessionMcpRuntime } from "../pi-bundle-mcp-tools.js";
|
||||
import { retireSessionMcpRuntime } from "../pi-bundle-mcp-tools.js";
|
||||
import {
|
||||
classifyFailoverReason,
|
||||
extractObservedOverflowTokenCount,
|
||||
@@ -2130,10 +2130,14 @@ export async function runEmbeddedPiAgent(
|
||||
await contextEngine.dispose?.();
|
||||
stopRuntimeAuthRefreshTimer();
|
||||
if (params.cleanupBundleMcpOnRunEnd === true) {
|
||||
await disposeSessionMcpRuntime(params.sessionId).catch((error) => {
|
||||
log.warn(
|
||||
`bundle-mcp cleanup failed after run for ${params.sessionId}: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
await retireSessionMcpRuntime({
|
||||
sessionId: params.sessionId,
|
||||
reason: "embedded-run-end",
|
||||
onError: (error, sessionId) => {
|
||||
log.warn(
|
||||
`bundle-mcp cleanup failed after run for ${sessionId}: ${formatErrorMessage(error)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +424,7 @@ vi.mock("../../pi-bundle-mcp-tools.js", () => ({
|
||||
createBundleMcpToolRuntime: async () => undefined,
|
||||
getOrCreateSessionMcpRuntime: async () => undefined,
|
||||
materializeBundleMcpToolsForRun: async () => undefined,
|
||||
retireSessionMcpRuntime: async () => true,
|
||||
}));
|
||||
|
||||
vi.mock("../../pi-bundle-lsp-runtime.js", () => ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
import { clearBootstrapSnapshotOnSessionRollover } from "../../agents/bootstrap-cache.js";
|
||||
import { getCliSessionBinding } from "../../agents/cli-session.js";
|
||||
import { resetRegisteredAgentHarnessSessions } from "../../agents/harness/registry.js";
|
||||
import { disposeSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js";
|
||||
import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js";
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import { resolveGroupSessionKey } from "../../config/sessions/group.js";
|
||||
import { canonicalizeMainSessionAlias } from "../../config/sessions/main-session.js";
|
||||
@@ -815,13 +815,14 @@ export async function initSessionState(params: {
|
||||
agentId,
|
||||
archivedTranscripts,
|
||||
});
|
||||
await disposeSessionMcpRuntime(previousSessionEntry.sessionId).catch((error) => {
|
||||
log.warn(
|
||||
`failed to dispose bundle MCP runtime for session ${previousSessionEntry.sessionId}`,
|
||||
{
|
||||
await retireSessionMcpRuntime({
|
||||
sessionId: previousSessionEntry.sessionId,
|
||||
reason: "reply-session-rollover",
|
||||
onError: (error, sessionId) => {
|
||||
log.warn(`failed to dispose bundle MCP runtime for session ${sessionId}`, {
|
||||
error: String(error),
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
await resetRegisteredAgentHarnessSessions({
|
||||
sessionId: previousSessionEntry.sessionId,
|
||||
|
||||
@@ -15,8 +15,9 @@ import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
|
||||
|
||||
// --- Module mocks (must be hoisted before imports) ---
|
||||
|
||||
const { countActiveDescendantRunsMock } = vi.hoisted(() => ({
|
||||
const { countActiveDescendantRunsMock, retireSessionMcpRuntimeMock } = vi.hoisted(() => ({
|
||||
countActiveDescendantRunsMock: vi.fn().mockReturnValue(0),
|
||||
retireSessionMcpRuntimeMock: vi.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/sessions/main-session.js", () => ({
|
||||
@@ -28,6 +29,10 @@ vi.mock("../../agents/subagent-registry-read.js", () => ({
|
||||
countActiveDescendantRuns: countActiveDescendantRunsMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/pi-bundle-mcp-tools.js", () => ({
|
||||
retireSessionMcpRuntime: retireSessionMcpRuntimeMock,
|
||||
}));
|
||||
|
||||
vi.mock("./delivery-subagent-registry.runtime.js", () => ({
|
||||
countActiveDescendantRuns: countActiveDescendantRunsMock,
|
||||
}));
|
||||
@@ -71,6 +76,7 @@ vi.mock("./subagent-followup.runtime.js", () => ({
|
||||
waitForDescendantSubagentSummary: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js";
|
||||
// Import after mocks
|
||||
import { countActiveDescendantRuns } from "../../agents/subagent-registry-read.js";
|
||||
import { callGateway } from "../../gateway/call.runtime.js";
|
||||
@@ -138,6 +144,7 @@ function makeBaseParams(overrides: {
|
||||
} as never,
|
||||
agentId: "main",
|
||||
agentSessionKey: "agent:main",
|
||||
sessionId: "test-session-id",
|
||||
runStartedAt,
|
||||
runEndedAt: runStartedAt,
|
||||
timeoutMs: 30_000,
|
||||
@@ -171,6 +178,7 @@ describe("dispatchCronDelivery — double-announce guard", () => {
|
||||
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
|
||||
vi.mocked(readDescendantSubagentFallbackReply).mockResolvedValue(undefined);
|
||||
vi.mocked(waitForDescendantSubagentSummary).mockResolvedValue(undefined);
|
||||
vi.mocked(retireSessionMcpRuntime).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -533,6 +541,28 @@ describe("dispatchCronDelivery — double-announce guard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retires the MCP runtime directly when deleteAfterRun gateway cleanup fails", async () => {
|
||||
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
|
||||
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
|
||||
vi.mocked(callGateway).mockRejectedValueOnce(new Error("gateway down"));
|
||||
|
||||
const params = makeBaseParams({ synthesizedText: SILENT_REPLY_TOKEN });
|
||||
(params.job as { deleteAfterRun?: boolean }).deleteAfterRun = true;
|
||||
|
||||
const state = await dispatchCronDelivery(params);
|
||||
|
||||
expect(state.result).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "ok",
|
||||
delivered: false,
|
||||
}),
|
||||
);
|
||||
expect(retireSessionMcpRuntime).toHaveBeenCalledWith({
|
||||
sessionId: "test-session-id",
|
||||
reason: "cron-delete-after-run-fallback",
|
||||
});
|
||||
});
|
||||
|
||||
it("text delivery fires exactly once (no double-deliver)", async () => {
|
||||
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
|
||||
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/reply-payload.js";
|
||||
import {
|
||||
isSilentReplyText,
|
||||
@@ -103,6 +104,7 @@ type DispatchCronDeliveryParams = {
|
||||
job: CronJob;
|
||||
agentId: string;
|
||||
agentSessionKey: string;
|
||||
sessionId: string;
|
||||
runStartedAt: number;
|
||||
runEndedAt: number;
|
||||
timeoutMs: number;
|
||||
@@ -477,6 +479,10 @@ export async function dispatchCronDelivery(
|
||||
});
|
||||
directCronSessionDeleted = true;
|
||||
} catch {
|
||||
await retireSessionMcpRuntime({
|
||||
sessionId: params.sessionId,
|
||||
reason: "cron-delete-after-run-fallback",
|
||||
});
|
||||
// Best-effort; direct delivery result should still be returned.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -160,6 +160,7 @@ export function createCronPromptExecutor(params: {
|
||||
sessionKey: params.agentSessionKey,
|
||||
agentId: params.agentId,
|
||||
trigger: "cron",
|
||||
cleanupBundleMcpOnRunEnd: params.job.sessionTarget === "isolated",
|
||||
allowGatewaySubagentBinding: true,
|
||||
senderIsOwner: false,
|
||||
messageChannel: params.messageChannel,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
loadRunCronIsolatedAgentTurn,
|
||||
makeCronSession,
|
||||
retireSessionMcpRuntimeMock,
|
||||
resolveFastModeStateMock,
|
||||
resolveCronSessionMock,
|
||||
runEmbeddedPiAgentMock,
|
||||
@@ -36,19 +37,25 @@ function mockSuccessfulModelFallback() {
|
||||
async function runFastModeCase(params: {
|
||||
configFastMode: boolean;
|
||||
expectedFastMode: boolean;
|
||||
expectedCleanupBundleMcpOnRunEnd?: boolean;
|
||||
expectedRetiredSessionId?: string;
|
||||
message: string;
|
||||
previousSessionId?: string;
|
||||
sessionId?: string;
|
||||
sessionFastMode?: boolean;
|
||||
sessionTarget?: string;
|
||||
}) {
|
||||
const baseSession = makeCronSession();
|
||||
resolveCronSessionMock.mockReturnValue(
|
||||
params.sessionFastMode === undefined
|
||||
? baseSession
|
||||
: makeCronSession({
|
||||
sessionEntry: {
|
||||
...baseSession.sessionEntry,
|
||||
fastMode: params.sessionFastMode,
|
||||
},
|
||||
}),
|
||||
makeCronSession({
|
||||
...baseSession,
|
||||
...(params.previousSessionId ? { previousSessionId: params.previousSessionId } : {}),
|
||||
sessionEntry: {
|
||||
...baseSession.sessionEntry,
|
||||
...(params.sessionId ? { sessionId: params.sessionId } : {}),
|
||||
...(params.sessionFastMode === undefined ? {} : { fastMode: params.sessionFastMode }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
mockSuccessfulModelFallback();
|
||||
resolveFastModeStateMock.mockImplementation(({ cfg, sessionEntry }) => {
|
||||
@@ -77,6 +84,7 @@ async function runFastModeCase(params: {
|
||||
},
|
||||
},
|
||||
job: makeIsolatedAgentTurnJob({
|
||||
sessionTarget: params.sessionTarget ?? "isolated",
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: params.message,
|
||||
@@ -92,8 +100,19 @@ async function runFastModeCase(params: {
|
||||
provider: "openai",
|
||||
model: EXPECTED_OPENAI_MODEL,
|
||||
fastMode: params.expectedFastMode,
|
||||
cleanupBundleMcpOnRunEnd: params.expectedCleanupBundleMcpOnRunEnd ?? true,
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
if (params.expectedRetiredSessionId) {
|
||||
expect(retireSessionMcpRuntimeMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: params.expectedRetiredSessionId,
|
||||
reason: "cron-session-rollover",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
expect(retireSessionMcpRuntimeMock).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
describe("runCronIsolatedAgentTurn — fast mode", () => {
|
||||
@@ -124,4 +143,27 @@ describe("runCronIsolatedAgentTurn — fast mode", () => {
|
||||
sessionFastMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves bundled MCP runtime state for persistent cron session targets", async () => {
|
||||
await runFastModeCase({
|
||||
configFastMode: true,
|
||||
expectedFastMode: true,
|
||||
expectedCleanupBundleMcpOnRunEnd: false,
|
||||
message: "test persistent cron session",
|
||||
sessionTarget: "session:agent:main:main:thread:9999",
|
||||
});
|
||||
});
|
||||
|
||||
it("retires the previous bundled MCP runtime when a persistent cron session rolls over", async () => {
|
||||
await runFastModeCase({
|
||||
configFastMode: true,
|
||||
expectedFastMode: true,
|
||||
expectedCleanupBundleMcpOnRunEnd: false,
|
||||
expectedRetiredSessionId: "stale-session-id",
|
||||
message: "test persistent cron session rollover",
|
||||
previousSessionId: "stale-session-id",
|
||||
sessionId: "rotated-session-id",
|
||||
sessionTarget: "session:agent:main:main:thread:9999",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +69,7 @@ export const resolveHeartbeatAckMaxCharsMock = createMock();
|
||||
export const resolveSessionAuthProfileOverrideMock = createMock();
|
||||
export const resolveFastModeStateMock = createMock();
|
||||
export const getChannelPluginMock = createMock();
|
||||
export const retireSessionMcpRuntimeMock = createMock();
|
||||
|
||||
const resolveBootstrapWarningSignaturesSeenMock = createMock();
|
||||
const resolveCronStyleNowMock = createMock();
|
||||
@@ -196,6 +197,10 @@ vi.mock("../../agents/cli-runner.runtime.js", () => ({
|
||||
setCliSessionId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/pi-bundle-mcp-tools.js", () => ({
|
||||
retireSessionMcpRuntime: retireSessionMcpRuntimeMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../config/sessions/store.runtime.js", () => ({
|
||||
updateSessionStore: updateSessionStoreMock,
|
||||
}));
|
||||
@@ -433,6 +438,8 @@ function resetRunSessionMocks(): void {
|
||||
updateSessionStoreMock.mockResolvedValue(undefined);
|
||||
resolveCronSessionMock.mockReset();
|
||||
resolveCronSessionMock.mockReturnValue(makeCronSession());
|
||||
retireSessionMcpRuntimeMock.mockReset();
|
||||
retireSessionMcpRuntimeMock.mockResolvedValue(true);
|
||||
}
|
||||
|
||||
export function resetRunCronIsolatedAgentTurnHarness(): void {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { hasAnyAuthProfileStoreSource } from "../../agents/auth-profiles/source-check.js";
|
||||
import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js";
|
||||
import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js";
|
||||
import type { SkillSnapshot } from "../../agents/skills.js";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type { CliDeps } from "../../cli/outbound-send-deps.js";
|
||||
import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { resolveCronDeliveryPlan, type CronDeliveryPlan } from "../delivery-plan.js";
|
||||
import type {
|
||||
CronDeliveryTrace,
|
||||
@@ -120,6 +122,29 @@ function resolveNonNegativeNumber(value: number | undefined): number | undefined
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
async function retireRolledCronSessionMcpRuntime(params: {
|
||||
job: CronJob;
|
||||
cronSession: MutableCronSession;
|
||||
}) {
|
||||
if (params.job.sessionTarget === "isolated") {
|
||||
return;
|
||||
}
|
||||
const previousSessionId = normalizeOptionalString(params.cronSession.previousSessionId);
|
||||
const currentSessionId = normalizeOptionalString(params.cronSession.sessionEntry.sessionId);
|
||||
if (!previousSessionId || previousSessionId === currentSessionId) {
|
||||
return;
|
||||
}
|
||||
await retireSessionMcpRuntime({
|
||||
sessionId: previousSessionId,
|
||||
reason: "cron-session-rollover",
|
||||
onError: (error, sessionId) => {
|
||||
logWarn(
|
||||
`[cron:${params.job.id}] Failed to dispose retired bundle MCP runtime for session ${sessionId}: ${String(error)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type { RunCronAgentTurnResult } from "./run.types.js";
|
||||
|
||||
type CronExecutionRuntime = typeof import("./run-executor.runtime.js");
|
||||
@@ -618,6 +643,10 @@ async function prepareCronRunContext(params: {
|
||||
} catch (err) {
|
||||
logWarn(`[cron:${input.job.id}] Failed to persist pre-run session entry: ${String(err)}`);
|
||||
}
|
||||
await retireRolledCronSessionMcpRuntime({
|
||||
job: input.job,
|
||||
cronSession,
|
||||
});
|
||||
const hasSessionAuthProfileOverride = Boolean(
|
||||
cronSession.sessionEntry.authProfileOverride?.trim(),
|
||||
);
|
||||
@@ -839,6 +868,7 @@ async function finalizeCronRun(params: {
|
||||
job: prepared.input.job,
|
||||
agentId: prepared.agentId,
|
||||
agentSessionKey: prepared.agentSessionKey,
|
||||
sessionId: prepared.runSessionId,
|
||||
runStartedAt: execution.runStartedAt,
|
||||
runEndedAt: execution.runEndedAt,
|
||||
timeoutMs: prepared.timeoutMs,
|
||||
|
||||
@@ -120,6 +120,7 @@ describe("resolveCronSession", () => {
|
||||
|
||||
expect(result.sessionEntry.sessionId).toBe("existing-session-id-123");
|
||||
expect(result.isNewSession).toBe(false);
|
||||
expect(result.previousSessionId).toBeUndefined();
|
||||
expect(result.systemSent).toBe(true);
|
||||
expect(clearBootstrapSnapshot).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -139,6 +140,7 @@ describe("resolveCronSession", () => {
|
||||
|
||||
expect(result.sessionEntry.sessionId).not.toBe("old-session-id");
|
||||
expect(result.isNewSession).toBe(true);
|
||||
expect(result.previousSessionId).toBe("old-session-id");
|
||||
expect(result.systemSent).toBe(false);
|
||||
expect(result.sessionEntry.modelOverride).toBe("gpt-4.1-mini");
|
||||
expect(result.sessionEntry.providerOverride).toBe("openai");
|
||||
@@ -161,6 +163,7 @@ describe("resolveCronSession", () => {
|
||||
|
||||
expect(result.sessionEntry.sessionId).not.toBe("existing-session-id-456");
|
||||
expect(result.isNewSession).toBe(true);
|
||||
expect(result.previousSessionId).toBe("existing-session-id-456");
|
||||
expect(result.systemSent).toBe(false);
|
||||
expect(result.sessionEntry.modelOverride).toBe("sonnet-4");
|
||||
expect(result.sessionEntry.providerOverride).toBe("anthropic");
|
||||
|
||||
@@ -59,9 +59,10 @@ export function resolveCronSession(params: {
|
||||
systemSent = false;
|
||||
}
|
||||
|
||||
const previousSessionId = isNewSession ? entry?.sessionId : undefined;
|
||||
clearBootstrapSnapshotOnSessionRollover({
|
||||
sessionKey: params.sessionKey,
|
||||
previousSessionId: isNewSession ? entry?.sessionId : undefined,
|
||||
previousSessionId,
|
||||
});
|
||||
|
||||
const sessionEntry: SessionEntry = {
|
||||
@@ -86,5 +87,5 @@ export function resolveCronSession(params: {
|
||||
sessionFile: undefined,
|
||||
}),
|
||||
};
|
||||
return { storePath, store, sessionEntry, systemSent, isNewSession };
|
||||
return { storePath, store, sessionEntry, systemSent, isNewSession, previousSessionId };
|
||||
}
|
||||
|
||||
@@ -207,6 +207,10 @@ vi.mock("../plugin-sdk/browser-maintenance.js", () => ({
|
||||
|
||||
vi.mock("../agents/pi-bundle-mcp-tools.js", () => ({
|
||||
disposeSessionMcpRuntime: bundleMcpRuntimeMocks.disposeSessionMcpRuntime,
|
||||
retireSessionMcpRuntime: ({ sessionId }: { sessionId?: string | null }) =>
|
||||
sessionId
|
||||
? bundleMcpRuntimeMocks.disposeSessionMcpRuntime(sessionId).then(() => true)
|
||||
: Promise.resolve(false),
|
||||
}));
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getAcpRuntimeBackend } from "../acp/runtime/registry.js";
|
||||
import { readAcpSessionEntry, upsertAcpSessionMeta } from "../acp/runtime/session-meta.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { clearBootstrapSnapshot } from "../agents/bootstrap-cache.js";
|
||||
import { disposeSessionMcpRuntime } from "../agents/pi-bundle-mcp-tools.js";
|
||||
import { retireSessionMcpRuntime } from "../agents/pi-bundle-mcp-tools.js";
|
||||
import { abortEmbeddedPiRun, waitForEmbeddedPiRunEnd } from "../agents/pi-embedded.js";
|
||||
import { stopSubagentsForRequester } from "../auto-reply/reply/abort.js";
|
||||
import { clearSessionQueues } from "../auto-reply/reply/queue.js";
|
||||
@@ -226,10 +226,14 @@ async function ensureSessionRuntimeCleanup(params: {
|
||||
const ended = await waitForEmbeddedPiRunEnd(params.sessionId, 15_000);
|
||||
clearBootstrapSnapshot(params.target.canonicalKey);
|
||||
if (ended) {
|
||||
await disposeSessionMcpRuntime(params.sessionId).catch((error) => {
|
||||
logVerbose(
|
||||
`sessions cleanup: failed to dispose bundle MCP runtime for ${params.sessionId}: ${String(error)}`,
|
||||
);
|
||||
await retireSessionMcpRuntime({
|
||||
sessionId: params.sessionId,
|
||||
reason: "gateway-session-cleanup",
|
||||
onError: (error, sessionId) => {
|
||||
logVerbose(
|
||||
`sessions cleanup: failed to dispose bundle MCP runtime for ${sessionId}: ${String(error)}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
await closeTrackedBrowserTabs();
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user