fix(codex/app-server): release session lane when projector throws on turn/completed

This commit is contained in:
ayeshakhalid192007-dev
2026-04-19 20:47:09 +05:00
committed by Peter Steinberger
parent 54a2a20447
commit f2f27775fb
3 changed files with 74 additions and 7 deletions

View File

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

View File

@@ -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<void> = 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<never>(() => undefined));
const params = createParams(

View File

@@ -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<void> => {