Skip to content

Commit 1d35d76

Browse files
Dennisclaude
andcommitted
fix: recognize Agent tool as subagent + harden Ctrl+C exit
The analyzer hardcoded 'Task' for subagent detection but Claude Code's tool is called 'Agent', causing all subagent time to be miscategorized as tool execution. Now recognizes both names via SUBAGENT_TOOLS set. Also wraps live mode cleanup in try/catch/finally so process.exit(0) always runs, and adds a re-entrancy guard for force-exit on second Ctrl+C. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ace49cb commit 1d35d76

File tree

3 files changed

+62
-21
lines changed

3 files changed

+62
-21
lines changed

src/analyzer.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ describe('analyzer: enhanced phases', () => {
111111
expect(result.enhancedStats.claudeThink).toBe(6000);
112112
});
113113

114-
it('should classify Task tool as subagent time', () => {
114+
it('should classify Task tool as subagent time (legacy)', () => {
115115
const result = analyzeSession('s1', [
116116
userMsg('2026-01-01T00:00:00Z'),
117117
assistantMsg('2026-01-01T00:00:02Z', { toolUses: [{ id: 'tu1', name: 'Task' }] }),
@@ -122,6 +122,27 @@ describe('analyzer: enhanced phases', () => {
122122
expect(result.enhancedStats.toolExec).toBe(0);
123123
});
124124

125+
it('should classify Agent tool as subagent time', () => {
126+
const result = analyzeSession('s1', [
127+
userMsg('2026-01-01T00:00:00Z'),
128+
assistantMsg('2026-01-01T00:00:02Z', { toolUses: [{ id: 'tu1', name: 'Agent' }] }),
129+
toolResultMsg('2026-01-01T00:01:00Z', ['tu1']), // 58s subagent
130+
]);
131+
132+
expect(result.enhancedStats.subagent).toBe(58000);
133+
expect(result.enhancedStats.toolExec).toBe(0);
134+
});
135+
136+
it('should classify Agent tool as subagent in legacy phases', () => {
137+
const result = analyzeSession('s1', [
138+
userMsg('2026-01-01T00:00:00Z'),
139+
assistantMsg('2026-01-01T00:00:02Z', { toolUses: [{ id: 'tu1', name: 'Agent' }] }),
140+
toolResultMsg('2026-01-01T00:01:00Z', ['tu1']),
141+
]);
142+
143+
expect(result.stats.subagent).toBeGreaterThan(0);
144+
});
145+
125146
it('should not double-count away time in active duration', () => {
126147
const result = analyzeSession('s1', [
127148
userMsg('2026-01-01T00:00:00Z'),

src/analyzer.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
import { estimateCost } from './pricing.js';
77

88
const IDLE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
9+
const SUBAGENT_TOOLS = new Set(['Task', 'Agent']);
910

1011
export function analyzeSession(sessionId: string, messages: SessionMessage[]): SessionAnalysis {
1112
const mainMessages = messages.filter(m => !m.isSidechain);
@@ -120,7 +121,7 @@ function detectPhases(messages: SessionMessage[]): TimeSegment[] {
120121
if (!planModeActive) { closeCurrentPhase(ts); planModeActive = true; currentPhase = 'planning'; }
121122
} else if (c.name === 'ExitPlanMode') {
122123
if (planModeActive) { closeCurrentPhase(ts); planModeActive = false; currentPhase = 'coding'; }
123-
} else if (c.name === 'Task' && c.id) {
124+
} else if (c.name && SUBAGENT_TOOLS.has(c.name) && c.id) {
124125
activeSubagents.set(c.id, ts);
125126
if (activeSubagents.size === 1) { closeCurrentPhase(ts); currentPhase = 'subagent'; }
126127
}
@@ -163,8 +164,8 @@ function detectEnhancedPhases(messages: SessionMessage[]): EnhancedTimeSegment[]
163164

164165
// Track pending tool_use calls: id -> { name, timestamp }
165166
const pendingTools = new Map<string, { name: string; ts: number }>();
166-
// Track pending subagent calls (Task tool): id -> timestamp
167-
const pendingSubagents = new Map<string, number>();
167+
// Track pending subagent calls (Task/Agent tool): id -> { name, timestamp }
168+
const pendingSubagents = new Map<string, { name: string; ts: number }>();
168169

169170
let lastAssistantEndTs: number | null = null;
170171
let lastExternalUserTs: number | null = null;
@@ -189,8 +190,8 @@ function detectEnhancedPhases(messages: SessionMessage[]): EnhancedTimeSegment[]
189190
if (c.type !== 'tool_result' || !c.tool_use_id) continue;
190191

191192
if (pendingSubagents.has(c.tool_use_id)) {
192-
const startTs = pendingSubagents.get(c.tool_use_id)!;
193-
emit('subagent', startTs, ts, 'Task');
193+
const { name, ts: startTs } = pendingSubagents.get(c.tool_use_id)!;
194+
emit('subagent', startTs, ts, name);
194195
pendingSubagents.delete(c.tool_use_id);
195196
} else if (pendingTools.has(c.tool_use_id)) {
196197
const { name, ts: startTs } = pendingTools.get(c.tool_use_id)!;
@@ -245,8 +246,8 @@ function detectEnhancedPhases(messages: SessionMessage[]): EnhancedTimeSegment[]
245246
planModeActive = true;
246247
} else if (c.name === 'ExitPlanMode') {
247248
planModeActive = false;
248-
} else if (c.name === 'Task') {
249-
pendingSubagents.set(c.id, ts);
249+
} else if (c.name && SUBAGENT_TOOLS.has(c.name)) {
250+
pendingSubagents.set(c.id, { name: c.name, ts });
250251
} else {
251252
pendingTools.set(c.id, { name: c.name ?? 'unknown', ts });
252253
}

src/live.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,25 @@ export async function startLiveMode(projectFilter?: string): Promise<void> {
135135
const periodicTimer = setInterval(refresh, 5000);
136136

137137
// Graceful shutdown
138+
let cleaningUp = false;
138139
function cleanup() {
139-
if (watcher) {
140-
watcher.close();
141-
watcher = null;
140+
if (cleaningUp) {
141+
process.exit(1);
142+
}
143+
cleaningUp = true;
144+
try {
145+
if (watcher) {
146+
watcher.close();
147+
watcher = null;
148+
}
149+
if (debounceTimer) clearTimeout(debounceTimer);
150+
clearInterval(periodicTimer);
151+
process.stdout.write('\x1b[?25h');
152+
} catch {
153+
// Ignore cleanup errors
154+
} finally {
155+
process.exit(0);
142156
}
143-
if (debounceTimer) clearTimeout(debounceTimer);
144-
clearInterval(periodicTimer);
145-
// Restore cursor
146-
process.stdout.write('\x1b[?25h');
147-
process.exit(0);
148157
}
149158

150159
process.on('SIGINT', cleanup);
@@ -237,12 +246,22 @@ export async function startAggregateLiveMode(
237246
// Periodic refresh (picks up new sessions, updates wall-clock timers)
238247
const periodicTimer = setInterval(refresh, 5000);
239248

249+
let cleaningUp = false;
240250
function cleanup() {
241-
if (watcher) { watcher.close(); watcher = null; }
242-
if (debounceTimer) clearTimeout(debounceTimer);
243-
clearInterval(periodicTimer);
244-
process.stdout.write('\x1b[?25h');
245-
process.exit(0);
251+
if (cleaningUp) {
252+
process.exit(1);
253+
}
254+
cleaningUp = true;
255+
try {
256+
if (watcher) { watcher.close(); watcher = null; }
257+
if (debounceTimer) clearTimeout(debounceTimer);
258+
clearInterval(periodicTimer);
259+
process.stdout.write('\x1b[?25h');
260+
} catch {
261+
// Ignore cleanup errors
262+
} finally {
263+
process.exit(0);
264+
}
246265
}
247266

248267
process.on('SIGINT', cleanup);

0 commit comments

Comments
 (0)