fix(codex): bound app-server timeout fallout · openclaw/openclaw@5a7d5c6
steipete
·
2026-05-27
·
via Recent Commits to openclaw:main
File tree
src/agents/pi-embedded-runner/run
| Original file line number | Diff line number | Diff line change |
|---|
@@ -22,3 +22,13 @@ export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
|
22 | 22 | import("./shared-client.js").then(({ getSharedCodexAppServerClient }) => |
23 | 23 | getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }), |
24 | 24 | ); |
| 25 | + |
| 26 | +export const defaultLeasedCodexAppServerClientFactory: CodexAppServerClientFactory = ( |
| 27 | +startOptions, |
| 28 | +authProfileId, |
| 29 | +agentDir, |
| 30 | +config, |
| 31 | +) => |
| 32 | +import("./shared-client.js").then(({ getLeasedSharedCodexAppServerClient }) => |
| 33 | +getLeasedSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }), |
| 34 | +); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -74,10 +74,13 @@ async function withCodexAppServerModelClient<T>(
|
74 | 74 | ): Promise<T> { |
75 | 75 | const timeoutMs = options.timeoutMs ?? 2500; |
76 | 76 | const useSharedClient = options.sharedClient !== false; |
77 | | -const { createIsolatedCodexAppServerClient, getSharedCodexAppServerClient } = |
78 | | -await import("./shared-client.js"); |
| 77 | +const { |
| 78 | + createIsolatedCodexAppServerClient, |
| 79 | + getLeasedSharedCodexAppServerClient, |
| 80 | + releaseLeasedSharedCodexAppServerClient, |
| 81 | +} = await import("./shared-client.js"); |
79 | 82 | const client = useSharedClient |
80 | | - ? await getSharedCodexAppServerClient({ |
| 83 | + ? await getLeasedSharedCodexAppServerClient({ |
81 | 84 | startOptions: options.startOptions, |
82 | 85 | timeoutMs, |
83 | 86 | authProfileId: options.authProfileId, |
@@ -94,7 +97,9 @@ async function withCodexAppServerModelClient<T>(
|
94 | 97 | try { |
95 | 98 | return await run({ client, timeoutMs }); |
96 | 99 | } finally { |
97 | | -if (!useSharedClient) { |
| 100 | +if (useSharedClient) { |
| 101 | +releaseLeasedSharedCodexAppServerClient(client); |
| 102 | +} else { |
98 | 103 | client.close(); |
99 | 104 | } |
100 | 105 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,7 +5,11 @@ const sharedClientMocks = vi.hoisted(() => ({
|
5 | 5 | getSharedCodexAppServerClient: vi.fn(), |
6 | 6 | })); |
7 | 7 | |
8 | | -vi.mock("./shared-client.js", () => sharedClientMocks); |
| 8 | +vi.mock("./shared-client.js", () => ({ |
| 9 | + ...sharedClientMocks, |
| 10 | +getLeasedSharedCodexAppServerClient: sharedClientMocks.getSharedCodexAppServerClient, |
| 11 | +releaseLeasedSharedCodexAppServerClient: vi.fn(), |
| 12 | +})); |
9 | 13 | |
10 | 14 | const { requestCodexAppServerJson } = await import("./request.js"); |
11 | 15 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -9,7 +9,8 @@ import type {
|
9 | 9 | import { resolveCodexAppServerDirectSandboxBypassBlock } from "./sandbox-guard.js"; |
10 | 10 | import { |
11 | 11 | createIsolatedCodexAppServerClient, |
12 | | -getSharedCodexAppServerClient, |
| 12 | +getLeasedSharedCodexAppServerClient, |
| 13 | +releaseLeasedSharedCodexAppServerClient, |
13 | 14 | } from "./shared-client.js"; |
14 | 15 | import { withTimeout } from "./timeout.js"; |
15 | 16 | |
@@ -63,7 +64,7 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
|
63 | 64 | return await withTimeout( |
64 | 65 | (async () => { |
65 | 66 | const client = await ( |
66 | | -params.isolated ? createIsolatedCodexAppServerClient : getSharedCodexAppServerClient |
| 67 | +params.isolated ? createIsolatedCodexAppServerClient : getLeasedSharedCodexAppServerClient |
67 | 68 | )({ |
68 | 69 | startOptions: params.startOptions, |
69 | 70 | timeoutMs, |
@@ -81,6 +82,8 @@ export async function requestCodexAppServerJson<T = JsonValue | undefined>(param
|
81 | 82 | // underlying codex binary, so the unref'd close() path can leave |
82 | 83 | // the child running and keep the parent's event loop alive. |
83 | 84 | await client.closeAndWait({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 }); |
| 85 | +} else { |
| 86 | +releaseLeasedSharedCodexAppServerClient(client); |
84 | 87 | } |
85 | 88 | } |
86 | 89 | })(), |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -4160,7 +4160,7 @@ describe("runCodexAppServerAttempt", () => {
|
4160 | 4160 | }); |
4161 | 4161 | }); |
4162 | 4162 | |
4163 | | -it("interrupts but keeps the app-server client alive when a turn timeout fires", async () => { |
| 4163 | +it("unsubscribes and closes the app-server client when the active turn goes idle past the attempt timeout", async () => { |
4164 | 4164 | const close = vi.fn(); |
4165 | 4165 | const request = vi.fn(async (method: string) => { |
4166 | 4166 | if (method === "thread/start") { |
@@ -4204,7 +4204,14 @@ describe("runCodexAppServerAttempt", () => {
|
4204 | 4204 | }, |
4205 | 4205 | { timeoutMs: 5_000 }, |
4206 | 4206 | ); |
4207 | | -expect(close).not.toHaveBeenCalled(); |
| 4207 | +expect(request).toHaveBeenCalledWith( |
| 4208 | +"thread/unsubscribe", |
| 4209 | +{ |
| 4210 | +threadId: "thread-1", |
| 4211 | +}, |
| 4212 | +{ timeoutMs: 5_000 }, |
| 4213 | +); |
| 4214 | +expect(close).toHaveBeenCalledTimes(1); |
4208 | 4215 | expect(queueActiveRunMessageForTest("session-1", "after timeout")).toBe(false); |
4209 | 4216 | }); |
4210 | 4217 | |
@@ -5562,9 +5569,13 @@ describe("runCodexAppServerAttempt", () => {
|
5562 | 5569 | ), |
5563 | 5570 | { interval: 1 }, |
5564 | 5571 | ); |
5565 | | -expect(warn).not.toHaveBeenCalledWith( |
| 5572 | +expect(warn).toHaveBeenCalledWith( |
5566 | 5573 | "codex app-server client retired after timed-out turn", |
5567 | | -expect.anything(), |
| 5574 | +expect.objectContaining({ |
| 5575 | +reason: "turn_completion_idle_timeout", |
| 5576 | +threadId: "thread-1", |
| 5577 | +turnId: "turn-1", |
| 5578 | +}), |
5568 | 5579 | ); |
5569 | 5580 | }); |
5570 | 5581 | |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。