diff --git a/CHANGELOG.md b/CHANGELOG.md index 61afe7136f2..2f2f8b4b2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai - Plugins/runtime deps: reuse enclosing versioned cache roots when bundled plugins resolve from nested staged paths, so plugin-runtime-deps no longer mints `openclaw-unknown-*` directories or loops on `ENOTEMPTY`. Fixes #72956. (#73205) Thanks @SymbolStar. - Agents/failover: classify CJK provider transport, quota, billing, auth, and overload error text so Chinese-language provider failures trigger fallback and user-facing transport copy instead of surfacing as unclassified raw errors. (#56242) Thanks @tomcatzh. - Agents/failover: seed non-claude-cli fallback prompts with Claude Code session context when a claude-cli attempt fails, so fallback models do not restart cold after billing or quota failover. (#72069) Thanks @stainlu. +- Agents/CLI runner: transfer bundle-MCP tempDir cleanup from the per-turn runner finally to the Claude live-session lifecycle, so persistent Claude CLI sessions keep their `--mcp-config` directory until the live subprocess closes. Fixes #73244. Thanks @edwin-rivera-dev. ## 2026.4.27 diff --git a/src/agents/cli-runner.spawn.test.ts b/src/agents/cli-runner.spawn.test.ts index db147457fa6..59cfd95e909 100644 --- a/src/agents/cli-runner.spawn.test.ts +++ b/src/agents/cli-runner.spawn.test.ts @@ -834,6 +834,60 @@ describe("runCliAgent spawn path", () => { } }); + it("defers prepared backend cleanup to the Claude live session lifecycle", async () => { + let stdoutListener: ((chunk: string) => void) | undefined; + const stdin = { + write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => { + stdoutListener?.( + [ + JSON.stringify({ type: "system", subtype: "init", session_id: "live-session-cleanup" }), + JSON.stringify({ + type: "result", + session_id: "live-session-cleanup", + result: "ok", + }), + ].join("\n") + "\n", + ); + cb?.(); + }), + end: vi.fn(), + }; + supervisorSpawnMock.mockImplementation(async (...args: unknown[]) => { + const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void }; + stdoutListener = input.onStdout; + return { + runId: "live-cleanup-run", + pid: 2346, + startedAtMs: Date.now(), + stdin, + wait: vi.fn(() => new Promise(() => {})), + cancel: vi.fn(), + }; + }); + const preparedBackendCleanup = vi.fn(async () => {}); + const context = buildPreparedCliRunContext({ + provider: "claude-cli", + model: "sonnet", + runId: "run-live-cleanup", + prompt: "first", + backend: { + args: ["-p", "--strict-mcp-config", "--mcp-config", "/tmp/mcp-cleanup.json"], + liveSession: "claude-stdio", + }, + mcpConfigHash: "cleanup-mcp-config", + }); + context.preparedBackend.cleanup = preparedBackendCleanup; + + const result = await executePreparedCliRun(context); + + expect(result.text).toBe("ok"); + expect(context.preparedBackend.cleanup).toBeUndefined(); + expect(preparedBackendCleanup).not.toHaveBeenCalled(); + + resetClaudeLiveSessionsForTest(); + await vi.waitFor(() => expect(preparedBackendCleanup).toHaveBeenCalledOnce()); + }); + it("accepts Claude live stream-json lines larger than 256 KiB", async () => { const largeText = "x".repeat(270 * 1024); let stdoutListener: ((chunk: string) => void) | undefined; diff --git a/src/agents/cli-runner/execute.ts b/src/agents/cli-runner/execute.ts index 31f6687b75a..0d6ed5c2cde 100644 --- a/src/agents/cli-runner/execute.ts +++ b/src/agents/cli-runner/execute.ts @@ -340,6 +340,8 @@ export async function executePreparedCliRun( throw new Error("Claude live session requires JSONL streaming parser"); } claudeSkillsPluginCleanupOwned = true; + const ownedPreparedBackendCleanup = context.preparedBackend.cleanup; + context.preparedBackend.cleanup = undefined; const liveResult = await runClaudeLiveSessionTurn({ context, args, @@ -364,7 +366,13 @@ export async function executePreparedCliRun( }, }); }, - cleanup: claudeSkillsPlugin.cleanup, + cleanup: async () => { + try { + await claudeSkillsPlugin.cleanup(); + } finally { + await ownedPreparedBackendCleanup?.(); + } + }, }); const rawText = liveResult.output.text; return { diff --git a/src/video-generation/provider-registry.test.ts b/src/video-generation/provider-registry.test.ts index dfdbd9aa178..108dc0c376d 100644 --- a/src/video-generation/provider-registry.test.ts +++ b/src/video-generation/provider-registry.test.ts @@ -26,9 +26,9 @@ async function loadProviderRegistry() { vi.resetModules(); return await import("./provider-registry.js"); } - describe("video-generation provider registry", () => { beforeEach(() => { + vi.resetModules(); resolvePluginCapabilityProvidersMock.mockReset(); resolvePluginCapabilityProvidersMock.mockReturnValue([]); }); @@ -44,8 +44,8 @@ describe("video-generation provider registry", () => { }); it("uses active plugin providers without loading from disk", async () => { - const { getVideoGenerationProvider } = await loadProviderRegistry(); resolvePluginCapabilityProvidersMock.mockReturnValue([createProvider({ id: "custom-video" })]); + const { getVideoGenerationProvider } = await loadProviderRegistry(); const provider = getVideoGenerationProvider("custom-video"); @@ -57,12 +57,12 @@ describe("video-generation provider registry", () => { }); it("ignores prototype-like provider ids and aliases", async () => { - const { getVideoGenerationProvider, listVideoGenerationProviders } = - await loadProviderRegistry(); resolvePluginCapabilityProvidersMock.mockReturnValue([ createProvider({ id: "__proto__", aliases: ["constructor", "prototype"] }), createProvider({ id: "safe-video", aliases: ["safe-alias", "constructor"] }), ]); + const { getVideoGenerationProvider, listVideoGenerationProviders } = + await loadProviderRegistry(); expect(listVideoGenerationProviders().map((provider) => provider.id)).toEqual(["safe-video"]); expect(getVideoGenerationProvider("__proto__")).toBeUndefined(); diff --git a/test/scripts/npm-telegram-live.test.ts b/test/scripts/npm-telegram-live.test.ts index 070d02cbd21..c43d1facba9 100644 --- a/test/scripts/npm-telegram-live.test.ts +++ b/test/scripts/npm-telegram-live.test.ts @@ -70,10 +70,6 @@ describe("package Telegram live Docker E2E", () => { expect(script).toContain('cp "$openclaw_package_dir/package.json" /app/package.json'); expect(script).toContain('ln -sfnT /app/extensions "$openclaw_package_dir/extensions"'); expect(script).toContain('"/app/node_modules/openclaw/package.json"'); - expect(script).toContain('pkg.exports["./plugin-sdk/qa-channel"]'); - expect(script).toContain('"./extensions/qa-channel/api.ts"'); - expect(script).toContain('pkg.exports["./plugin-sdk/qa-channel-protocol"]'); - expect(script).toContain('"./extensions/qa-channel/src/protocol.ts"'); expect(script).toContain('pkg.exports["./plugin-sdk/gateway-runtime"]'); expect(script).toContain('"./dist/plugin-sdk/gateway-runtime.js"'); expect(gatewayRpcClient).toContain('from "openclaw/plugin-sdk/gateway-runtime"'); diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index 936fa2262c5..1cf34fde04f 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -69,12 +69,7 @@ import { createUtilsVitestConfig } from "./vitest/vitest.utils.config.ts"; import { createWizardVitestConfig } from "./vitest/vitest.wizard.config.ts"; const EXTENSIONS_CHANNEL_GLOB = ["extensions", "channel", "**"].join("/"); -const PRIVATE_PLUGIN_SDK_SUBPATHS = [ - "qa-channel", - "qa-channel-protocol", - "qa-lab", - "qa-runtime", -] as const; +const PRIVATE_PLUGIN_SDK_SUBPATHS = ["qa-lab", "qa-runtime"] as const; function bundledExcludePatternCouldMatchFile(pattern: string, file: string): boolean { if (pattern === file) {