@@ -34,6 +34,7 @@ import type {
3434 ThreadArchiveResponse ,
3535 ThreadCompactStartParams ,
3636 ThreadCompactStartResponse ,
37+ ThreadGoalClearedNotification ,
3738 ThreadGoalClearParams ,
3839 ThreadGoalClearResponse ,
3940 ThreadGoalSetParams ,
@@ -85,6 +86,11 @@ export type McpStartupResult = {
8586 cancelled : Array < string > ;
8687} ;
8788
89+ type ServerNotificationItemType = Extract <
90+ ServerNotification ,
91+ { params : { item : { type : unknown } } }
92+ > [ "params" ] [ "item" ] [ "type" ] ;
93+
8894const CommandExecutionApprovalRequest = new RequestType <
8995 CommandExecutionRequestApprovalParams ,
9096 CommandExecutionRequestApprovalResponse ,
@@ -110,7 +116,45 @@ const McpServerElicitationRequest = new RequestType<
110116> ( 'mcpServer/elicitation/request' ) ;
111117
112118const GOAL_RUNTIME_EFFECTS_GRACE_MS = 1_000 ;
113- const GOAL_TURN_COMPLETION_GRACE_MS = 250 ;
119+ const VISIBLE_TURN_ACTIVITY_METHODS : ReadonlySet < ServerNotification [ "method" ] > = new Set ( [
120+ "error" ,
121+ "turn/plan/updated" ,
122+ "item/autoApprovalReview/started" ,
123+ "item/autoApprovalReview/completed" ,
124+ "item/agentMessage/delta" ,
125+ "item/commandExecution/outputDelta" ,
126+ "item/commandExecution/terminalInteraction" ,
127+ "item/mcpToolCall/progress" ,
128+ "item/reasoning/summaryTextDelta" ,
129+ "item/reasoning/summaryPartAdded" ,
130+ "item/reasoning/textDelta" ,
131+ "model/rerouted" ,
132+ "warning" ,
133+ "configWarning" ,
134+ "fuzzyFileSearch/sessionUpdated" ,
135+ "fuzzyFileSearch/sessionCompleted" ,
136+ ] ) ;
137+ const VISIBLE_STARTED_ITEM_TYPES : ReadonlySet < ServerNotificationItemType > = new Set ( [
138+ "fileChange" ,
139+ "commandExecution" ,
140+ "mcpToolCall" ,
141+ "dynamicToolCall" ,
142+ "webSearch" ,
143+ "imageView" ,
144+ "imageGeneration" ,
145+ "collabAgentToolCall" ,
146+ ] ) ;
147+ const VISIBLE_COMPLETED_ITEM_TYPES : ReadonlySet < ServerNotificationItemType > = new Set ( [
148+ "fileChange" ,
149+ "commandExecution" ,
150+ "mcpToolCall" ,
151+ "dynamicToolCall" ,
152+ "webSearch" ,
153+ "imageView" ,
154+ "imageGeneration" ,
155+ "collabAgentToolCall" ,
156+ "contextCompaction" ,
157+ ] ) ;
114158
115159/**
116160 * A type-safe client over the Codex App Server's JSON-RPC API.
@@ -127,8 +171,10 @@ export class CodexAppServerClient {
127171 private readonly pendingCompactionCompletionResolvers = new Map < string , Set < ( event : CompactionCompletedNotification ) => void > > ( ) ;
128172 private readonly turnCompletionCaptures = new Map < string , Set < ( event : TurnCompletedNotification ) => void > > ( ) ;
129173 private readonly turnRoutingCaptures = new Map < string , Set < ( turnId : string ) => void > > ( ) ;
174+ private readonly turnActivityCaptures = new Map < string , Set < ( turnId : string ) => void > > ( ) ;
130175 private readonly threadStatusCaptures = new Map < string , Set < ( status : ThreadStatus ) => void > > ( ) ;
131176 private readonly threadGoalUpdateCaptures = new Map < string , Set < ( event : ThreadGoalUpdatedNotification ) => void > > ( ) ;
177+ private readonly threadGoalClearedCaptures = new Map < string , Set < ( ) => void > > ( ) ;
132178 private readonly staleTurnIds = new Map < string , Set < string > > ( ) ;
133179
134180 constructor ( connection : MessageConnection ) {
@@ -156,6 +202,9 @@ export class CodexAppServerClient {
156202 if ( isThreadGoalUpdatedNotification ( serverNotification ) ) {
157203 this . recordThreadGoalUpdated ( serverNotification . params ) ;
158204 }
205+ if ( isThreadGoalClearedNotification ( serverNotification ) ) {
206+ this . recordThreadGoalCleared ( serverNotification . params ) ;
207+ }
159208 const routing = extractTurnRouting ( serverNotification ) ;
160209 if ( this . handleStaleTurnNotification ( serverNotification , routing ) ) {
161210 return ;
@@ -164,6 +213,7 @@ export class CodexAppServerClient {
164213 if ( this . handleStaleTurnNotification ( serverNotification , routing ) ) {
165214 return ;
166215 }
216+ this . recordVisibleTurnActivity ( serverNotification , routing ) ;
167217 this . notify ( serverNotification ) ;
168218 for ( const callback of this . codexEventHandlers ) {
169219 callback ( { eventType : "notification" , ...serverNotification } ) ;
@@ -286,7 +336,6 @@ export class CodexAppServerClient {
286336 params : ThreadGoalSetParams ,
287337 onTurnStarted ?: ( turnId : string ) => void ,
288338 runtimeEffectsGraceMs = GOAL_RUNTIME_EFFECTS_GRACE_MS ,
289- turnCompletionGraceMs = GOAL_TURN_COMPLETION_GRACE_MS ,
290339 ) : Promise < TurnCompletedNotification | null > {
291340 let goalTurnId : string | null = null ;
292341 const capturedCompletions : Array < TurnCompletedNotification > = [ ] ;
@@ -304,6 +353,14 @@ export class CodexAppServerClient {
304353 const goalTurnStarted = new Promise < string > ( ( resolve ) => {
305354 resolveGoalTurnStarted = resolve ;
306355 } ) ;
356+ let resolveGoalUpdateHandled : ( ) => void = ( ) => { } ;
357+ const matchingGoalUpdateHandled = new Promise < null > ( ( resolve ) => {
358+ resolveGoalUpdateHandled = ( ) => resolve ( null ) ;
359+ } ) ;
360+ let resolveGoalTurnActivity : ( ) => void = ( ) => { } ;
361+ const goalTurnActivity = new Promise < null > ( ( resolve ) => {
362+ resolveGoalTurnActivity = ( ) => resolve ( null ) ;
363+ } ) ;
307364 let goalUpdateHandled = false ;
308365 let expectedGoal : ThreadGoal | null = null ;
309366 const noGoalTurnStarted = this . createNoGoalTurnStartedPromise ( runtimeEffectsGraceMs ) ;
@@ -316,10 +373,17 @@ export class CodexAppServerClient {
316373 onTurnStarted ?.( turnId ) ;
317374 resolveGoalTurnStarted ( turnId ) ;
318375 } ) ;
376+ const releaseActivityCapture = this . captureTurnActivities ( params . threadId , ( turnId ) => {
377+ if ( ! goalUpdateHandled || goalTurnId !== turnId ) {
378+ return ;
379+ }
380+ resolveGoalTurnActivity ( ) ;
381+ } ) ;
319382 const releaseGoalUpdateCapture = this . captureThreadGoalUpdates ( params . threadId , ( event ) => {
320383 capturedGoalUpdates . push ( event ) ;
321384 if ( expectedGoal !== null && goalsMatch ( event . goal , expectedGoal ) ) {
322385 goalUpdateHandled = true ;
386+ resolveGoalUpdateHandled ( ) ;
323387 noGoalTurnStarted . goalUpdated ( ) ;
324388 }
325389 } ) ;
@@ -335,35 +399,60 @@ export class CodexAppServerClient {
335399 expectedGoal = goalSetResponse . goal ;
336400 if ( capturedGoalUpdates . some ( event => goalsMatch ( event . goal , expectedGoal ! ) ) ) {
337401 goalUpdateHandled = true ;
402+ resolveGoalUpdateHandled ( ) ;
338403 noGoalTurnStarted . goalUpdated ( ) ;
339404 }
405+ if ( expectedGoal . status !== "active" ) {
406+ await matchingGoalUpdateHandled ;
407+ return null ;
408+ }
340409 const turnId = goalTurnId ?? await Promise . race ( [ goalTurnStarted , noGoalTurnStarted . promise ] ) ;
341410 noGoalTurnStarted . release ( ) ;
342411 releaseRoutingCapture ( ) ;
343412 releaseStatusCapture ( ) ;
344413 releaseGoalUpdateCapture ( ) ;
345414 if ( turnId === null ) {
415+ releaseActivityCapture ( ) ;
346416 return null ;
347417 }
348418 const earlyCompletion = capturedCompletions . find ( event => event . turn . id === turnId ) ;
349419 if ( earlyCompletion ) {
420+ releaseActivityCapture ( ) ;
350421 return earlyCompletion ;
351422 }
352- const completionGrace = this . createGoalTurnCompletionGracePromise ( turnCompletionGraceMs ) ;
353- try {
354- return await Promise . race ( [ goalTurnCompleted , completionGrace . promise ] ) ;
355- } finally {
356- completionGrace . release ( ) ;
357- }
423+ return await Promise . race ( [ goalTurnCompleted , goalTurnActivity ] ) ;
358424 } finally {
359425 noGoalTurnStarted . release ( ) ;
360426 releaseCompletionCapture ( ) ;
361427 releaseRoutingCapture ( ) ;
428+ releaseActivityCapture ( ) ;
362429 releaseStatusCapture ( ) ;
363430 releaseGoalUpdateCapture ( ) ;
364431 }
365432 }
366433
434+ async runGoalClear ( params : ThreadGoalClearParams ) : Promise < void > {
435+ let goalClearedHandled = false ;
436+ let resolveGoalClearedHandled : ( ) => void = ( ) => { } ;
437+ const matchingGoalClearedHandled = new Promise < void > ( ( resolve ) => {
438+ resolveGoalClearedHandled = ( ) => resolve ( ) ;
439+ } ) ;
440+ const releaseGoalClearedCapture = this . captureThreadGoalClears ( params . threadId , ( ) => {
441+ goalClearedHandled = true ;
442+ resolveGoalClearedHandled ( ) ;
443+ } ) ;
444+
445+ try {
446+ const response = await this . threadGoalClear ( params ) ;
447+ if ( ! response . cleared || goalClearedHandled ) {
448+ return ;
449+ }
450+ await matchingGoalClearedHandled ;
451+ } finally {
452+ releaseGoalClearedCapture ( ) ;
453+ }
454+ }
455+
367456 private createNoGoalTurnStartedPromise (
368457 runtimeEffectsGraceMs : number ,
369458 ) : {
@@ -435,30 +524,6 @@ export class CodexAppServerClient {
435524 } ;
436525 }
437526
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-
462527 async runCompact ( params : ThreadCompactStartParams ) : Promise < CompactionCompletedNotification > {
463528 const compactionCompleted = this . awaitCompactionCompleted ( params . threadId ) ;
464529 await this . threadCompactStart ( params ) ;
@@ -691,6 +756,16 @@ export class CodexAppServerClient {
691756 }
692757 }
693758
759+ private recordThreadGoalCleared ( event : ThreadGoalClearedNotification ) : void {
760+ const captures = this . threadGoalClearedCaptures . get ( event . threadId ) ;
761+ if ( ! captures ) {
762+ return ;
763+ }
764+ for ( const capture of captures ) {
765+ capture ( ) ;
766+ }
767+ }
768+
694769 private recordTurnRouting ( routing : { threadId : string | null , turnId : string | null } ) : void {
695770 if ( routing . threadId === null || routing . turnId === null ) {
696771 return ;
@@ -704,6 +779,22 @@ export class CodexAppServerClient {
704779 }
705780 }
706781
782+ private recordVisibleTurnActivity (
783+ notification : ServerNotification ,
784+ routing : { threadId : string | null , turnId : string | null } ,
785+ ) : void {
786+ if ( routing . threadId === null || routing . turnId === null || ! isVisibleTurnActivityNotification ( notification ) ) {
787+ return ;
788+ }
789+ const captures = this . turnActivityCaptures . get ( routing . threadId ) ;
790+ if ( ! captures ) {
791+ return ;
792+ }
793+ for ( const capture of captures ) {
794+ capture ( routing . turnId ) ;
795+ }
796+ }
797+
707798 private handleStaleTurnNotification (
708799 notification : ServerNotification ,
709800 routing : { threadId : string | null , turnId : string | null } ,
@@ -782,6 +873,23 @@ export class CodexAppServerClient {
782873 } ;
783874 }
784875
876+ private captureTurnActivities ( threadId : string , capture : ( turnId : string ) => void ) : ( ) => void {
877+ const captures = this . turnActivityCaptures . get ( threadId ) ?? new Set < ( turnId : string ) => void > ( ) ;
878+ captures . add ( capture ) ;
879+ this . turnActivityCaptures . set ( threadId , captures ) ;
880+ let released = false ;
881+ return ( ) => {
882+ if ( released ) {
883+ return ;
884+ }
885+ released = true ;
886+ captures . delete ( capture ) ;
887+ if ( captures . size === 0 ) {
888+ this . turnActivityCaptures . delete ( threadId ) ;
889+ }
890+ } ;
891+ }
892+
785893 private captureThreadStatuses ( threadId : string , capture : ( status : ThreadStatus ) => void ) : ( ) => void {
786894 const captures = this . threadStatusCaptures . get ( threadId ) ?? new Set < ( status : ThreadStatus ) => void > ( ) ;
787895 captures . add ( capture ) ;
@@ -816,6 +924,23 @@ export class CodexAppServerClient {
816924 } ;
817925 }
818926
927+ private captureThreadGoalClears ( threadId : string , capture : ( ) => void ) : ( ) => void {
928+ const captures = this . threadGoalClearedCaptures . get ( threadId ) ?? new Set < ( ) => void > ( ) ;
929+ captures . add ( capture ) ;
930+ this . threadGoalClearedCaptures . set ( threadId , captures ) ;
931+ let released = false ;
932+ return ( ) => {
933+ if ( released ) {
934+ return ;
935+ }
936+ released = true ;
937+ captures . delete ( capture ) ;
938+ if ( captures . size === 0 ) {
939+ this . threadGoalClearedCaptures . delete ( threadId ) ;
940+ }
941+ } ;
942+ }
943+
819944 private resolveMcpServerStartupResolvers ( ) : void {
820945 const pendingResolvers : Array < McpServerStartupResolver > = [ ] ;
821946 for ( const resolver of this . mcpServerStartupResolvers ) {
@@ -934,6 +1059,13 @@ function isThreadGoalUpdatedNotification(notification: ServerNotification): noti
9341059 return notification . method === "thread/goal/updated" ;
9351060}
9361061
1062+ function isThreadGoalClearedNotification ( notification : ServerNotification ) : notification is {
1063+ method : "thread/goal/cleared" ;
1064+ params : ThreadGoalClearedNotification ;
1065+ } {
1066+ return notification . method === "thread/goal/cleared" ;
1067+ }
1068+
9371069function isCompactionCompletedNotification ( notification : ServerNotification ) : notification is CompactionCompletedNotification {
9381070 if ( notification . method === "thread/compacted" ) {
9391071 return true ;
@@ -949,6 +1081,31 @@ function goalsMatch(left: ThreadGoal, right: ThreadGoal): boolean {
9491081 && left . updatedAt === right . updatedAt ;
9501082}
9511083
1084+ function isVisibleTurnActivityNotification ( notification : ServerNotification ) : boolean {
1085+ if ( notification . method === "item/started" ) {
1086+ return isVisibleStartedItemType ( notification . params . item . type ) ;
1087+ }
1088+ if ( notification . method === "item/completed" ) {
1089+ const item = notification . params . item ;
1090+ if ( item . type === "reasoning" ) {
1091+ return item . summary . length > 0 || item . content . length > 0 ;
1092+ }
1093+ if ( item . type === "exitedReviewMode" ) {
1094+ return item . review . trim ( ) . length > 0 ;
1095+ }
1096+ return isVisibleCompletedItemType ( item . type ) ;
1097+ }
1098+ return VISIBLE_TURN_ACTIVITY_METHODS . has ( notification . method ) ;
1099+ }
1100+
1101+ function isVisibleStartedItemType ( itemType : ServerNotificationItemType ) : boolean {
1102+ return VISIBLE_STARTED_ITEM_TYPES . has ( itemType ) ;
1103+ }
1104+
1105+ function isVisibleCompletedItemType ( itemType : ServerNotificationItemType ) : boolean {
1106+ return VISIBLE_COMPLETED_ITEM_TYPES . has ( itemType ) ;
1107+ }
1108+
9521109function extractThreadId ( notification : ServerNotification ) : string | null {
9531110 const params = notification . params as { threadId ?: unknown } | undefined ;
9541111 if ( params && typeof params . threadId === "string" ) {
0 commit comments