@@ -109,6 +109,9 @@ const McpServerElicitationRequest = new RequestType<
109109 void
110110> ( 'mcpServer/elicitation/request' ) ;
111111
112+ const GOAL_RUNTIME_EFFECTS_GRACE_MS = 1_000 ;
113+ const GOAL_TURN_COMPLETION_GRACE_MS = 250 ;
114+
112115/**
113116 * A type-safe client over the Codex App Server's JSON-RPC API.
114117 * Maps each request to its expected response and exposes clear, typed methods for supported JSON-RPC operations.
@@ -282,90 +285,180 @@ export class CodexAppServerClient {
282285 async runGoalSet (
283286 params : ThreadGoalSetParams ,
284287 onTurnStarted ?: ( turnId : string ) => void ,
288+ runtimeEffectsGraceMs = GOAL_RUNTIME_EFFECTS_GRACE_MS ,
289+ turnCompletionGraceMs = GOAL_TURN_COMPLETION_GRACE_MS ,
285290 ) : Promise < TurnCompletedNotification | null > {
291+ let goalTurnId : string | null = null ;
286292 const capturedCompletions : Array < TurnCompletedNotification > = [ ] ;
293+ let resolveGoalTurnCompleted : ( event : TurnCompletedNotification ) => void = ( ) => { } ;
294+ const goalTurnCompleted = new Promise < TurnCompletedNotification > ( ( resolve ) => {
295+ resolveGoalTurnCompleted = resolve ;
296+ } ) ;
287297 const releaseCompletionCapture = this . captureTurnCompletions ( params . threadId , ( event ) => {
288298 capturedCompletions . push ( event ) ;
299+ if ( goalTurnId === event . turn . id ) {
300+ resolveGoalTurnCompleted ( event ) ;
301+ }
289302 } ) ;
290- let goalTurnId : string | null = null ;
291303 let resolveGoalTurnStarted : ( turnId : string ) => void = ( ) => { } ;
292304 const goalTurnStarted = new Promise < string > ( ( resolve ) => {
293305 resolveGoalTurnStarted = resolve ;
294306 } ) ;
295- let resolveNoGoalTurnStarted : ( ) => void = ( ) => { } ;
296- const noGoalTurnStarted = new Promise < null > ( ( resolve ) => {
297- resolveNoGoalTurnStarted = ( ) => resolve ( null ) ;
298- } ) ;
299- let acceptNoGoalTurnStatus = false ;
307+ let goalUpdateHandled = false ;
300308 let expectedGoal : ThreadGoal | null = null ;
301- let resolveGoalUpdated : ( ) => void = ( ) => { } ;
302- const goalUpdated = new Promise < void > ( ( resolve ) => {
303- resolveGoalUpdated = resolve ;
304- } ) ;
309+ const noGoalTurnStarted = this . createNoGoalTurnStartedPromise ( runtimeEffectsGraceMs ) ;
305310 const capturedGoalUpdates : Array < ThreadGoalUpdatedNotification > = [ ] ;
306311 const releaseRoutingCapture = this . captureTurnRoutings ( params . threadId , ( turnId ) => {
307- if ( goalTurnId !== null ) {
312+ if ( ! goalUpdateHandled || goalTurnId !== null ) {
308313 return ;
309314 }
310315 goalTurnId = turnId ;
311316 onTurnStarted ?.( turnId ) ;
312317 resolveGoalTurnStarted ( turnId ) ;
313318 } ) ;
314- const releaseStatusCapture = this . captureThreadStatuses ( params . threadId , ( status ) => {
315- if ( ! acceptNoGoalTurnStatus || goalTurnId !== null || status . type === "active" ) {
316- return ;
317- }
318- resolveNoGoalTurnStarted ( ) ;
319- } ) ;
320319 const releaseGoalUpdateCapture = this . captureThreadGoalUpdates ( params . threadId , ( event ) => {
321320 capturedGoalUpdates . push ( event ) ;
322321 if ( expectedGoal !== null && goalsMatch ( event . goal , expectedGoal ) ) {
323- resolveGoalUpdated ( ) ;
322+ goalUpdateHandled = true ;
323+ noGoalTurnStarted . goalUpdated ( ) ;
324+ }
325+ } ) ;
326+ const releaseStatusCapture = this . captureThreadStatuses ( params . threadId , ( status ) => {
327+ if ( ! goalUpdateHandled || goalTurnId !== null ) {
328+ return ;
324329 }
330+ noGoalTurnStarted . threadStatusChanged ( status ) ;
325331 } ) ;
326332
327333 try {
328334 const goalSetResponse = await this . threadGoalSet ( params ) ;
329335 expectedGoal = goalSetResponse . goal ;
330336 if ( capturedGoalUpdates . some ( event => goalsMatch ( event . goal , expectedGoal ! ) ) ) {
331- resolveGoalUpdated ( ) ;
332- }
333- if ( goalTurnId === null ) {
334- const threadResponse = await this . threadRead ( { threadId : params . threadId } ) ;
335- if ( goalTurnId === null && threadResponse . thread . status . type !== "active" ) {
336- await goalUpdated ;
337- if ( goalTurnId === null ) {
338- resolveNoGoalTurnStarted ( ) ;
339- }
340- } else {
341- acceptNoGoalTurnStatus = true ;
342- }
343- }
344- let turnId = goalTurnId ?? await Promise . race ( [ goalTurnStarted , noGoalTurnStarted ] ) ;
345- if ( turnId === null ) {
346- await goalUpdated ;
347- turnId = goalTurnId ;
348- if ( turnId === null ) {
349- return null ;
350- }
337+ goalUpdateHandled = true ;
338+ noGoalTurnStarted . goalUpdated ( ) ;
351339 }
352- const earlyCompletion = capturedCompletions . find ( event => event . turn . id === turnId ) ;
353- releaseCompletionCapture ( ) ;
340+ const turnId = goalTurnId ?? await Promise . race ( [ goalTurnStarted , noGoalTurnStarted . promise ] ) ;
341+ noGoalTurnStarted . release ( ) ;
354342 releaseRoutingCapture ( ) ;
355343 releaseStatusCapture ( ) ;
356344 releaseGoalUpdateCapture ( ) ;
345+ if ( turnId === null ) {
346+ return null ;
347+ }
348+ const earlyCompletion = capturedCompletions . find ( event => event . turn . id === turnId ) ;
357349 if ( earlyCompletion ) {
358350 return earlyCompletion ;
359351 }
360- return await this . awaitTurnCompleted ( params . threadId , turnId ) ;
352+ const completionGrace = this . createGoalTurnCompletionGracePromise ( turnCompletionGraceMs ) ;
353+ try {
354+ return await Promise . race ( [ goalTurnCompleted , completionGrace . promise ] ) ;
355+ } finally {
356+ completionGrace . release ( ) ;
357+ }
361358 } finally {
359+ noGoalTurnStarted . release ( ) ;
362360 releaseCompletionCapture ( ) ;
363361 releaseRoutingCapture ( ) ;
364362 releaseStatusCapture ( ) ;
365363 releaseGoalUpdateCapture ( ) ;
366364 }
367365 }
368366
367+ private createNoGoalTurnStartedPromise (
368+ runtimeEffectsGraceMs : number ,
369+ ) : {
370+ promise : Promise < null > ,
371+ release : ( ) => void ,
372+ goalUpdated : ( ) => void ,
373+ threadStatusChanged : ( status : ThreadStatus ) => void ,
374+ } {
375+ let released = false ;
376+ let resolved = false ;
377+ let goalUpdated = false ;
378+ let activeAfterGoalUpdate = false ;
379+ let timeout : ReturnType < typeof setTimeout > | null = null ;
380+ let resolveNoGoalTurnStarted : ( ) => void = ( ) => { } ;
381+ const clearTimer = ( ) => {
382+ if ( timeout !== null ) {
383+ clearTimeout ( timeout ) ;
384+ timeout = null ;
385+ }
386+ } ;
387+ const resolveNoTurn = ( ) => {
388+ if ( released || resolved ) {
389+ return ;
390+ }
391+ resolved = true ;
392+ clearTimer ( ) ;
393+ resolveNoGoalTurnStarted ( ) ;
394+ } ;
395+ const scheduleNoTurnTimer = ( ) => {
396+ if ( released || resolved || ! goalUpdated || activeAfterGoalUpdate || timeout !== null ) {
397+ return ;
398+ }
399+ timeout = setTimeout ( resolveNoTurn , runtimeEffectsGraceMs ) ;
400+ } ;
401+ const release = ( ) => {
402+ if ( released ) {
403+ return ;
404+ }
405+ released = true ;
406+ clearTimer ( ) ;
407+ } ;
408+ const promise = new Promise < null > ( ( resolve ) => {
409+ resolveNoGoalTurnStarted = ( ) => {
410+ resolve ( null ) ;
411+ } ;
412+ } ) ;
413+ const handleGoalUpdated = ( ) => {
414+ goalUpdated = true ;
415+ scheduleNoTurnTimer ( ) ;
416+ } ;
417+ const handleThreadStatusChanged = ( status : ThreadStatus ) => {
418+ if ( ! goalUpdated || released || resolved ) {
419+ return ;
420+ }
421+ if ( status . type === "active" ) {
422+ activeAfterGoalUpdate = true ;
423+ clearTimer ( ) ;
424+ return ;
425+ }
426+ if ( activeAfterGoalUpdate ) {
427+ resolveNoTurn ( ) ;
428+ }
429+ } ;
430+ return {
431+ promise,
432+ release,
433+ goalUpdated : handleGoalUpdated ,
434+ threadStatusChanged : handleThreadStatusChanged ,
435+ } ;
436+ }
437+
438+ private createGoalTurnCompletionGracePromise (
439+ turnCompletionGraceMs : number ,
440+ ) : { promise : Promise < null > , release : ( ) => void } {
441+ let released = false ;
442+ let timeout : ReturnType < typeof setTimeout > | null = null ;
443+ const release = ( ) => {
444+ if ( released ) {
445+ return ;
446+ }
447+ released = true ;
448+ if ( timeout !== null ) {
449+ clearTimeout ( timeout ) ;
450+ }
451+ } ;
452+ const promise = new Promise < null > ( ( resolve ) => {
453+ const resolveNoGoalTurnCompleted = ( ) => {
454+ timeout = null ;
455+ resolve ( null ) ;
456+ } ;
457+ timeout = setTimeout ( resolveNoGoalTurnCompleted , turnCompletionGraceMs ) ;
458+ } ) ;
459+ return { promise, release} ;
460+ }
461+
369462 async runCompact ( params : ThreadCompactStartParams ) : Promise < CompactionCompletedNotification > {
370463 const compactionCompleted = this . awaitCompactionCompleted ( params . threadId ) ;
371464 await this . threadCompactStart ( params ) ;
0 commit comments