@@ -208,6 +208,8 @@ export class TaskLiveUpdateManager {
208208 private metadataRefreshTimer ?: ReturnType < typeof setInterval > ;
209209 private running = false ;
210210 private inflightTick ?: Promise < void > ;
211+ private inflightMetadataRefresh ?: Promise < void > ;
212+ private readonly inflightPublishes = new Set < Promise < unknown > > ( ) ;
211213 private started = false ;
212214 private completed = false ;
213215 private readonly startedAt = new Date ( ) ;
@@ -259,12 +261,7 @@ export class TaskLiveUpdateManager {
259261 if ( this . completed ) return ;
260262 this . completed = true ;
261263 this . stop ( ) ;
262- // Wait for any in-flight tick to finish so the sink's publish queue is drained
263- if ( this . running && this . inflightTick ) {
264- try {
265- await this . inflightTick ;
266- } catch { }
267- }
264+ await this . waitForInflightSinkPublishes ( ) ;
268265 try {
269266 logger . info ( `[TaskLiveUpdates] Publishing final success update for task ${ this . ctx . taskId } ` ) ;
270267 const result = await this . ctx . sink . complete ( this . decorateText ( finalText ) ) ;
@@ -283,12 +280,7 @@ export class TaskLiveUpdateManager {
283280 if ( this . completed ) return ;
284281 this . completed = true ;
285282 this . stop ( ) ;
286- // Wait for any in-flight tick to finish so the sink's publish queue is drained
287- if ( this . running && this . inflightTick ) {
288- try {
289- await this . inflightTick ;
290- } catch { }
291- }
283+ await this . waitForInflightSinkPublishes ( ) ;
292284 try {
293285 logger . info ( `[TaskLiveUpdates] Publishing final failure update for task ${ this . ctx . taskId } ` ) ;
294286 const result = await this . ctx . sink . fail ( this . decorateText ( finalText ) ) ;
@@ -319,6 +311,17 @@ export class TaskLiveUpdateManager {
319311 }
320312
321313 async tick ( ) : Promise < void > {
314+ if ( this . inflightTick ) return this . inflightTick ;
315+ const promise = this . runTick ( ) . finally ( ( ) => {
316+ if ( this . inflightTick === promise ) {
317+ this . inflightTick = undefined ;
318+ }
319+ } ) ;
320+ this . inflightTick = promise ;
321+ return promise ;
322+ }
323+
324+ private async runTick ( ) : Promise < void > {
322325 if ( this . completed || this . running ) return ;
323326 const traceState = this . getTraceState ( ) ;
324327 if ( ! traceState . traceRef && ! traceState . traceId ) {
@@ -402,7 +405,13 @@ export class TaskLiveUpdateManager {
402405 } ,
403406 traceState . traceId
404407 ) ;
405- const result = await this . ctx . sink . update ( message ) ;
408+ if ( this . completed ) {
409+ logger . debug (
410+ `[TaskLiveUpdates] Aborting progress publish for task ${ this . ctx . taskId } : task already completed before sink update`
411+ ) ;
412+ return ;
413+ }
414+ const result = await this . trackPublish ( this . ctx . sink . update ( message ) ) ;
406415 this . recordSinkRef ( result ) ;
407416 this . ctx . appendHistory ?.( cleaned , 'progress' ) ;
408417 this . lastUpdateText = cleaned ;
@@ -424,27 +433,43 @@ export class TaskLiveUpdateManager {
424433 private async runFirstTick ( ) : Promise < void > {
425434 if ( this . completed ) return ;
426435 logger . debug ( `[TaskLiveUpdates] Running first scheduled tick for task ${ this . ctx . taskId } ` ) ;
427- this . inflightTick = this . tick ( ) ;
428- await this . inflightTick ;
436+ await this . tick ( ) ;
429437 if ( this . completed ) return ;
430438 this . timer = setInterval ( ( ) => {
431- this . inflightTick = this . tick ( ) ;
439+ void this . tick ( ) ;
432440 } , this . ctx . config . intervalSeconds * 1000 ) ;
433441 if ( typeof ( this . timer as any ) ?. unref === 'function' ) {
434442 ( this . timer as any ) . unref ( ) ;
435443 }
436444 this . metadataRefreshTimer = setInterval ( ( ) => {
437- void this . refreshProgressMetadata ( ) ;
445+ void this . scheduleMetadataRefresh ( ) ;
438446 } , DEFAULT_TASK_LIVE_UPDATE_METADATA_REFRESH_SECONDS * 1000 ) ;
439447 if ( typeof ( this . metadataRefreshTimer as any ) ?. unref === 'function' ) {
440448 ( this . metadataRefreshTimer as any ) . unref ( ) ;
441449 }
442450 }
443451
452+ private async waitForInflightSinkPublishes ( ) : Promise < void > {
453+ const pending = [ ...this . inflightPublishes ] ;
454+ if ( pending . length === 0 ) return ;
455+ try {
456+ await Promise . allSettled ( pending ) ;
457+ } catch { }
458+ }
459+
444460 private recordSinkRef ( result : { ref ?: Record < string , unknown > } | null | undefined ) : void {
445461 if ( result ?. ref ) this . ctx . onPostedRef ?.( result . ref ) ;
446462 }
447463
464+ private async trackPublish < T > ( publish : Promise < T > ) : Promise < T > {
465+ this . inflightPublishes . add ( publish ) ;
466+ try {
467+ return await publish ;
468+ } finally {
469+ this . inflightPublishes . delete ( publish ) ;
470+ }
471+ }
472+
448473 private getTraceState ( ) : { traceRef ?: string ; traceId ?: string } {
449474 const resolved = this . ctx . resolveTraceState ?.( ) ;
450475 return {
@@ -462,7 +487,7 @@ export class TaskLiveUpdateManager {
462487 private decorateProgressText (
463488 text : string ,
464489 timing : ProgressTimingMetadata ,
465- traceId ?: string
490+ _traceId ?: string
466491 ) : string {
467492 const normalized = normalizeProgressSummary ( text ) ;
468493 const taskSummary = formatTaskSummary ( timing . taskSummary ) ;
@@ -473,8 +498,21 @@ export class TaskLiveUpdateManager {
473498 taskSummary ,
474499 normalized ,
475500 formatProgressMetadata ( timing ) ,
501+ `\`task_id: ${ this . ctx . taskId } \`` ,
502+ '`live_update: true`' ,
476503 ] . filter ( Boolean ) ;
477- return this . decorateText ( blocks . join ( '\n\n' ) , traceId ) ;
504+ return blocks . join ( '\n\n' ) ;
505+ }
506+
507+ private scheduleMetadataRefresh ( ) : Promise < void > {
508+ if ( this . inflightMetadataRefresh ) return this . inflightMetadataRefresh ;
509+ const promise = this . refreshProgressMetadata ( ) . finally ( ( ) => {
510+ if ( this . inflightMetadataRefresh === promise ) {
511+ this . inflightMetadataRefresh = undefined ;
512+ }
513+ } ) ;
514+ this . inflightMetadataRefresh = promise ;
515+ return promise ;
478516 }
479517
480518 private async refreshProgressMetadata ( ) : Promise < void > {
@@ -499,7 +537,7 @@ export class TaskLiveUpdateManager {
499537 logger . debug (
500538 `[TaskLiveUpdates] Refreshing metadata-only live update for task ${ this . ctx . taskId } `
501539 ) ;
502- const result = await this . ctx . sink . update ( message ) ;
540+ const result = await this . trackPublish ( this . ctx . sink . update ( message ) ) ;
503541 this . recordSinkRef ( result ) ;
504542 this . lastPostedMessage = message ;
505543 } catch ( err ) {
@@ -559,7 +597,8 @@ export class TaskLiveUpdateManager {
559597 this . lastStallFallbackAt = now ;
560598 return ;
561599 }
562- const result = await this . ctx . sink . update ( message ) ;
600+ if ( this . completed ) return ;
601+ const result = await this . trackPublish ( this . ctx . sink . update ( message ) ) ;
563602 this . recordSinkRef ( result ) ;
564603 if ( ! this . lastUpdateText ) {
565604 this . ctx . appendHistory ?.( baseText , 'progress' ) ;
@@ -698,20 +737,28 @@ function formatTaskLabel(task: { id: string; title: string; status: string }): s
698737 return title || task . id || task . status ;
699738}
700739
740+ function scopeHasRenderableTasks ( scope : NonNullable < ProbeTaskSummary [ 'scopes' ] > [ number ] ) : boolean {
741+ if ( scope . tasks . some ( task => ! task . synthetic ) ) return true ;
742+ return scope . children . some ( child => scopeHasRenderableTasks ( child ) ) ;
743+ }
744+
701745function appendTaskScopeLines (
702746 lines : string [ ] ,
703747 scope : NonNullable < ProbeTaskSummary [ 'scopes' ] > [ number ] ,
704748 depth = 0
705749) : void {
750+ if ( ! scopeHasRenderableTasks ( scope ) ) return ;
706751 const prefix = ' ' . repeat ( depth ) ;
707752 if ( depth > 0 || scope . label !== 'Main Agent' ) {
708753 lines . push ( `${ prefix } ${ scope . label } ` ) ;
709754 }
710- for ( const task of scope . tasks . slice ( 0 , 8 ) ) {
755+ const visibleTasks = scope . tasks . filter ( task => ! task . synthetic ) . slice ( 0 , 8 ) ;
756+ for ( const task of visibleTasks ) {
711757 lines . push ( `${ prefix } • ${ formatTaskStatusMarker ( task . status ) } ${ formatTaskLabel ( task ) } ` ) ;
712758 }
713- if ( scope . tasks . length > 8 ) {
714- lines . push ( `${ prefix } • ... ${ scope . tasks . length - 8 } more` ) ;
759+ const hiddenCount = scope . tasks . filter ( task => ! task . synthetic ) . length - visibleTasks . length ;
760+ if ( hiddenCount > 0 ) {
761+ lines . push ( `${ prefix } • ... ${ hiddenCount } more` ) ;
715762 }
716763 for ( const child of scope . children ) {
717764 appendTaskScopeLines ( lines , child , depth + 1 ) ;
@@ -724,14 +771,16 @@ function formatTaskSummary(summary?: ProbeTaskSummary): string | undefined {
724771 if ( summary . scopes . length > 0 ) {
725772 for ( const scope of summary . scopes ) appendTaskScopeLines ( lines , scope ) ;
726773 } else {
727- for ( const task of summary . tasks . slice ( 0 , 8 ) ) {
774+ const visibleTasks = summary . tasks . filter ( task => ! task . synthetic ) . slice ( 0 , 8 ) ;
775+ for ( const task of visibleTasks ) {
728776 lines . push ( `• ${ formatTaskStatusMarker ( task . status ) } ${ formatTaskLabel ( task ) } ` ) ;
729777 }
730- if ( summary . tasks . length > 8 ) {
731- lines . push ( `• ... ${ summary . tasks . length - 8 } more` ) ;
778+ const hiddenCount = summary . tasks . filter ( task => ! task . synthetic ) . length - visibleTasks . length ;
779+ if ( hiddenCount > 0 ) {
780+ lines . push ( `• ... ${ hiddenCount } more` ) ;
732781 }
733782 }
734- return lines . join ( '\n' ) ;
783+ return lines . length > 1 ? lines . join ( '\n' ) : undefined ;
735784}
736785
737786function formatSkillList ( skills : string [ ] ) : string {
0 commit comments