From f2f27775fbd755900421eeb82b76cd135353fa86 Mon Sep 17 00:00:00 2001 From: ayeshakhalid192007-dev Date: Sun, 19 Apr 2026 20:47:09 +0500 Subject: [PATCH] fix(codex/app-server): release session lane when projector throws on turn/completed --- CHANGELOG.md | 2 + .../codex/src/app-server/run-attempt.test.ts | 55 +++++++++++++++++++ .../codex/src/app-server/run-attempt.ts | 24 +++++--- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd96d327eb..54d12ecc389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ Docs: https://docs.openclaw.ai - Agents/compaction: always reload embedded Pi resources through an explicit loader and reapply reserve-token overrides so runs without extension factories no longer silently lose compaction settings before session start. (#67146) Thanks @ly85206559. - Memory-core/dreaming: normalize sweep timestamps and reuse hashed narrative session keys for fallback cleanup so Dreaming narrative sub-sessions stop leaking. (#67023) Thanks @chiyouYCH. - Gateway/startup: delay HTTP bind until websocket handlers are attached, so immediate post-startup websocket health/connect probes no longer hit the startup race window. (#43392) Thanks @dalefrieswthat. +- Codex/app-server: release the session lane when a downstream consumer throws while draining the `turn/completed` notification, so follow-up messages after a Codex plugin reply stop queueing behind a stale lane lock. Fixes #67996. (#69072) Thanks @ayeshakhalid192007-dev. + ## 2026.4.20 ### Changes diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 91e5e61811b..5e37f5e4790 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -295,6 +295,61 @@ describe("runCodexAppServerAttempt", () => { }); }); + it("releases completion when a projector callback throws during turn/completed", async () => { + // Regression for openclaw/openclaw#67996: a throw inside the projector's + // turn/completed handler must not strand resolveCompletion, otherwise the + // gateway session lane stays locked and every follow-up message queues + // behind a run that will never resolve. + let notify: (notification: CodexServerNotification) => Promise = async () => undefined; + const request = vi.fn(async (method: string) => { + if (method === "thread/start") { + return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" }; + } + if (method === "turn/start") { + return { turn: { id: "turn-1", status: "inProgress" } }; + } + return {}; + }); + __testing.setCodexAppServerClientFactoryForTests( + async () => + ({ + request, + addNotificationHandler: (handler: typeof notify) => { + notify = handler; + return () => undefined; + }, + addRequestHandler: () => () => undefined, + }) as never, + ); + const params = createParams( + path.join(tempDir, "session.jsonl"), + path.join(tempDir, "workspace"), + ); + params.onAgentEvent = () => { + throw new Error("downstream consumer exploded"); + }; + const run = runCodexAppServerAttempt(params); + await vi.waitFor(() => + expect(request.mock.calls.some(([method]) => method === "turn/start")).toBe(true), + ); + await notify({ + method: "turn/completed", + params: { + threadId: "thread-1", + turnId: "turn-1", + turn: { + id: "turn-1", + status: "completed", + items: [{ id: "plan-1", type: "plan", text: "step one\nstep two" }], + }, + }, + }); + await expect(run).resolves.toMatchObject({ + aborted: false, + timedOut: false, + }); + }); + it("times out app-server startup before thread setup can hang forever", async () => { __testing.setCodexAppServerClientFactoryForTests(() => new Promise(() => undefined)); const params = createParams( diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 9613b3e3a60..f8d7238523d 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -136,13 +136,23 @@ export async function runCodexAppServerAttempt( pendingNotifications.push(notification); return; } - await projector.handleNotification(notification); - if ( - notification.method === "turn/completed" && - isTurnNotification(notification.params, turnId) - ) { - completed = true; - resolveCompletion?.(); + // Determine terminal-turn status before invoking the projector so a throw + // inside projector.handleNotification still releases the session lane. + // See openclaw/openclaw#67996. + const isTurnCompletion = + notification.method === "turn/completed" && isTurnNotification(notification.params, turnId); + try { + await projector.handleNotification(notification); + } catch (error) { + embeddedAgentLog.debug("codex app-server projector notification threw", { + method: notification.method, + error, + }); + } finally { + if (isTurnCompletion) { + completed = true; + resolveCompletion?.(); + } } }; const enqueueNotification = (notification: CodexServerNotification): Promise => {