@@ -45,6 +45,7 @@ type ParentState = {
4545 agentId ?: string ;
4646 taskRuntime ?: AgentHarnessTaskRuntime ;
4747 mirror ?: CodexNativeSubagentTaskMirror ;
48+ deferredSettlement ?: ( ) => Promise < void > | void ;
4849 deliveredCompletionKeys : Set < string > ;
4950} ;
5051
@@ -62,6 +63,7 @@ type ChildState = {
6263 completionDeliveryTimer ?: ReturnType < typeof setTimeout > ;
6364 deliveringCompletionKey ?: string ;
6465 noFinalCompletionFallbackTimer ?: ReturnType < typeof setTimeout > ;
66+ settledWithoutCompletion : boolean ;
6567} ;
6668
6769type ChildAssistantMessages = {
@@ -111,7 +113,7 @@ export function registerCodexNativeSubagentMonitor(params: {
111113 agentId ?: string ;
112114 codexHome ?: string ;
113115 runtime ?: NativeSubagentMonitorRuntime ;
114- } ) : void {
116+ } ) : CodexNativeSubagentMonitor {
115117 let monitor = monitors . get ( params . client ) ;
116118 if ( ! monitor ) {
117119 monitor = new CodexNativeSubagentMonitor ( params . client , params . runtime ?? defaultRuntime , {
@@ -127,6 +129,7 @@ export function registerCodexNativeSubagentMonitor(params: {
127129 taskRuntimeScope : params . taskRuntimeScope ,
128130 agentId : params . agentId ,
129131 } ) ;
132+ return monitor ;
130133}
131134
132135/** Tracks native subagent thread notifications, transcript completions, and task delivery. */
@@ -168,6 +171,18 @@ export class CodexNativeSubagentMonitor {
168171 this . transcriptPathsByChildThreadId . clear ( ) ;
169172 }
170173
174+ deferUntilParentSettles ( parentThreadId : string , callback : ( ) => Promise < void > | void ) : boolean {
175+ const normalizedParentThreadId = parentThreadId . trim ( ) ;
176+ const state = this . parentStates . get ( normalizedParentThreadId ) ;
177+ if ( ! state || ! this . hasUnsettledChildren ( normalizedParentThreadId ) ) {
178+ return false ;
179+ }
180+ // A yielded one-shot turn must keep this monitor alive until its child
181+ // result reaches the parent; cleanup ownership transfers back afterward.
182+ state . deferredSettlement = callback ;
183+ return true ;
184+ }
185+
171186 configure ( options : MonitorOptions ) : void {
172187 const codexHome = normalizeOptionalString ( options . codexHome ) ;
173188 if ( codexHome ) {
@@ -222,11 +237,42 @@ export class CodexNativeSubagentMonitor {
222237 } ) ;
223238 }
224239 }
240+ this . markChildTurnStarted ( notification ) ;
241+ await this . handleChildSystemError ( notification ) ;
225242 this . captureChildAssistantMessage ( notification ) ;
226243 await this . handleChildTurnCompletion ( notification ) ;
227244 await this . handleCompletionNotification ( notification ) ;
228245 }
229246
247+ private markChildTurnStarted ( notification : CodexServerNotification ) : void {
248+ if ( notification . method !== "turn/started" ) {
249+ return ;
250+ }
251+ const params = isJsonObject ( notification . params ) ? notification . params : undefined ;
252+ const childThreadId = readString ( params , "threadId" ) ?. trim ( ) ;
253+ const childState = childThreadId ? this . childStates . get ( childThreadId ) : undefined ;
254+ if ( childState ) {
255+ childState . settledWithoutCompletion = false ;
256+ }
257+ }
258+
259+ private async handleChildSystemError ( notification : CodexServerNotification ) : Promise < void > {
260+ if ( notification . method !== "thread/status/changed" ) {
261+ return ;
262+ }
263+ const params = isJsonObject ( notification . params ) ? notification . params : undefined ;
264+ const status = isJsonObject ( params ?. status ) ? params . status : undefined ;
265+ if ( readString ( status , "type" ) !== "systemError" ) {
266+ return ;
267+ }
268+ const childThreadId = readString ( params , "threadId" ) ?. trim ( ) ;
269+ const childState = childThreadId ? this . childStates . get ( childThreadId ) : undefined ;
270+ if ( childState ) {
271+ childState . settledWithoutCompletion = true ;
272+ await this . flushDeferredParentSettlements ( childState . parentThreadId ) ;
273+ }
274+ }
275+
230276 private ensureParentTaskRuntime ( state : ParentState ) : void {
231277 if ( state . taskRuntime || ! state . requesterSessionKey || ! state . taskRuntimeScope ) {
232278 return ;
@@ -434,6 +480,10 @@ export class CodexNativeSubagentMonitor {
434480 if ( turnId ) {
435481 childState . assistantMessagesByTurn . delete ( turnId ) ;
436482 }
483+ // Codex keeps interrupted agents resumable but intentionally sends no
484+ // parent completion, so one-shot cleanup may settle until another turn starts.
485+ childState . settledWithoutCompletion = true ;
486+ await this . flushDeferredParentSettlements ( childState . parentThreadId ) ;
437487 return ;
438488 }
439489 if ( childState && turn ) {
@@ -516,6 +566,7 @@ export class CodexNativeSubagentMonitor {
516566 }
517567 }
518568 if ( ! state . requesterSessionKey ) {
569+ await this . flushDeferredParentSettlements ( state . parentThreadId ) ;
519570 return ;
520571 }
521572 const completionKey = buildCompletionDedupeKey ( state . parentThreadId , completion ) ;
@@ -585,6 +636,7 @@ export class CodexNativeSubagentMonitor {
585636 } ) ;
586637 } finally {
587638 childState . deliveringCompletionKey = undefined ;
639+ await this . flushDeferredParentSettlements ( state . parentThreadId ) ;
588640 }
589641 }
590642
@@ -635,6 +687,47 @@ export class CodexNativeSubagentMonitor {
635687 unrefTimer ( childState . completionDeliveryTimer ) ;
636688 }
637689
690+ private hasUnsettledChildren ( parentThreadId : string ) : boolean {
691+ for ( const childState of this . childStates . values ( ) ) {
692+ if (
693+ childState . parentThreadId === parentThreadId &&
694+ ( childState . pendingCompletion !== undefined ||
695+ childState . deliveringCompletionKey !== undefined ||
696+ ( ! childState . transcriptTerminal && ! childState . settledWithoutCompletion ) )
697+ ) {
698+ return true ;
699+ }
700+ }
701+ return false ;
702+ }
703+
704+ private async flushDeferredParentSettlements ( parentThreadId : string ) : Promise < void > {
705+ if ( this . hasUnsettledChildren ( parentThreadId ) ) {
706+ return ;
707+ }
708+ const state = this . parentStates . get ( parentThreadId ) ;
709+ const callback = state ?. deferredSettlement ;
710+ if ( ! state || ! callback ) {
711+ return ;
712+ }
713+ state . deferredSettlement = undefined ;
714+ await this . runDeferredParentSettlement ( parentThreadId , callback ) ;
715+ }
716+
717+ private async runDeferredParentSettlement (
718+ parentThreadId : string ,
719+ callback : ( ) => Promise < void > | void ,
720+ ) : Promise < void > {
721+ try {
722+ await callback ( ) ;
723+ } catch ( error ) {
724+ embeddedAgentLog . warn ( "Failed to finish deferred Codex app-server cleanup" , {
725+ parentThreadId,
726+ error : formatErrorMessage ( error ) ,
727+ } ) ;
728+ }
729+ }
730+
638731 private finalizeCompletionTask ( completion : CodexNativeSubagentCompletion , eventAt : number ) : void {
639732 const taskRuntime = this . getTaskRuntimeForChild ( completion . childThreadId ) ;
640733 if ( ! taskRuntime ) {
@@ -701,6 +794,7 @@ export class CodexNativeSubagentMonitor {
701794 transcriptPollAttempt : 0 ,
702795 transcriptTerminal : false ,
703796 completionDeliveryAttempt : 0 ,
797+ settledWithoutCompletion : false ,
704798 } ;
705799 this . childStates . set ( normalizedChildThreadId , childState ) ;
706800 }
0 commit comments