diff --git a/CHANGELOG.md b/CHANGELOG.md index bc07fc47077..c4bc3e2aeae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/watch: leave `OPENCLAW_TRACE_SYNC_IO` disabled by default in `pnpm gateway:watch:raw` so watch mode avoids noisy Node sync-I/O stack traces unless explicitly requested. +- CLI/Codex: dispose registered agent harnesses during short-lived CLI shutdown so successful Codex-backed `agent --local` runs do not leave app-server child processes alive. - Agents/Codex: auto-enable the Codex harness plugin for one-shot OpenAI model overrides so `openclaw agent --local --model openai/...` does not fail with an unregistered `codex` harness. - Gateway/live tests: avoid full model-registry enumeration for explicit provider-qualified live model filters, preventing `.profile` OpenAI gateway profile runs from hanging before provider dispatch. - Providers: preserve non-OK `text/event-stream` response bodies so provider HTTP errors keep their JSON detail instead of collapsing to generic streaming failures. Fixes #78180. diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index fe064b3f997..371d098b816 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -10,6 +10,8 @@ const ensurePathMock = vi.hoisted(() => vi.fn()); const assertRuntimeMock = vi.hoisted(() => vi.fn()); const closeActiveMemorySearchManagersMock = vi.hoisted(() => vi.fn(async () => {})); const hasMemoryRuntimeMock = vi.hoisted(() => vi.fn(() => false)); +const listAgentHarnessIdsMock = vi.hoisted(() => vi.fn((): string[] => [])); +const disposeRegisteredAgentHarnessesMock = vi.hoisted(() => vi.fn(async () => {})); const ensureTaskRegistryReadyMock = vi.hoisted(() => vi.fn()); const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn()); const outputRootHelpMock = vi.hoisted(() => vi.fn()); @@ -120,6 +122,11 @@ vi.mock("../plugins/memory-state.js", () => ({ hasMemoryRuntime: hasMemoryRuntimeMock, })); +vi.mock("../agents/harness/registry.js", () => ({ + listAgentHarnessIds: listAgentHarnessIdsMock, + disposeRegisteredAgentHarnesses: disposeRegisteredAgentHarnessesMock, +})); + vi.mock("../tasks/task-registry.js", () => ({ ensureTaskRegistryReady: ensureTaskRegistryReadyMock, })); @@ -216,6 +223,7 @@ describe("runCli exit behavior", () => { beforeEach(() => { vi.clearAllMocks(); hasMemoryRuntimeMock.mockReturnValue(false); + listAgentHarnessIdsMock.mockReturnValue([]); outputPrecomputedBrowserHelpTextMock.mockReturnValue(false); outputPrecomputedRootHelpTextMock.mockReturnValue(false); hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false); @@ -242,12 +250,28 @@ describe("runCli exit behavior", () => { expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "status"]); expect(tryRouteCliMock).toHaveBeenCalledWith(["node", "openclaw", "status"]); expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled(); + expect(disposeRegisteredAgentHarnessesMock).not.toHaveBeenCalled(); expect(ensureTaskRegistryReadyMock).not.toHaveBeenCalled(); expect(startTaskRegistryMaintenanceMock).not.toHaveBeenCalled(); expect(exitSpy).not.toHaveBeenCalled(); exitSpy.mockRestore(); }); + it("disposes registered harnesses after full CLI command completion", async () => { + listAgentHarnessIdsMock.mockReturnValueOnce(["codex"]); + tryRouteCliMock.mockResolvedValueOnce(false); + const parseAsync = vi.fn().mockResolvedValueOnce(undefined); + buildProgramMock.mockReturnValueOnce({ + commands: [{ name: () => "agent", aliases: () => [] }], + parseAsync, + }); + + await runCli(["node", "openclaw", "agent", "--local"]); + + expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "agent", "--local"]); + expect(disposeRegisteredAgentHarnessesMock).toHaveBeenCalledTimes(1); + }); + it("pauses non-tty stdin after full CLI command completion", async () => { tryRouteCliMock.mockResolvedValueOnce(false); const parseAsync = vi.fn().mockResolvedValueOnce(undefined); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index e4ee17de5b9..7d533f0cbac 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -207,6 +207,20 @@ async function closeCliMemoryManagers(): Promise { } } +async function disposeCliAgentHarnesses(): Promise { + try { + const { listAgentHarnessIds, disposeRegisteredAgentHarnesses } = + await import("../agents/harness/registry.js"); + if (listAgentHarnessIds().length === 0) { + return; + } + await disposeRegisteredAgentHarnesses(); + } catch { + // Best-effort teardown for short-lived CLI commands. Harness plugins may + // own subprocesses, but cleanup must not hide the command's real outcome. + } +} + function pauseNonTtyStdinForCliExit(): void { const stdin = process.stdin; if (stdin.isTTY) { @@ -691,6 +705,7 @@ export async function runCli(argv: string[] = process.argv) { process.off("exit", onExit); } await stopStartedProxy(); + await disposeCliAgentHarnesses(); await closeCliMemoryManagers(); pauseNonTtyStdinForCliExit(); }