@@ -26,6 +26,8 @@ import type {
2626 SkillsExtraRootsSetParams ,
2727 SkillsListParams ,
2828 SkillsListResponse ,
29+ ThreadStatus ,
30+ ThreadStatusChangedNotification ,
2931 ThreadArchiveParams ,
3032 ThreadArchiveResponse ,
3133 ThreadCompactStartParams ,
@@ -105,8 +107,6 @@ const McpServerElicitationRequest = new RequestType<
105107 void
106108> ( 'mcpServer/elicitation/request' ) ;
107109
108- const GOAL_TURN_START_GRACE_MS = 1_000 ;
109-
110110/**
111111 * A type-safe client over the Codex App Server's JSON-RPC API.
112112 * Maps each request to its expected response and exposes clear, typed methods for supported JSON-RPC operations.
@@ -122,6 +122,7 @@ export class CodexAppServerClient {
122122 private readonly pendingCompactionCompletionResolvers = new Map < string , Set < ( event : CompactionCompletedNotification ) => void > > ( ) ;
123123 private readonly turnCompletionCaptures = new Map < string , Set < ( event : TurnCompletedNotification ) => void > > ( ) ;
124124 private readonly turnRoutingCaptures = new Map < string , Set < ( turnId : string ) => void > > ( ) ;
125+ private readonly threadStatusCaptures = new Map < string , Set < ( status : ThreadStatus ) => void > > ( ) ;
125126 private readonly staleTurnIds = new Map < string , Set < string > > ( ) ;
126127
127128 constructor ( connection : MessageConnection ) {
@@ -143,6 +144,9 @@ export class CodexAppServerClient {
143144 if ( isCompactionCompletedNotification ( serverNotification ) ) {
144145 this . recordCompactionCompleted ( serverNotification ) ;
145146 }
147+ if ( isThreadStatusChangedNotification ( serverNotification ) ) {
148+ this . recordThreadStatusChanged ( serverNotification . params ) ;
149+ }
146150 const routing = extractTurnRouting ( serverNotification ) ;
147151 if ( this . handleStaleTurnNotification ( serverNotification , routing ) ) {
148152 return ;
@@ -272,7 +276,6 @@ export class CodexAppServerClient {
272276 async runGoalSet (
273277 params : ThreadGoalSetParams ,
274278 onTurnStarted ?: ( turnId : string ) => void ,
275- turnStartGraceMs = GOAL_TURN_START_GRACE_MS ,
276279 ) : Promise < TurnCompletedNotification | null > {
277280 const capturedCompletions : Array < TurnCompletedNotification > = [ ] ;
278281 const releaseCompletionCapture = this . captureTurnCompletions ( params . threadId , ( event ) => {
@@ -283,6 +286,10 @@ export class CodexAppServerClient {
283286 const goalTurnStarted = new Promise < string > ( ( resolve ) => {
284287 resolveGoalTurnStarted = resolve ;
285288 } ) ;
289+ let resolveNoGoalTurnStarted : ( ) => void = ( ) => { } ;
290+ const noGoalTurnStarted = new Promise < null > ( ( resolve ) => {
291+ resolveNoGoalTurnStarted = ( ) => resolve ( null ) ;
292+ } ) ;
286293 const releaseRoutingCapture = this . captureTurnRoutings ( params . threadId , ( turnId ) => {
287294 if ( goalTurnId !== null ) {
288295 return ;
@@ -291,37 +298,31 @@ export class CodexAppServerClient {
291298 onTurnStarted ?.( turnId ) ;
292299 resolveGoalTurnStarted ( turnId ) ;
293300 } ) ;
301+ const releaseStatusCapture = this . captureThreadStatuses ( params . threadId , ( status ) => {
302+ if ( goalTurnId !== null || status . type === "active" ) {
303+ return ;
304+ }
305+ resolveNoGoalTurnStarted ( ) ;
306+ } ) ;
294307
295308 try {
296309 await this . threadGoalSet ( params ) ;
297- const turnId = goalTurnId ?? await this . waitForGoalTurnStarted ( goalTurnStarted , turnStartGraceMs ) ;
310+ const turnId = goalTurnId ?? await Promise . race ( [ goalTurnStarted , noGoalTurnStarted ] ) ;
298311 if ( turnId === null ) {
299312 return null ;
300313 }
301314 const earlyCompletion = capturedCompletions . find ( event => event . turn . id === turnId ) ;
302315 releaseCompletionCapture ( ) ;
303316 releaseRoutingCapture ( ) ;
317+ releaseStatusCapture ( ) ;
304318 if ( earlyCompletion ) {
305319 return earlyCompletion ;
306320 }
307321 return await this . awaitTurnCompleted ( params . threadId , turnId ) ;
308322 } finally {
309323 releaseCompletionCapture ( ) ;
310324 releaseRoutingCapture ( ) ;
311- }
312- }
313-
314- private async waitForGoalTurnStarted ( goalTurnStarted : Promise < string > , timeoutMs : number ) : Promise < string | null > {
315- let timeout : ReturnType < typeof setTimeout > | null = null ;
316- const timeoutPromise = new Promise < null > ( ( resolve ) => {
317- timeout = setTimeout ( ( ) => resolve ( null ) , timeoutMs ) ;
318- } ) ;
319- try {
320- return await Promise . race ( [ goalTurnStarted , timeoutPromise ] ) ;
321- } finally {
322- if ( timeout !== null ) {
323- clearTimeout ( timeout ) ;
324- }
325+ releaseStatusCapture ( ) ;
325326 }
326327 }
327328
@@ -537,6 +538,16 @@ export class CodexAppServerClient {
537538 }
538539 }
539540
541+ private recordThreadStatusChanged ( event : ThreadStatusChangedNotification ) : void {
542+ const captures = this . threadStatusCaptures . get ( event . threadId ) ;
543+ if ( ! captures ) {
544+ return ;
545+ }
546+ for ( const capture of captures ) {
547+ capture ( event . status ) ;
548+ }
549+ }
550+
540551 private recordTurnRouting ( routing : { threadId : string | null , turnId : string | null } ) : void {
541552 if ( routing . threadId === null || routing . turnId === null ) {
542553 return ;
@@ -628,6 +639,23 @@ export class CodexAppServerClient {
628639 } ;
629640 }
630641
642+ private captureThreadStatuses ( threadId : string , capture : ( status : ThreadStatus ) => void ) : ( ) => void {
643+ const captures = this . threadStatusCaptures . get ( threadId ) ?? new Set < ( status : ThreadStatus ) => void > ( ) ;
644+ captures . add ( capture ) ;
645+ this . threadStatusCaptures . set ( threadId , captures ) ;
646+ let released = false ;
647+ return ( ) => {
648+ if ( released ) {
649+ return ;
650+ }
651+ released = true ;
652+ captures . delete ( capture ) ;
653+ if ( captures . size === 0 ) {
654+ this . threadStatusCaptures . delete ( threadId ) ;
655+ }
656+ } ;
657+ }
658+
631659 private resolveMcpServerStartupResolvers ( ) : void {
632660 const pendingResolvers : Array < McpServerStartupResolver > = [ ] ;
633661 for ( const resolver of this . mcpServerStartupResolvers ) {
@@ -732,6 +760,13 @@ function isTurnCompletedNotification(notification: ServerNotification): notifica
732760 return notification . method === "turn/completed" ;
733761}
734762
763+ function isThreadStatusChangedNotification ( notification : ServerNotification ) : notification is {
764+ method : "thread/status/changed" ;
765+ params : ThreadStatusChangedNotification ;
766+ } {
767+ return notification . method === "thread/status/changed" ;
768+ }
769+
735770function isCompactionCompletedNotification ( notification : ServerNotification ) : notification is CompactionCompletedNotification {
736771 if ( notification . method === "thread/compacted" ) {
737772 return true ;
0 commit comments