Skip to content

Commit ea16a5e

Browse files
authored
fix(codex): yield app-server notification projection (#82333)
* fix(codex): yield app-server notification projection * docs(changelog): note codex notification yield fix
1 parent 6921d90 commit ea16a5e

3 files changed

Lines changed: 42 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
4444
- LINE: stop cron recovery from inferring lowercased LINE recipients from canonical session keys, so long-running task replies do not silently retry undeliverable push targets. Fixes #81628. (#81704) Thanks @edenfunf.
4545
- TTS: preserve channel-derived voice-note delivery for `/tts audio` replies even when the provider output is not natively voice-compatible. (#82174) Thanks @xuruiray.
4646
- Codex app-server: preserve inbound sender metadata and source-channel provenance on mirrored user prompts, including failure snapshots, so channel history keeps the original sender identity. (#82184) Thanks @zknicker.
47+
- Codex app-server: yield projector work to the event loop between embedded-run notifications while preserving pre-turn rate-limit capture, reducing gateway stalls from account and MCP status notifications. Fixes #81936. (#82333) Thanks @joshavant.
4748
- Codex account/status: treat metadata-only rate-limit buckets as returned but empty so `/codex status` and `/codex account` report `none returned` instead of counting phantom limits.
4849
- Codex/Lossless: keep Codex explicit compaction on native app-server threads while allowing Lossless through the context-engine slot; `openclaw doctor --fix` now migrates legacy `compaction.provider: "lossless-claw"` config to `plugins.slots.contextEngine`.
4950
- Cron/doctor: report scheduled jobs with explicit `payload.model` overrides, including provider namespace counts and default-model mismatches, so stale cron model pins are visible during auth or billing investigations. Fixes #82151. Thanks @mgonto.

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ import {
3939
resolveCodexPluginAppCacheEndpoint,
4040
} from "./plugin-app-cache-key.js";
4141
import type { CodexServerNotification } from "./protocol.js";
42-
import { rememberCodexRateLimits, resetCodexRateLimitCacheForTests } from "./rate-limit-cache.js";
42+
import {
43+
readRecentCodexRateLimits,
44+
rememberCodexRateLimits,
45+
resetCodexRateLimitCacheForTests,
46+
} from "./rate-limit-cache.js";
4347
import {
4448
runCodexAppServerAttempt as runCodexAppServerAttemptImpl,
4549
__testing,
@@ -1982,6 +1986,29 @@ describe("runCodexAppServerAttempt", () => {
19821986
);
19831987
});
19841988

1989+
it("yields a macrotask before processing queued app-server notifications", async () => {
1990+
const harness = createStartedThreadHarness();
1991+
const params = createParams(
1992+
path.join(tempDir, "session.jsonl"),
1993+
path.join(tempDir, "workspace"),
1994+
);
1995+
params.timeoutMs = 1_000;
1996+
1997+
const run = runCodexAppServerAttempt(params);
1998+
await harness.waitForMethod("turn/start");
1999+
2000+
const notification = rateLimitsUpdated(Date.now() + 60_000);
2001+
const processing = harness.notify(notification);
2002+
await Promise.resolve();
2003+
2004+
expect(readRecentCodexRateLimits()).toBeUndefined();
2005+
await processing;
2006+
expect(readRecentCodexRateLimits()).toEqual(notification.params);
2007+
2008+
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
2009+
await expect(run).resolves.toMatchObject({ aborted: false, timedOut: false });
2010+
});
2011+
19852012
it("releases the session when a completed agent message item goes quiet", async () => {
19862013
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
19872014
const request = vi.fn(async (method: string) => {
@@ -3250,7 +3277,7 @@ describe("runCodexAppServerAttempt", () => {
32503277
if (!harnessRef.current) {
32513278
throw new Error("Expected Codex app-server harness to be initialized");
32523279
}
3253-
await harnessRef.current.notify(rateLimitsUpdated(resetsAt));
3280+
void harnessRef.current.notify(rateLimitsUpdated(resetsAt));
32543281
throw Object.assign(new Error("You've reached your usage limit."), {
32553282
data: { codexErrorInfo: "usageLimitExceeded" },
32563283
});

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,7 @@ export async function runCodexAppServerAttempt(
13191319
isCodexTurnAbortMarkerNotification(notification, { currentPromptText: promptBuild.prompt });
13201320
const isTurnTerminal = isTurnCompletion || isTurnAbortMarker;
13211321
try {
1322+
await waitForCodexNotificationDispatchTurn();
13221323
await projector.handleNotification(notification);
13231324
} catch (error) {
13241325
embeddedAgentLog.debug("codex app-server projector notification threw", {
@@ -1342,6 +1343,11 @@ export async function runCodexAppServerAttempt(
13421343
}
13431344
};
13441345
const enqueueNotification = (notification: CodexServerNotification): Promise<void> => {
1346+
if (!projector || !turnId) {
1347+
userInputBridge?.handleNotification(notification);
1348+
pendingNotifications.push(notification);
1349+
return Promise.resolve();
1350+
}
13451351
notificationQueue = notificationQueue.then(
13461352
() => handleNotification(notification),
13471353
() => handleNotification(notification),
@@ -3255,6 +3261,12 @@ function prependCurrentTurnContext(
32553261
return text ? [text, prompt].filter(Boolean).join("\n\n") : prompt;
32563262
}
32573263

3264+
function waitForCodexNotificationDispatchTurn(): Promise<void> {
3265+
return new Promise((resolve) => {
3266+
setImmediate(resolve);
3267+
});
3268+
}
3269+
32583270
function handleApprovalRequest(params: {
32593271
method: string;
32603272
params: JsonValue | undefined;

0 commit comments

Comments
 (0)