fix(codex): yield app-server notification projection (#82333)

* fix(codex): yield app-server notification projection

* docs(changelog): note codex notification yield fix
This commit is contained in:
Josh Avant
2026-05-15 16:53:10 -05:00
committed by GitHub
parent 6921d9072e
commit ea16a5e9e1
3 changed files with 42 additions and 2 deletions

View File

@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- 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.
- TTS: preserve channel-derived voice-note delivery for `/tts audio` replies even when the provider output is not natively voice-compatible. (#82174) Thanks @xuruiray.
- 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.
- 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.
- 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.
- 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`.
- 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.

View File

@@ -39,7 +39,11 @@ import {
resolveCodexPluginAppCacheEndpoint,
} from "./plugin-app-cache-key.js";
import type { CodexServerNotification } from "./protocol.js";
import { rememberCodexRateLimits, resetCodexRateLimitCacheForTests } from "./rate-limit-cache.js";
import {
readRecentCodexRateLimits,
rememberCodexRateLimits,
resetCodexRateLimitCacheForTests,
} from "./rate-limit-cache.js";
import {
runCodexAppServerAttempt as runCodexAppServerAttemptImpl,
__testing,
@@ -1982,6 +1986,29 @@ describe("runCodexAppServerAttempt", () => {
);
});
it("yields a macrotask before processing queued app-server notifications", async () => {
const harness = createStartedThreadHarness();
const params = createParams(
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
);
params.timeoutMs = 1_000;
const run = runCodexAppServerAttempt(params);
await harness.waitForMethod("turn/start");
const notification = rateLimitsUpdated(Date.now() + 60_000);
const processing = harness.notify(notification);
await Promise.resolve();
expect(readRecentCodexRateLimits()).toBeUndefined();
await processing;
expect(readRecentCodexRateLimits()).toEqual(notification.params);
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await expect(run).resolves.toMatchObject({ aborted: false, timedOut: false });
});
it("releases the session when a completed agent message item goes quiet", async () => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
const request = vi.fn(async (method: string) => {
@@ -3250,7 +3277,7 @@ describe("runCodexAppServerAttempt", () => {
if (!harnessRef.current) {
throw new Error("Expected Codex app-server harness to be initialized");
}
await harnessRef.current.notify(rateLimitsUpdated(resetsAt));
void harnessRef.current.notify(rateLimitsUpdated(resetsAt));
throw Object.assign(new Error("You've reached your usage limit."), {
data: { codexErrorInfo: "usageLimitExceeded" },
});

View File

@@ -1319,6 +1319,7 @@ export async function runCodexAppServerAttempt(
isCodexTurnAbortMarkerNotification(notification, { currentPromptText: promptBuild.prompt });
const isTurnTerminal = isTurnCompletion || isTurnAbortMarker;
try {
await waitForCodexNotificationDispatchTurn();
await projector.handleNotification(notification);
} catch (error) {
embeddedAgentLog.debug("codex app-server projector notification threw", {
@@ -1342,6 +1343,11 @@ export async function runCodexAppServerAttempt(
}
};
const enqueueNotification = (notification: CodexServerNotification): Promise<void> => {
if (!projector || !turnId) {
userInputBridge?.handleNotification(notification);
pendingNotifications.push(notification);
return Promise.resolve();
}
notificationQueue = notificationQueue.then(
() => handleNotification(notification),
() => handleNotification(notification),
@@ -3255,6 +3261,12 @@ function prependCurrentTurnContext(
return text ? [text, prompt].filter(Boolean).join("\n\n") : prompt;
}
function waitForCodexNotificationDispatchTurn(): Promise<void> {
return new Promise((resolve) => {
setImmediate(resolve);
});
}
function handleApprovalRequest(params: {
method: string;
params: JsonValue | undefined;