@@ -105,6 +105,8 @@ const McpServerElicitationRequest = new RequestType<
105105 void
106106> ( 'mcpServer/elicitation/request' ) ;
107107
108+ const GOAL_TURN_START_GRACE_MS = 1_000 ;
109+
108110/**
109111 * A type-safe client over the Codex App Server's JSON-RPC API.
110112 * Maps each request to its expected response and exposes clear, typed methods for supported JSON-RPC operations.
@@ -268,14 +270,18 @@ export class CodexAppServerClient {
268270 }
269271 }
270272
271- async runGoalSet ( params : ThreadGoalSetParams , onTurnStarted ?: ( turnId : string ) => void ) : Promise < TurnCompletedNotification > {
273+ async runGoalSet (
274+ params : ThreadGoalSetParams ,
275+ onTurnStarted ?: ( turnId : string ) => void ,
276+ turnStartGraceMs = GOAL_TURN_START_GRACE_MS ,
277+ ) : Promise < TurnCompletedNotification | null > {
272278 const capturedCompletions : Array < TurnCompletedNotification > = [ ] ;
273279 const releaseCompletionCapture = this . captureTurnCompletions ( params . threadId , ( event ) => {
274280 capturedCompletions . push ( event ) ;
275281 } ) ;
276282 let goalTurnId : string | null = null ;
277- let resolveGoalTurnStarted : ( ) => void = ( ) => { } ;
278- const goalTurnStarted = new Promise < void > ( ( resolve ) => {
283+ let resolveGoalTurnStarted : ( turnId : string ) => void = ( ) => { } ;
284+ const goalTurnStarted = new Promise < string > ( ( resolve ) => {
279285 resolveGoalTurnStarted = resolve ;
280286 } ) ;
281287 const releaseRoutingCapture = this . captureTurnRoutings ( params . threadId , ( turnId ) => {
@@ -284,17 +290,14 @@ export class CodexAppServerClient {
284290 }
285291 goalTurnId = turnId ;
286292 onTurnStarted ?.( turnId ) ;
287- resolveGoalTurnStarted ( ) ;
293+ resolveGoalTurnStarted ( turnId ) ;
288294 } ) ;
289295
290296 try {
291297 await this . threadGoalSet ( params ) ;
292- if ( goalTurnId === null ) {
293- await goalTurnStarted ;
294- }
295- const turnId = goalTurnId ;
298+ const turnId = goalTurnId ?? await this . waitForGoalTurnStarted ( goalTurnStarted , turnStartGraceMs ) ;
296299 if ( turnId === null ) {
297- throw new Error ( "Goal command did not start a turn" ) ;
300+ return null ;
298301 }
299302 const earlyCompletion = capturedCompletions . find ( event => event . turn . id === turnId ) ;
300303 releaseCompletionCapture ( ) ;
@@ -309,6 +312,20 @@ export class CodexAppServerClient {
309312 }
310313 }
311314
315+ private async waitForGoalTurnStarted ( goalTurnStarted : Promise < string > , timeoutMs : number ) : Promise < string | null > {
316+ let timeout : ReturnType < typeof setTimeout > | null = null ;
317+ const timeoutPromise = new Promise < null > ( ( resolve ) => {
318+ timeout = setTimeout ( ( ) => resolve ( null ) , timeoutMs ) ;
319+ } ) ;
320+ try {
321+ return await Promise . race ( [ goalTurnStarted , timeoutPromise ] ) ;
322+ } finally {
323+ if ( timeout !== null ) {
324+ clearTimeout ( timeout ) ;
325+ }
326+ }
327+ }
328+
312329 async runCompact ( params : ThreadCompactStartParams ) : Promise < CompactionCompletedNotification > {
313330 const compactionCompleted = this . awaitCompactionCompleted ( params . threadId ) ;
314331 await this . threadCompactStart ( params ) ;
0 commit comments