mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 09:10:43 +00:00
fix(cli): dispose agent harnesses on exit
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user