fix(codex): flush pending steering on completion

This commit is contained in:
Peter Steinberger
2026-04-30 03:06:01 +01:00
parent 1a103088ba
commit 58153d38af
3 changed files with 40 additions and 3 deletions

View File

@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
- CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so `status --all`, `status --deep`, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb.
- Google Meet: block managed Chrome intro/test speech until browser health proves the participant is in-call, and expose `speechReady` diagnostics so login, admission, permission, and audio-bridge blockers no longer look like successful speech. Refs #72478. Thanks @DougButdorf.
- Slack/commands: keep native command argument menus on select controls for encoded choice values up to Slack's option limit and truncate fallback button labels to Slack's button-text limit, so long valid choices no longer render invalid Slack blocks. Thanks @slackapi.
- Agents/Codex: flush accepted debounced steering messages before normal app-server turn cleanup, so inbound follow-ups acknowledged as queued are not dropped when the turn completes before the debounce fires. Thanks @vincentkoc.
- CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash.
- Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme.
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.

View File

@@ -1089,6 +1089,31 @@ describe("runCodexAppServerAttempt", () => {
await run;
});
it("flushes pending default queued steering during normal turn cleanup", async () => {
const { requests, waitForMethod, completeTurn } = createStartedThreadHarness();
const run = runCodexAppServerAttempt(
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
);
await waitForMethod("turn/start");
expect(queueAgentHarnessMessage("session-1", "late steer", { debounceMs: 30_000 })).toBe(true);
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
expect(requests.filter((entry) => entry.method === "turn/steer")).toEqual([
{
method: "turn/steer",
params: {
threadId: "thread-1",
expectedTurnId: "turn-1",
input: [{ type: "text", text: "late steer", text_elements: [] }],
},
},
]);
});
it("keeps legacy queue steering as separate turn/steer requests", async () => {
const { requests, waitForMethod, completeTurn } = createStartedThreadHarness();

View File

@@ -193,6 +193,9 @@ function createCodexSteeringQueue(params: {
void flushBatch();
}, debounceMs);
},
async flushPending() {
await flushBatch();
},
cancel() {
clearBatchTimer();
batchedTexts = [];
@@ -453,6 +456,7 @@ export async function runCodexAppServerAttempt(
let turnId: string | undefined;
const pendingNotifications: CodexServerNotification[] = [];
let userInputBridge: ReturnType<typeof createCodexUserInputBridge> | undefined;
let steeringQueue: ReturnType<typeof createCodexSteeringQueue> | undefined;
let completed = false;
let timedOut = false;
let turnCompletionIdleTimedOut = false;
@@ -586,6 +590,9 @@ export async function runCodexAppServerAttempt(
});
} finally {
if (isTurnCompletion) {
if (!timedOut && !runAbortController.signal.aborted) {
await steeringQueue?.flushPending();
}
completed = true;
clearTurnCompletionIdleTimer();
resolveCompletion?.();
@@ -814,17 +821,18 @@ export async function runCodexAppServerAttempt(
});
}
const steeringQueue = createCodexSteeringQueue({
const activeSteeringQueue = createCodexSteeringQueue({
client,
threadId: thread.threadId,
turnId: activeTurnId,
answerPendingUserInput: (text) => userInputBridge?.handleQueuedMessage(text) ?? false,
signal: runAbortController.signal,
});
steeringQueue = activeSteeringQueue;
const handle = {
kind: "embedded" as const,
queueMessage: async (text: string, options?: CodexSteeringQueueOptions) =>
steeringQueue.queue(text, options),
activeSteeringQueue.queue(text, options),
isStreaming: () => !completed,
isCompacting: () => projector?.isCompacting() ?? false,
cancel: () => runAbortController.abort("cancelled"),
@@ -991,6 +999,9 @@ export async function runCodexAppServerAttempt(
await trajectoryRecorder?.flush();
},
});
if (!timedOut && !runAbortController.signal.aborted) {
await steeringQueue?.flushPending();
}
userInputBridge?.cancelPending();
clearTimeout(timeout);
clearTurnCompletionIdleTimer();
@@ -999,7 +1010,7 @@ export async function runCodexAppServerAttempt(
nativeHookRelay?.unregister();
runAbortController.signal.removeEventListener("abort", abortListener);
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
steeringQueue.cancel();
steeringQueue?.cancel();
clearActiveEmbeddedRun(params.sessionId, handle, params.sessionKey);
}
}