diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c0833aedb..e63be104c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index c8955c8c847..8d81370b30e 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -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(); diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index ee763dfd190..34ae4f0a2e4 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -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 | undefined; + let steeringQueue: ReturnType | 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); } }