Skip to content

Commit 9646080

Browse files
author
StackMemory Bot (CLI)
committed
refactor(conductor): extract shared helpers, fix broken monitor footer
- Extract scanAgentStatuses() and enrichStatus() to deduplicate 3 scan loops - Replace magic numbers with STALE_UI_THRESHOLD_MS / STALE_FINALIZE_THRESHOLD_MS - Fix broken monitor footer using out-of-scope d/r vars → c.d/c.r - Remove shadowed ANSI vars in printUsageSummary, use module-level c.* - Cache Conductor instance in monitor to avoid re-instantiation per render
1 parent 40ac01f commit 9646080

File tree

1 file changed

+74
-118
lines changed

1 file changed

+74
-118
lines changed

src/cli/commands/orchestrate.ts

Lines changed: 74 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -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 ──
100104
const 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+
209249
function 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

Comments
 (0)