Skip to content

Commit 4057d03

Browse files
brookscclaude
andcommitted
fix(mcp): fix stale task status and missing exit indicator in get_task_output
Three improvements to make agent exit visible to coordinators: 1. Export hasAgentSession() from pty.ts — lets the orchestrator do a synchronous liveness check without a round-trip through the event bus. 2. getTaskStatus proactive check: if the PTY session is gone but the task still shows a live status (can happen when the renderer re-spawns an agent, killing the old one without firing an exit event), mark it exited immediately so the caller doesn't see stale "running". 3. getTaskOutput exit indicator: append "[Process exited with code X (signal Y)]" when the agent is dead. Previously the last output buffer showed "almost done thinking" with no indication the process had exited. Also capture exitSignal in OrchestratedTask/ApiTaskDetail so coordinators can distinguish SIGHUP (129) from clean exits. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 56d396f commit 4057d03

3 files changed

Lines changed: 36 additions & 5 deletions

File tree

electron/ipc/pty.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,11 @@ export function getActiveAgentIds(): string[] {
460460
return Array.from(sessions.keys());
461461
}
462462

463+
/** Check whether a PTY session exists for this agent (i.e. process is alive). */
464+
export function hasAgentSession(agentId: string): boolean {
465+
return sessions.has(agentId);
466+
}
467+
463468
/** Return metadata for a specific agent, or null if not found. */
464469
export function getAgentMeta(
465470
agentId: string,

electron/mcp/orchestrator.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
subscribeToAgent,
1212
unsubscribeFromAgent,
1313
getAgentScrollback,
14+
hasAgentSession,
1415
onPtyEvent,
1516
} from '../ipc/pty.js';
1617
import { getChangedFiles, getAllFileDiffs, mergeTask as gitMergeTask } from '../ipc/git.js';
@@ -40,9 +41,10 @@ export class Orchestrator {
4041
onPtyEvent('exit', (agentId, data) => {
4142
for (const task of this.tasks.values()) {
4243
if (task.agentId === agentId) {
43-
const { exitCode } = (data ?? {}) as { exitCode?: number };
44+
const { exitCode, signal } = (data ?? {}) as { exitCode?: number; signal?: number };
4445
task.status = 'exited';
4546
task.exitCode = exitCode ?? null;
47+
task.exitSignal = signal !== undefined ? String(signal) : null;
4648
// Resolve any idle waiters so they don't hang
4749
const resolvers = this.idleResolvers.get(task.id);
4850
if (resolvers?.length) {
@@ -109,6 +111,7 @@ export class Orchestrator {
109111
coordinatorTaskId: coordinatorId,
110112
status: 'creating',
111113
exitCode: null,
114+
exitSignal: null,
112115
};
113116

114117
this.tasks.set(task.id, task);
@@ -210,6 +213,16 @@ export class Orchestrator {
210213
getTaskStatus(taskId: string): ApiTaskDetail | null {
211214
const task = this.tasks.get(taskId);
212215
if (!task) return null;
216+
217+
// Proactive liveness check: if the PTY session is gone but the task still
218+
// shows a live status, the exit event was suppressed (e.g. the renderer
219+
// spawned a new session with the same agentId, killing the old one without
220+
// firing an exit event). Sync the status here so callers always get a
221+
// consistent view without waiting for the next PTY event.
222+
if (task.status !== 'exited' && task.status !== 'error' && !hasAgentSession(task.agentId)) {
223+
task.status = 'exited';
224+
}
225+
213226
return {
214227
id: task.id,
215228
name: task.name,
@@ -220,6 +233,7 @@ export class Orchestrator {
220233
status: task.status,
221234
coordinatorTaskId: task.coordinatorTaskId,
222235
exitCode: task.exitCode,
236+
exitSignal: task.exitSignal,
223237
pendingPrompt: task.pendingPrompt,
224238
};
225239
}
@@ -295,11 +309,21 @@ export class Orchestrator {
295309

296310
// Try scrollback buffer first, fall back to tail buffer
297311
const scrollback = getAgentScrollback(task.agentId);
298-
if (scrollback) {
299-
const decoded = Buffer.from(scrollback, 'base64').toString('utf8');
300-
return stripAnsi(decoded);
312+
const raw = scrollback
313+
? Buffer.from(scrollback, 'base64').toString('utf8')
314+
: (this.tailBuffers.get(task.agentId) ?? '');
315+
316+
let output = stripAnsi(raw);
317+
318+
// Append a clear exit notice so callers don't interpret stale "last words"
319+
// as the current state of a running agent.
320+
if (task.status === 'exited' || (task.status !== 'error' && !hasAgentSession(task.agentId))) {
321+
const code = task.exitCode !== null ? task.exitCode : '?';
322+
const sig = task.exitSignal ? ` (signal ${task.exitSignal})` : '';
323+
output += `\n\n[Process exited with code ${code}${sig}]`;
301324
}
302-
return stripAnsi(this.tailBuffers.get(task.agentId) ?? '');
325+
326+
return output;
303327
}
304328

305329
async mergeTask(

electron/mcp/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface OrchestratedTask {
1010
coordinatorTaskId: string;
1111
status: 'creating' | 'running' | 'idle' | 'exited' | 'error';
1212
exitCode: number | null;
13+
exitSignal: string | null;
1314
pendingPrompt?: string;
1415
}
1516

@@ -71,6 +72,7 @@ export interface ApiTaskDetail extends ApiTaskSummary {
7172
projectId: string;
7273
agentId: string;
7374
exitCode: number | null;
75+
exitSignal: string | null;
7476
pendingPrompt?: string;
7577
}
7678

0 commit comments

Comments
 (0)