@@ -563,39 +563,80 @@ export function runtimeEventToActivities(
563563 }
564564
565565 case "task.progress" : {
566+ const linkage = taskLinkageActivityFields ( event . payload as Record < string , unknown > ) ;
567+ // Usage and activity are independent latest-state streams. Keeping them
568+ // under separate stable ids prevents a command/reasoning update from
569+ // replacing the last known token count (and prevents a usage-only tick
570+ // from blanking the last meaningful activity).
571+ const identityLinkage = { ...linkage } ;
572+ delete identityLinkage . typedUsage ;
573+ delete identityLinkage . status ;
574+ delete identityLinkage . error ;
575+ const title =
576+ event . payload . description . trim ( ) . length > 0
577+ ? { title : truncateDetail ( event . payload . description , 120 ) }
578+ : { } ;
579+ const hasProgressState =
580+ event . payload . typedUsage === undefined ||
581+ event . payload . summary !== undefined ||
582+ event . payload . lastToolName !== undefined ||
583+ event . payload . status !== undefined ||
584+ event . payload . error !== undefined ;
566585 return [
567- {
568- // Stable per-task id: progress is "latest state", not history, so
569- // each tick REPLACES the last via the activity upsert (PK + the
570- // replace-by-id apply in projector and client reducer). Keeps one
571- // progress row per task instead of thousands, so a large fleet's
572- // ticks can no longer evict its own start/terminal rows out of
573- // the 500-row retention window. Thread-scoped: activity_id is a
574- // GLOBAL primary key and Claude task ids are session-local, so a
575- // bare taskId could collide across threads and steal another
576- // thread's row (review finding).
577- id : EventId . make ( `task-progress:${ event . threadId } :${ event . payload . taskId } ` ) ,
578- createdAt : event . createdAt ,
579- tone : "info" ,
580- kind : "task.progress" ,
581- summary :
582- event . payload . description . trim ( ) . length > 0
583- ? truncateDetail ( event . payload . description , 120 )
584- : "Reasoning update" ,
585- payload : {
586- taskId : event . payload . taskId ,
587- ...( event . payload . description . trim ( ) . length > 0
588- ? { title : truncateDetail ( event . payload . description , 120 ) }
589- : { } ) ,
590- detail : truncateDetail ( event . payload . summary ?? event . payload . description ) ,
591- ...( event . payload . summary ? { summary : truncateDetail ( event . payload . summary ) } : { } ) ,
592- ...( event . payload . lastToolName ? { lastToolName : event . payload . lastToolName } : { } ) ,
593- ...( event . payload . usage !== undefined ? { usage : event . payload . usage } : { } ) ,
594- ...taskLinkageActivityFields ( event . payload as Record < string , unknown > ) ,
595- } ,
596- turnId : toTurnId ( event . turnId ) ?? null ,
597- ...maybeSequence ,
598- } ,
586+ ...( hasProgressState
587+ ? [
588+ {
589+ // Stable per-task id: activity is "latest state", not
590+ // history, so each meaningful tick replaces the last. This
591+ // bounds a large fleet to one activity row per task.
592+ id : EventId . make ( `task-progress:${ event . threadId } :${ event . payload . taskId } ` ) ,
593+ createdAt : event . createdAt ,
594+ tone : "info" as const ,
595+ kind : "task.progress" as const ,
596+ summary :
597+ event . payload . description . trim ( ) . length > 0
598+ ? truncateDetail ( event . payload . description , 120 )
599+ : "Reasoning update" ,
600+ payload : {
601+ taskId : event . payload . taskId ,
602+ ...title ,
603+ detail : truncateDetail ( event . payload . summary ?? event . payload . description ) ,
604+ ...( event . payload . summary
605+ ? { summary : truncateDetail ( event . payload . summary ) }
606+ : { } ) ,
607+ ...( event . payload . lastToolName
608+ ? { lastToolName : event . payload . lastToolName }
609+ : { } ) ,
610+ ...( event . payload . status ? { status : event . payload . status } : { } ) ,
611+ ...( event . payload . error ? { error : event . payload . error } : { } ) ,
612+ ...( event . payload . usage !== undefined ? { usage : event . payload . usage } : { } ) ,
613+ ...identityLinkage ,
614+ } ,
615+ turnId : toTurnId ( event . turnId ) ?? null ,
616+ ...maybeSequence ,
617+ } ,
618+ ]
619+ : [ ] ) ,
620+ ...( event . payload . typedUsage !== undefined
621+ ? [
622+ {
623+ id : EventId . make ( `task-usage:${ event . threadId } :${ event . payload . taskId } ` ) ,
624+ createdAt : event . createdAt ,
625+ tone : "info" as const ,
626+ kind : "task.progress" as const ,
627+ summary : "Task usage updated" ,
628+ payload : {
629+ taskId : event . payload . taskId ,
630+ ...title ,
631+ ...identityLinkage ,
632+ usageSnapshot : true ,
633+ typedUsage : event . payload . typedUsage ,
634+ } ,
635+ turnId : toTurnId ( event . turnId ) ?? null ,
636+ ...maybeSequence ,
637+ } ,
638+ ]
639+ : [ ] ) ,
599640 ] ;
600641 }
601642
0 commit comments