diff --git a/src/backend/codex-stdio.ts b/src/backend/codex-stdio.ts index 370db5b..1aefa66 100644 --- a/src/backend/codex-stdio.ts +++ b/src/backend/codex-stdio.ts @@ -470,6 +470,20 @@ async function runCodexStdioTurnWithSession(options: CodexStdioTurnOptions, sess let terminal: { status: TerminalStatus; failureKind: FailureKind | null; message: string | null } | null = null; let terminalResolve!: () => void; const terminalPromise = new Promise((resolve) => { terminalResolve = resolve; }); + const terminalNotificationDrainMs = codexTerminalNotificationDrainMs(env); + let terminalNotificationDrainTimeout: NodeJS.Timeout | null = null; + const resolveTerminalAfterNotificationDrain = (): void => { + if (terminalNotificationDrainTimeout) clearTimeout(terminalNotificationDrainTimeout); + if (terminalNotificationDrainMs <= 0) { + terminalResolve(); + return; + } + terminalNotificationDrainTimeout = setTimeout(() => { + terminalNotificationDrainTimeout = null; + terminalResolve(); + }, terminalNotificationDrainMs); + terminalNotificationDrainTimeout.unref?.(); + }; let client: CodexStdioClient | null = null; const requestTimeoutMs = Math.min(positiveTimeout(options.timeoutMs), requestTimeoutCapMs); let stopActiveTurn: (() => void) | undefined; @@ -676,7 +690,7 @@ async function runCodexStdioTurnWithSession(options: CodexStdioTurnOptions, sess if (normalized.terminalEvents) deferredTerminalEvents.push(...normalized.terminalEvents); if (normalized.terminal && !terminal) { terminal = normalized.terminal; - terminalResolve(); + resolveTerminalAfterNotificationDrain(); } }); try { @@ -1487,6 +1501,12 @@ function codexMissingTerminalAfterToolTimeoutMs(env: NodeJS.ProcessEnv, turnTime return positiveTimeout(turnTimeoutMs); } +function codexTerminalNotificationDrainMs(env: NodeJS.ProcessEnv): number { + const configured = Number(env.AGENTRUN_CODEX_TERMINAL_NOTIFICATION_DRAIN_MS); + if (Number.isFinite(configured) && configured >= 0) return Math.floor(configured); + return 250; +} + function emitCodexNotificationOtel(options: CodexStdioTurnOptions, env: NodeJS.ProcessEnv, message: JsonRecord, state: JsonRecord): void { const attributes = { ...state, ...notificationOtelAttributes(message) }; emitCodexOtelSpan("codex_stdio.notification", options, env, attributes); diff --git a/src/selftest/fake-codex-app-server.ts b/src/selftest/fake-codex-app-server.ts index eb0cdf8..0886c12 100644 --- a/src/selftest/fake-codex-app-server.ts +++ b/src/selftest/fake-codex-app-server.ts @@ -226,12 +226,14 @@ for await (const line of rl) { if (mode === "multi-agent-message-terminal-before-final") { turnCounter += 1; const turn = { id: `turn_selftest_${turnCounter}`, status: "completed" }; + respond(message.id, { turn }); notify("turn/started", { turn }); notify("item/agentMessage/delta", { itemId: "msg_late_progress", delta: "Progress before delayed final. " }); notify("turn/completed", { turn }); - notify("item/completed", { item: { id: "msg_late_progress", type: "agentMessage", text: "Progress before delayed final." } }); - notify("item/completed", { item: { id: "msg_late_final", type: "agentMessage", text: "Delayed final answer." } }); - respond(message.id, { turn }); + setTimeout(() => { + notify("item/completed", { item: { id: "msg_late_progress", type: "agentMessage", text: "Progress before delayed final." } }); + notify("item/completed", { item: { id: "msg_late_final", type: "agentMessage", text: "Delayed final answer." } }); + }, 20).unref?.(); continue; } if (mode === "web-search-progress") {