diff --git a/src/main/daemon/daemon-protocol-version.test.ts b/src/main/daemon/daemon-protocol-version.test.ts index dfe84947ff5..78b41a947c5 100644 --- a/src/main/daemon/daemon-protocol-version.test.ts +++ b/src/main/daemon/daemon-protocol-version.test.ts @@ -19,7 +19,7 @@ import { describe('daemon protocol version', () => { it('ships bounded history transfer after the 2031-unsubscribe fact', () => { - expect(PROTOCOL_VERSION).toBe(36) + expect(PROTOCOL_VERSION).toBe(37) expect(CONTENT_ADDRESSED_SHELL_WRAPPER_DAEMON_PROTOCOL_VERSION).toBe(36) expect(ASYNC_CWD_VALIDATION_DAEMON_PROTOCOL_VERSION).toBe(35) expect(CODEX_SHELL_LAUNCH_PREFLIGHT_DAEMON_PROTOCOL_VERSION).toBe(34) @@ -33,7 +33,7 @@ describe('daemon protocol version', () => { expect(AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION).toBe(26) expect(AGENT_SESSION_CREATE_OPERATION_DAEMON_PROTOCOL_VERSION).toBe(26) expect(PREVIOUS_DAEMON_PROTOCOL_VERSIONS).toEqual( - Array.from({ length: 35 }, (_, index) => index + 1) + Array.from({ length: 36 }, (_, index) => index + 1) ) }) diff --git a/src/main/daemon/daemon-protocol-version.ts b/src/main/daemon/daemon-protocol-version.ts index 492d8203e83..d348d065755 100644 --- a/src/main/daemon/daemon-protocol-version.ts +++ b/src/main/daemon/daemon-protocol-version.ts @@ -31,7 +31,7 @@ export const CLEAN_DISCONNECT_PROTOCOL_VERSION = 24 export const MODE_2031_UNSUBSCRIBE_FACT_PROTOCOL_VERSION = 29 export const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, - 28, 29, 30, 31, 32, 33, 34, 35 + 28, 29, 30, 31, 32, 33, 34, 35, 36 ] as const export function supportsPtyStartupIngress(protocolVersion: number): boolean { diff --git a/src/main/ipc/pty/ipc/shutdown-single.ts b/src/main/ipc/pty/ipc/shutdown-single.ts index f01e2fa9cfd..ecd616ac7cd 100644 --- a/src/main/ipc/pty/ipc/shutdown-single.ts +++ b/src/main/ipc/pty/ipc/shutdown-single.ts @@ -72,6 +72,7 @@ export async function shutdownSinglePty( // A pending daemon teardown can reject a stale incarnation without throwing. // Keep client ownership intact until bulk verification classifies that refusal. if (result?.fenceUnavailable) { + deps.runtime?.clearPtyStopRequested?.(id) return result } } catch (err) { diff --git a/src/main/providers/local-pty-termination.ts b/src/main/providers/local-pty-termination.ts index 3ce96c6a4a8..2295766654b 100644 --- a/src/main/providers/local-pty-termination.ts +++ b/src/main/providers/local-pty-termination.ts @@ -217,7 +217,7 @@ export async function shutdownLocalPty( ): Promise { const expectedIncarnationId = ptyIncarnations.get(id) if (opts.incarnationId !== undefined && opts.incarnationId !== expectedIncarnationId) { - throw new Error(`PTY incarnation mismatch: ${id}`) + return { fenceUnavailable: true } } cancelPendingLocalPtySpawns(id) const pending = ptyShutdownOperations.get(id) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 6af4a05a560..9ee66d8e89e 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -20823,6 +20823,10 @@ export class OrcaRuntimeService { this.stopRequestedPtyIds.add(ptyId) } + clearPtyStopRequested(ptyId: string): void { + this.stopRequestedPtyIds.delete(ptyId) + } + isPtyStopRequested(ptyId: string): boolean { return this.stopRequestedPtyIds.has(ptyId) } diff --git a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts index 06897de1c47..aaa34695359 100644 --- a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts +++ b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts @@ -36,6 +36,7 @@ import type { UnifiedWorktreeRow } from './resource-usage-merge-types' import { buildResourceSessionBindingIndex } from './resource-session-bindings' +import { killVerdictFields } from './resource-session-kill-verdict-fields' // ─── Helpers ──────────────────────────────────────────────────────── @@ -134,6 +135,11 @@ function resolveDaemonSessionLabel( export const UNATTRIBUTED_REPO_ID = '__unattributed__' export const UNATTRIBUTED_REPO_NAME = 'Unattributed' +type SessionWithKillVerdict = DaemonSession & { + killVerdict?: UnifiedSessionRow['killVerdict'] + killReason?: string +} + export function mergeSnapshotAndSessions( snapshot: MemorySnapshot | null, daemonSessions: readonly DaemonSession[], @@ -152,6 +158,9 @@ export function mergeSnapshotAndSessions( const ownershipBySessionId = new Map( daemonSessions.map((session) => [session.id, session.agentOwnership]) ) + const verdictBySessionId = new Map( + daemonSessions.map((session) => [session.id, session as SessionWithKillVerdict]) + ) function isRepoRemote(repoId: string): boolean { // Why: missing entry === we don't know about this repo (typically the @@ -206,8 +215,10 @@ export function mergeSnapshotAndSessions( const sessions: UnifiedSessionRow[] = wt.sessions.map((s) => { seenSessionIds.add(s.sessionId) const tabId = index.ptyIdToTabId.get(s.sessionId) ?? null + const verdict = verdictBySessionId.get(s.sessionId) return { sessionId: s.sessionId, + ...(verdict ? killVerdictFields(verdict) : {}), paneKey: s.paneKey, pid: s.pid, label: resolveSnapshotSessionLabel(s, wt.worktreeId, ctx), @@ -297,6 +308,7 @@ export function mergeSnapshotAndSessions( row.sessions.push({ sessionId: session.id, ...(session.incarnationId ? { incarnationId: session.incarnationId } : {}), + ...killVerdictFields(session as SessionWithKillVerdict), paneKey: null, pid: 0, label: resolveDaemonSessionLabel(session, worktreeId, tabId, ctx), diff --git a/src/renderer/src/components/status-bar/resource-session-kill-verdict-fields.ts b/src/renderer/src/components/status-bar/resource-session-kill-verdict-fields.ts new file mode 100644 index 00000000000..d4360eaa8a6 --- /dev/null +++ b/src/renderer/src/components/status-bar/resource-session-kill-verdict-fields.ts @@ -0,0 +1,17 @@ +import type { UnifiedSessionRow } from './resource-usage-merge-types' + +type SessionWithKillVerdict = { + killVerdict?: UnifiedSessionRow['killVerdict'] + killReason?: string +} + +export function killVerdictFields( + session: SessionWithKillVerdict +): Pick { + return session.killVerdict + ? { + killVerdict: session.killVerdict, + ...(session.killReason ? { killReason: session.killReason } : {}) + } + : {} +}