feat(issues): parse agent_started/agent_done events for per-agent status tracking

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-14 16:35:08 +01:00
co-authored by Claude Opus 4.6
parent b3db0b2828
commit 7c448dff00
@@ -162,6 +162,8 @@ function parseInvestigationLogLine(line: string): {
thinkingPreview?: string;
thinkingChars?: number;
isStructured?: boolean;
lifecycleEvent?: 'started' | 'done' | 'failed';
lifecycleError?: string;
} | null {
// Try JSON-structured events first (emitted by emit_json_event in investigation_hooks.py)
if (line.startsWith('{')) {
@@ -182,6 +184,33 @@ function parseInvestigationLogLine(line: string): {
const isTool = event.event === 'tool_start' || event.event === 'tool_end';
let content = '';
// Lifecycle events: agent_started, agent_done
if (event.event === 'agent_started') {
content = 'Agent started';
return {
agentType: agentType as InvestigationAgentType | 'orchestrator',
content,
isError: false,
isTool: false,
isStructured: true,
lifecycleEvent: 'started' as const,
};
}
if (event.event === 'agent_done') {
const doneSuccess = event.success !== false;
content = doneSuccess ? 'Agent completed' : `Agent failed: ${event.error ?? 'unknown'}`;
return {
agentType: agentType as InvestigationAgentType | 'orchestrator',
content,
isError: !doneSuccess,
isTool: false,
isStructured: true,
lifecycleEvent: (doneSuccess ? 'done' : 'failed') as 'done' | 'failed',
lifecycleError: event.error,
};
}
if (event.event === 'thinking') {
content = `Thinking (${event.chars?.toLocaleString() ?? '?'} chars)`;
} else if (event.event === 'tool_start') {
@@ -301,6 +330,23 @@ class InvestigationLogCollector {
const parsed = parseInvestigationLogLine(line);
if (!parsed) return;
// Handle lifecycle events from backend
if (parsed.lifecycleEvent) {
const lifecycleAgent = this.logs.agents[parsed.agentType];
if (parsed.lifecycleEvent === 'started') {
lifecycleAgent.status = 'active';
lifecycleAgent.startedAt = new Date().toISOString();
} else if (parsed.lifecycleEvent === 'done') {
lifecycleAgent.status = 'completed';
lifecycleAgent.completedAt = new Date().toISOString();
} else if (parsed.lifecycleEvent === 'failed') {
lifecycleAgent.status = 'failed';
lifecycleAgent.completedAt = new Date().toISOString();
}
this.save();
return; // lifecycle events are metadata, not log entries
}
const agent = this.logs.agents[parsed.agentType];
const wasNotActive = agent.status !== 'active';
@@ -364,13 +410,16 @@ class InvestigationLogCollector {
finalize(success: boolean): void {
for (const agent of Object.values(this.logs.agents)) {
// Skip agents already marked by lifecycle events
if (agent.status === 'completed' || agent.status === 'failed') {
continue;
}
if (success) {
// A successful investigation means all agents completed their work.
// Mark all agents as completed regardless of current status, since
// some agents may not have emitted log lines we could parse.
agent.status = 'completed';
agent.completedAt = new Date().toISOString();
} else if (agent.status === 'active') {
agent.status = 'failed';
agent.completedAt = new Date().toISOString();
}
// On failure, pending agents stay pending (they never started)
}