fix: defer Claude live MCP cleanup (#73351)

Thanks @edwin-rivera-dev.
This commit is contained in:
Edwin Rivera
2026-04-28 08:59:58 +00:00
committed by GitHub
parent 249cb54373
commit bca30b62be
6 changed files with 69 additions and 15 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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"');

View File

@@ -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) {