fix(cli): dispose agent harnesses on exit

This commit is contained in:
Peter Steinberger
2026-05-08 02:56:47 +01:00
parent 22657861c8
commit bee3a7372e
3 changed files with 40 additions and 0 deletions

View File

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

View File

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

View File

@@ -207,6 +207,20 @@ async function closeCliMemoryManagers(): Promise<void> {
}
}
async function disposeCliAgentHarnesses(): Promise<void> {
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();
}