@@ -116,7 +116,11 @@ import {
116116 defaultLeasedCodexAppServerClientFactory ,
117117 type CodexAppServerClientFactory ,
118118} from "./client-factory.js" ;
119- import { isCodexAppServerApprovalRequest , type CodexAppServerClient } from "./client.js" ;
119+ import {
120+ CodexAppServerRpcError ,
121+ isCodexAppServerApprovalRequest ,
122+ type CodexAppServerClient ,
123+ } from "./client.js" ;
120124import {
121125 isCodexAppServerApprovalPolicyAllowedByRequirements ,
122126 isCodexSandboxExecServerEnabled ,
@@ -194,15 +198,16 @@ import {
194198 assertCodexTurnStartResponse ,
195199 readCodexDynamicToolCallParams ,
196200} from "./protocol-validators.js" ;
197- import type {
198- CodexSandboxPolicy ,
199- CodexTurnEnvironmentParams ,
200- CodexServerNotification ,
201- CodexDynamicToolCallParams ,
202- CodexDynamicToolCallResponse ,
203- CodexTurnStartResponse ,
204- JsonObject ,
205- JsonValue ,
201+ import {
202+ isJsonObject ,
203+ type CodexSandboxPolicy ,
204+ type CodexTurnEnvironmentParams ,
205+ type CodexServerNotification ,
206+ type CodexDynamicToolCallParams ,
207+ type CodexDynamicToolCallResponse ,
208+ type CodexTurnStartResponse ,
209+ type JsonObject ,
210+ type JsonValue ,
206211} from "./protocol.js" ;
207212import { releaseCodexSandboxExecServerEnvironment } from "./sandbox-exec-server.js" ;
208213import {
@@ -249,6 +254,7 @@ import { createCodexUserInputBridge } from "./user-input-bridge.js";
249254
250255const CODEX_NATIVE_HOOK_RELAY_RENEW_INTERVAL_MS = 60_000 ;
251256const CODEX_APP_SERVER_PROJECTED_CHARS_PER_TOKEN = 4 ;
257+ const CODEX_APP_SERVER_ACTIVE_NATIVE_TURN_WAIT_TIMEOUT_MS = 30_000 ;
252258const ensuredCodexWorkspaceDirs = new Set < string > ( ) ;
253259
254260function estimateCodexAppServerProjectedTurnTokens ( params : {
@@ -1490,6 +1496,48 @@ export async function runCodexAppServerAttempt(
14901496 }
14911497 }
14921498 } ;
1499+ let activeNativeTurnCompletionWaiter :
1500+ | { matches : ( notification : CodexServerNotification ) => boolean ; resolve : ( ) => void }
1501+ | undefined ;
1502+ const waitForActiveNativeTurnCompletion = async (
1503+ turnIds ?: readonly string [ ] ,
1504+ ) : Promise < boolean > => {
1505+ const turnIdSet = turnIds ?. length ? new Set ( turnIds ) : undefined ;
1506+ const matchesCompletion = ( notification : CodexServerNotification ) =>
1507+ isCodexThreadTurnCompletedNotification ( notification , thread . threadId , turnIdSet ) ;
1508+ if ( pendingNotifications . some ( ( notification ) => matchesCompletion ( notification ) ) ) {
1509+ return true ;
1510+ }
1511+ return await new Promise < boolean > ( ( resolve ) => {
1512+ let settled = false ;
1513+ const timeoutRef : { current ?: ReturnType < typeof setTimeout > } = { } ;
1514+ const finish = ( completedNativeTurn : boolean ) => {
1515+ if ( settled ) {
1516+ return ;
1517+ }
1518+ settled = true ;
1519+ if ( timeoutRef . current ) {
1520+ clearTimeout ( timeoutRef . current ) ;
1521+ }
1522+ runAbortController . signal . removeEventListener ( "abort" , abortListener ) ;
1523+ if ( activeNativeTurnCompletionWaiter ?. resolve === finishComplete ) {
1524+ activeNativeTurnCompletionWaiter = undefined ;
1525+ }
1526+ resolve ( completedNativeTurn ) ;
1527+ } ;
1528+ const finishComplete = ( ) => finish ( true ) ;
1529+ const abortListener = ( ) => finish ( false ) ;
1530+ timeoutRef . current = setTimeout (
1531+ ( ) => finish ( false ) ,
1532+ Math . min ( appServer . requestTimeoutMs , CODEX_APP_SERVER_ACTIVE_NATIVE_TURN_WAIT_TIMEOUT_MS ) ,
1533+ ) ;
1534+ activeNativeTurnCompletionWaiter = {
1535+ matches : matchesCompletion ,
1536+ resolve : finishComplete ,
1537+ } ;
1538+ runAbortController . signal . addEventListener ( "abort" , abortListener , { once : true } ) ;
1539+ } ) ;
1540+ } ;
14931541 const enqueueNotification = ( notification : CodexServerNotification ) : Promise < void > => {
14941542 const projector = projectorRef . current ;
14951543 const turnId = turnIdRef . current ;
@@ -1512,6 +1560,11 @@ export async function runCodexAppServerAttempt(
15121560 ) ;
15131561 }
15141562 }
1563+ if ( notification . method === "turn/completed" && correlation . matchesActiveThread ) {
1564+ if ( activeNativeTurnCompletionWaiter ?. matches ( notification ) ) {
1565+ activeNativeTurnCompletionWaiter . resolve ( ) ;
1566+ }
1567+ }
15151568 if ( isCodexNotificationOutsideActiveRun ( correlation ) ) {
15161569 return Promise . resolve ( ) ;
15171570 }
@@ -1918,6 +1971,32 @@ export async function runCodexAppServerAttempt(
19181971 throwIfTurnStartAcceptedAfterAbort ( ) ;
19191972 return startedTurn ;
19201973 } ;
1974+ const activeNativeTurnIds =
1975+ thread . lifecycle . action === "resumed" ? ( thread . lifecycle . activeTurnIds ?? [ ] ) : [ ] ;
1976+ if ( activeNativeTurnIds . length > 0 ) {
1977+ // A resumed Codex thread can already be running a native compact/review turn.
1978+ // Starting an OpenClaw turn before that native turn completes can wedge the
1979+ // accepted turn behind a completion event we intentionally ignore.
1980+ embeddedAgentLog . info (
1981+ "codex app-server resumed thread has active native turn; waiting before turn/start" ,
1982+ { threadId : thread . threadId , activeTurnIds : activeNativeTurnIds } ,
1983+ ) ;
1984+ emitCodexAppServerEvent ( params , {
1985+ stream : "codex_app_server.lifecycle" ,
1986+ data : {
1987+ phase : "turn_start_waiting_for_native_turn" ,
1988+ threadId : thread . threadId ,
1989+ activeTurnIds : activeNativeTurnIds ,
1990+ } ,
1991+ } ) ;
1992+ const nativeTurnCompleted = await waitForActiveNativeTurnCompletion ( activeNativeTurnIds ) ;
1993+ if ( ! nativeTurnCompleted && ! runAbortController . signal . aborted ) {
1994+ embeddedAgentLog . warn (
1995+ "codex app-server active native turn did not complete before turn/start wait timed out" ,
1996+ { threadId : thread . threadId , activeTurnIds : activeNativeTurnIds } ,
1997+ ) ;
1998+ }
1999+ }
19212000 try {
19222001 codexModelCallDiagnostics . emitStarted ( ) ;
19232002 runAgentHarnessLlmInputHook ( {
@@ -1932,7 +2011,29 @@ export async function runCodexAppServerAttempt(
19322011 turn = await startCodexTurn ( ) ;
19332012 } catch ( error ) {
19342013 let turnStartError = error ;
2014+ if ( isCodexActiveCompactTurnError ( turnStartError ) ) {
2015+ // Codex native compaction returns before its compact turn finishes. If
2016+ // the next OpenClaw turn collides with that compact turn, wait for the
2017+ // terminal notification and retry once instead of surfacing drift.
2018+ embeddedAgentLog . info (
2019+ "codex app-server turn/start blocked by active compact turn; waiting to retry" ,
2020+ { threadId : thread . threadId } ,
2021+ ) ;
2022+ const compactTurnCompleted = await waitForActiveNativeTurnCompletion ( ) ;
2023+ if ( compactTurnCompleted && ! runAbortController . signal . aborted ) {
2024+ emitCodexAppServerEvent ( params , {
2025+ stream : "codex_app_server.lifecycle" ,
2026+ data : { phase : "turn_start_retry_after_compact" , threadId : thread . threadId } ,
2027+ } ) ;
2028+ try {
2029+ turn = await startCodexTurn ( ) ;
2030+ } catch ( retryError ) {
2031+ turnStartError = retryError ;
2032+ }
2033+ }
2034+ }
19352035 if (
2036+ turn === undefined &&
19362037 shouldUseFreshCodexThreadAfterContextEngineOverflow ( {
19372038 error : turnStartError ,
19382039 contextEngineActive : Boolean ( activeContextEngine ) ,
@@ -2695,6 +2796,34 @@ function isCodexContextWindowError(error: unknown): boolean {
26952796 ) ;
26962797}
26972798
2799+ function isCodexActiveCompactTurnError ( error : unknown ) : boolean {
2800+ if ( ! ( error instanceof CodexAppServerRpcError ) ) {
2801+ return false ;
2802+ }
2803+ const data = isJsonObject ( error . data ) ? error . data : undefined ;
2804+ const codexErrorInfo = isJsonObject ( data ?. codexErrorInfo ) ? data . codexErrorInfo : undefined ;
2805+ const activeTurn = isJsonObject ( codexErrorInfo ?. activeTurnNotSteerable )
2806+ ? codexErrorInfo . activeTurnNotSteerable
2807+ : undefined ;
2808+ return activeTurn ?. turnKind === "compact" ;
2809+ }
2810+
2811+ function isCodexThreadTurnCompletedNotification (
2812+ notification : CodexServerNotification ,
2813+ threadId : string ,
2814+ turnIds ?: ReadonlySet < string > ,
2815+ ) : boolean {
2816+ if ( notification . method !== "turn/completed" ) {
2817+ return false ;
2818+ }
2819+ const correlation = describeCodexNotificationCorrelation ( notification , { threadId } ) ;
2820+ if ( ! correlation . matchesActiveThread ) {
2821+ return false ;
2822+ }
2823+ const turnId = correlation . turnId ?? correlation . nestedTurnId ;
2824+ return ! turnIds || ( turnId !== undefined && turnIds . has ( turnId ) ) ;
2825+ }
2826+
26982827function joinPresentSections ( ...sections : Array < string | undefined > ) : string {
26992828 return sections . filter ( ( section ) : section is string => Boolean ( section ?. trim ( ) ) ) . join ( "\n\n" ) ;
27002829}
0 commit comments