@@ -96,6 +96,10 @@ function budgetBar(pct: number, width = 30): string {
9696 return `${ color } ${ '█' . repeat ( filled ) } ${ dim } ${ '░' . repeat ( empty ) } ${ rst } ${ String ( pct ) . padStart ( 3 ) } %` ;
9797}
9898
99+ // ── Constants ──
100+ const STALE_UI_THRESHOLD_MS = 5 * 60 * 1000 ; // 5 minutes - UI "stalled" indicator
101+ const STALE_FINALIZE_THRESHOLD_MS = 60 * 60 * 1000 ; // 1 hour - finalize threshold
102+
99103// ── ANSI helpers ──
100104const c = {
101105 r : '\x1b[0m' , // reset
@@ -206,6 +210,42 @@ function progressBar(pct: number, width: number): string {
206210 return `${ col } ${ '━' . repeat ( filled ) } ${ c . d } ${ '╌' . repeat ( empty ) } ${ c . r } ` ;
207211}
208212
213+ /** Scan all agent status files, returning parsed statuses with dir name */
214+ function scanAgentStatuses ( ) : ( AgentStatusFile & { dir : string } ) [ ] {
215+ const agentsDir = join ( homedir ( ) , '.stackmemory' , 'conductor' , 'agents' ) ;
216+ if ( ! existsSync ( agentsDir ) ) return [ ] ;
217+ const entries = readdirSync ( agentsDir , { withFileTypes : true } ) ;
218+ const statuses : ( AgentStatusFile & { dir : string } ) [ ] = [ ] ;
219+ for ( const entry of entries ) {
220+ if ( ! entry . isDirectory ( ) ) continue ;
221+ const statusPath = join ( agentsDir , entry . name , 'status.json' ) ;
222+ if ( ! existsSync ( statusPath ) ) continue ;
223+ try {
224+ const data = JSON . parse ( readFileSync ( statusPath , 'utf-8' ) ) ;
225+ statuses . push ( { ...( data as AgentStatusFile ) , dir : entry . name } ) ;
226+ } catch {
227+ // skip corrupt files
228+ }
229+ }
230+ statuses . sort (
231+ ( a , b ) =>
232+ new Date ( b . lastUpdate ) . getTime ( ) - new Date ( a . lastUpdate ) . getTime ( )
233+ ) ;
234+ return statuses ;
235+ }
236+
237+ /** Enrich a status entry with computed liveness fields */
238+ function enrichStatus ( s : AgentStatusFile ) : {
239+ elapsed : number ;
240+ alive : boolean ;
241+ stale : boolean ;
242+ } {
243+ const elapsed = Date . now ( ) - new Date ( s . lastUpdate ) . getTime ( ) ;
244+ const alive = isProcessAlive ( s . pid ) ;
245+ const stale = alive && elapsed > STALE_UI_THRESHOLD_MS ;
246+ return { elapsed, alive, stale } ;
247+ }
248+
209249function fmtMinutes ( m : number ) : string {
210250 if ( m < 0 ) return 'N/A' ;
211251 if ( m >= 60 ) return `${ Math . floor ( m / 60 ) } h ${ m % 60 } m` ;
@@ -224,25 +264,20 @@ function printUsageSummary(u: Record<string, unknown>): void {
224264 const mins20x = ( u . minutesRemaining20x as number ) ?? - 1 ;
225265 const cacheHitRate = ( u . cacheHitRate as number ) || 0 ;
226266
227- const b = '\x1b[1m' ;
228- const d = '\x1b[2m' ;
229- const w = '\x1b[37m' ;
230- const r = '\x1b[0m' ;
231-
232- console . log ( `${ b } Token Usage${ r } ` ) ;
267+ console . log ( `${ c . b } Token Usage${ c . r } ` ) ;
233268 console . log (
234- ` Input ${ w } ${ fmtTokens ( inputTokens ) } ${ r } ${ d } |${ r } Output ${ w } ${ fmtTokens ( outputTokens ) } ${ r } ${ d } |${ r } Total ${ w } ${ fmtTokens ( totalTokens ) } ${ r } `
269+ ` Input ${ c . white } ${ fmtTokens ( inputTokens ) } ${ c . r } ${ c . d } |${ c . r } Output ${ c . white } ${ fmtTokens ( outputTokens ) } ${ c . r } ${ c . d } |${ c . r } Total ${ c . white } ${ fmtTokens ( totalTokens ) } ${ c . r } `
235270 ) ;
236271 console . log (
237- ` Rate ${ w } ${ fmtTokens ( tokensPerMin ) } /min${ r } ${ d } |${ r } Messages ${ w } ${ estMessages } ${ r } ${ d } |${ r } Cache hit ${ w } ${ cacheHitRate } %${ r } `
272+ ` Rate ${ c . white } ${ fmtTokens ( tokensPerMin ) } /min${ c . r } ${ c . d } |${ c . r } Messages ${ c . white } ${ estMessages } ${ c . r } ${ c . d } |${ c . r } Cache hit ${ c . white } ${ cacheHitRate } %${ c . r } `
238273 ) ;
239274 console . log ( '' ) ;
240- console . log ( `${ b } Budget (Max plan, 5h window)${ r } ` ) ;
275+ console . log ( `${ c . b } Budget (Max plan, 5h window)${ c . r } ` ) ;
241276 console . log (
242- ` 5x (225 msgs) ${ budgetBar ( budgetPct5x ) } ${ d } ~${ fmtMinutes ( mins5x ) } left${ r } `
277+ ` 5x (225 msgs) ${ budgetBar ( budgetPct5x ) } ${ c . d } ~${ fmtMinutes ( mins5x ) } left${ c . r } `
243278 ) ;
244279 console . log (
245- ` 20x (900 msgs) ${ budgetBar ( budgetPct20x ) } ${ d } ~${ fmtMinutes ( mins20x ) } left${ r } `
280+ ` 20x (900 msgs) ${ budgetBar ( budgetPct20x ) } ${ c . d } ~${ fmtMinutes ( mins20x ) } left${ c . r } `
246281 ) ;
247282}
248283
@@ -576,46 +611,18 @@ export function createConductorCommands(): Command {
576611 . command ( 'status' )
577612 . description ( 'Show running agent status table' )
578613 . action ( async ( ) => {
579- const agentsDir = join ( homedir ( ) , '.stackmemory' , 'conductor' , 'agents' ) ;
580- if ( ! existsSync ( agentsDir ) ) {
581- console . log ( 'No agent status files found' ) ;
582- return ;
583- }
584-
585- const entries = readdirSync ( agentsDir , { withFileTypes : true } ) ;
586- const statuses : AgentStatusFile [ ] = [ ] ;
587-
588- for ( const entry of entries ) {
589- if ( ! entry . isDirectory ( ) ) continue ;
590- const statusPath = join ( agentsDir , entry . name , 'status.json' ) ;
591- if ( ! existsSync ( statusPath ) ) continue ;
592- try {
593- const data = JSON . parse ( readFileSync ( statusPath , 'utf-8' ) ) ;
594- statuses . push ( data as AgentStatusFile ) ;
595- } catch {
596- // skip corrupt files
597- }
598- }
614+ const statuses = scanAgentStatuses ( ) ;
599615
600616 if ( statuses . length === 0 ) {
601617 console . log ( 'No agent status files found' ) ;
602618 return ;
603619 }
604620
605- // Sort by lastUpdate descending (most recent first)
606- statuses . sort (
607- ( a , b ) =>
608- new Date ( b . lastUpdate ) . getTime ( ) - new Date ( a . lastUpdate ) . getTime ( )
609- ) ;
610-
611- // Compact grid display
612- const active = statuses . filter ( ( s ) => isProcessAlive ( s . pid ) ) ;
613- const stalled = statuses . filter (
614- ( s ) =>
615- isProcessAlive ( s . pid ) &&
616- Date . now ( ) - new Date ( s . lastUpdate ) . getTime ( ) > 5 * 60 * 1000
617- ) ;
618- const dead = statuses . filter ( ( s ) => ! isProcessAlive ( s . pid ) ) ;
621+ // Compute liveness once per entry
622+ const enriched = statuses . map ( ( s ) => ( { ...s , ...enrichStatus ( s ) } ) ) ;
623+ const active = enriched . filter ( ( s ) => s . alive ) ;
624+ const stalled = enriched . filter ( ( s ) => s . stale ) ;
625+ const dead = enriched . filter ( ( s ) => ! s . alive ) ;
619626 const healthy = active . length - stalled . length ;
620627
621628 const parts : string [ ] = [ ] ;
@@ -630,22 +637,19 @@ export function createConductorCommands(): Command {
630637 const cols = ( process . stdout . columns || 80 ) >= 90 ? 2 : 1 ;
631638 const rows : string [ ] [ ] = [ ] ;
632639
633- for ( const s of statuses ) {
634- const elapsed = Date . now ( ) - new Date ( s . lastUpdate ) . getTime ( ) ;
635- const staleFlag = elapsed > 5 * 60 * 1000 ;
636- const alive = isProcessAlive ( s . pid ) ;
640+ for ( const s of enriched ) {
637641 const { icon, color, pct, label } = phaseProgress (
638642 s . phase ,
639643 s . toolCalls ,
640- staleFlag ,
641- alive
644+ s . stale ,
645+ s . alive
642646 ) ;
643647 const bar = progressBar ( pct , 8 ) ;
644- const timeColor = ! alive ? c . red : staleFlag ? c . orange : c . gray ;
648+ const timeColor = ! s . alive ? c . red : s . stale ? c . orange : c . gray ;
645649
646650 const cell = [
647651 `${ color } ${ icon } ${ c . r } ${ c . b } ${ s . issue } ${ c . r } ${ color } ${ label } ${ c . r } ` ,
648- ` ${ bar } ${ c . d } ${ pct } %${ c . r } ${ c . gray } ${ s . toolCalls } t ${ s . filesModified } f${ c . r } ${ timeColor } ${ formatElapsed ( elapsed ) } ${ c . r } ` ,
652+ ` ${ bar } ${ c . d } ${ pct } %${ c . r } ${ c . gray } ${ s . toolCalls } t ${ s . filesModified } f${ c . r } ${ timeColor } ${ formatElapsed ( s . elapsed ) } ${ c . r } ` ,
649653 ] ;
650654 rows . push ( cell ) ;
651655 }
@@ -690,33 +694,14 @@ export function createConductorCommands(): Command {
690694 . description ( 'Clean up completed/dead agents that conductor missed' )
691695 . option ( '--dry-run' , 'Show what would be done without doing it' , false )
692696 . action ( async ( options ) => {
693- const agentsDir = join ( homedir ( ) , '.stackmemory' , 'conductor' , 'agents' ) ;
694- if ( ! existsSync ( agentsDir ) ) {
695- console . log ( 'No agent status files found' ) ;
696- return ;
697- }
698-
699- const entries = readdirSync ( agentsDir , { withFileTypes : true } ) ;
700- const statuses : ( AgentStatusFile & { dir : string } ) [ ] = [ ] ;
701-
702- for ( const entry of entries ) {
703- if ( ! entry . isDirectory ( ) ) continue ;
704- const statusPath = join ( agentsDir , entry . name , 'status.json' ) ;
705- if ( ! existsSync ( statusPath ) ) continue ;
706- try {
707- const data = JSON . parse ( readFileSync ( statusPath , 'utf-8' ) ) ;
708- statuses . push ( { ...( data as AgentStatusFile ) , dir : entry . name } ) ;
709- } catch {
710- // skip
711- }
712- }
697+ const statuses = scanAgentStatuses ( ) ;
713698
714- // Find agents that are dead or stale
715- const needsFinalize = statuses . filter ( ( s ) => {
716- const alive = isProcessAlive ( s . pid ) ;
717- const elapsed = Date . now ( ) - new Date ( s . lastUpdate ) . getTime ( ) ;
718- return ! alive || elapsed > 60 * 60 * 1000 ;
719- } ) ;
699+ // Find agents that are dead or stale (1 hour threshold for finalize)
700+ const needsFinalize = statuses
701+ . map ( ( s ) => ( { ... s , ... enrichStatus ( s ) } ) )
702+ . filter ( ( s ) => {
703+ return ! s . alive || s . elapsed > STALE_FINALIZE_THRESHOLD_MS ;
704+ } ) ;
720705
721706 if ( needsFinalize . length === 0 ) {
722707 console . log (
@@ -730,9 +715,7 @@ export function createConductorCommands(): Command {
730715 ) ;
731716
732717 for ( const s of needsFinalize ) {
733- const alive = isProcessAlive ( s . pid ) ;
734- const elapsed = Date . now ( ) - new Date ( s . lastUpdate ) . getTime ( ) ;
735- const elapsedStr = formatElapsed ( elapsed ) . replace ( ' ago' , '' ) ;
718+ const elapsedStr = formatElapsed ( s . elapsed ) . replace ( ' ago' , '' ) ;
736719
737720 // Check for commits in worktree
738721 let hasCommits = false ;
@@ -749,7 +732,7 @@ export function createConductorCommands(): Command {
749732 }
750733 }
751734
752- const statusIcon = ! alive
735+ const statusIcon = ! s . alive
753736 ? `${ c . red } ✗ dead${ c . r } `
754737 : `${ c . orange } ⏸ stalled ${ elapsedStr } ${ c . r } ` ;
755738 const commitStatus = hasCommits
@@ -761,7 +744,7 @@ export function createConductorCommands(): Command {
761744 if ( options . dryRun ) continue ;
762745
763746 // Kill if still alive
764- if ( alive ) {
747+ if ( s . alive ) {
765748 try {
766749 process . kill ( s . pid , 'SIGTERM' ) ;
767750 console . log ( ` ${ c . gray } Sent SIGTERM to pid ${ s . pid } ${ c . r } ` ) ;
@@ -981,30 +964,7 @@ export function createConductorCommands(): Command {
981964 // Use module-level color constants (c.b, c.d, c.r, etc.)
982965
983966 function readStatuses ( ) : AgentStatusFile [ ] {
984- const agentsDir = join (
985- homedir ( ) ,
986- '.stackmemory' ,
987- 'conductor' ,
988- 'agents'
989- ) ;
990- if ( ! existsSync ( agentsDir ) ) return [ ] ;
991- const entries = readdirSync ( agentsDir , { withFileTypes : true } ) ;
992- const statuses : AgentStatusFile [ ] = [ ] ;
993- for ( const entry of entries ) {
994- if ( ! entry . isDirectory ( ) ) continue ;
995- const statusPath = join ( agentsDir , entry . name , 'status.json' ) ;
996- if ( ! existsSync ( statusPath ) ) continue ;
997- try {
998- statuses . push ( JSON . parse ( readFileSync ( statusPath , 'utf-8' ) ) ) ;
999- } catch {
1000- // skip corrupt files
1001- }
1002- }
1003- statuses . sort (
1004- ( a , b ) =>
1005- new Date ( b . lastUpdate ) . getTime ( ) - new Date ( a . lastUpdate ) . getTime ( )
1006- ) ;
1007- // Apply phase filter
967+ const statuses = scanAgentStatuses ( ) as AgentStatusFile [ ] ;
1008968 if ( phaseFilter ) {
1009969 return statuses . filter ( ( s ) => s . phase === phaseFilter ) ;
1010970 }
@@ -1018,9 +978,7 @@ export function createConductorCommands(): Command {
1018978 return ;
1019979 }
1020980 for ( const s of statuses ) {
1021- const elapsed = Date . now ( ) - new Date ( s . lastUpdate ) . getTime ( ) ;
1022- const stale = elapsed > 5 * 60 * 1000 ;
1023- const alive = isProcessAlive ( s . pid ) ;
981+ const { elapsed, alive, stale } = enrichStatus ( s ) ;
1024982 const prog = phaseProgress ( s . phase , s . toolCalls , stale , alive ) ;
1025983 const bar = progressBar ( prog . pct , 10 ) ;
1026984
@@ -1058,9 +1016,7 @@ export function createConductorCommands(): Command {
10581016 return ;
10591017 }
10601018 for ( const s of statuses ) {
1061- const elapsed = Date . now ( ) - new Date ( s . lastUpdate ) . getTime ( ) ;
1062- const stale = elapsed > 5 * 60 * 1000 ;
1063- const alive = isProcessAlive ( s . pid ) ;
1019+ const { elapsed, alive, stale } = enrichStatus ( s ) ;
10641020 const prog = phaseProgress ( s . phase , s . toolCalls , stale , alive ) ;
10651021
10661022 console . log (
@@ -1086,10 +1042,10 @@ export function createConductorCommands(): Command {
10861042 }
10871043 }
10881044
1045+ const cachedConductor = new Conductor ( { repoRoot : process . cwd ( ) } ) ;
10891046 async function getUsage ( ) : Promise < Record < string , unknown > > {
1090- const conductor = new Conductor ( { repoRoot : process . cwd ( ) } ) ;
1091- await conductor . scanUsageLogs ( ) ;
1092- return conductor . getUsageSummary ( ) as Record < string , unknown > ;
1047+ await cachedConductor . scanUsageLogs ( ) ;
1048+ return cachedConductor . getUsageSummary ( ) as Record < string , unknown > ;
10931049 }
10941050
10951051 async function render ( ) : Promise < void > {
@@ -1148,7 +1104,7 @@ export function createConductorCommands(): Command {
11481104
11491105 console . log ( '' ) ;
11501106 console . log (
1151- `${ d } ──────────────────────────────────────────────────${ r } `
1107+ `${ c . d } ──────────────────────────────────────────────────${ c . r } `
11521108 ) ;
11531109 }
11541110
0 commit comments