From a324f6cc6620797689fe6794f62a1938f7e6b13c Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 24 May 2026 17:03:01 +0300 Subject: [PATCH] fix(team): show stopped runtime from spawn status --- .../components/team/teamRuntimeDisplayRows.ts | 72 ++++++++++++++- .../team/teamRuntimeDisplayRows.test.ts | 92 ++++++++++++++++++- 2 files changed, 158 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/team/teamRuntimeDisplayRows.ts b/src/renderer/components/team/teamRuntimeDisplayRows.ts index 430c2d9a..0737cbf0 100644 --- a/src/renderer/components/team/teamRuntimeDisplayRows.ts +++ b/src/renderer/components/team/teamRuntimeDisplayRows.ts @@ -42,6 +42,12 @@ interface SpawnDegradation { diagnosticSeverity: TeamAgentRuntimeDiagnosticSeverity; } +interface SpawnStoppedEvidence { + reason: string; + diagnostic?: string; + diagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity; +} + const ACTIVE_SPAWN_STATUSES = new Set(['waiting', 'spawning']); export function buildTeamRuntimeDisplayRows({ @@ -134,12 +140,16 @@ function buildRuntimeBackedDisplayRow( ): TeamRuntimeDisplayRow { const hasErrorDiagnostic = runtime.runtimeDiagnosticSeverity === 'error'; const spawnDegradation = getSpawnDegradation(spawn); - const state = getRuntimeBackedState(runtime, hasErrorDiagnostic, spawnDegradation != null); + const spawnStoppedEvidence = spawnDegradation ? null : getSpawnStoppedEvidence(runtime, spawn); + const state = spawnStoppedEvidence + ? 'stopped' + : getRuntimeBackedState(runtime, hasErrorDiagnostic, spawnDegradation != null); const degradedReason = spawnDegradation ? withLiveProcessContext(spawnDegradation.reason, runtime) : undefined; const stateReason = degradedReason ?? + spawnStoppedEvidence?.reason ?? runtime.runtimeDiagnostic ?? (runtime.alive === true ? 'Runtime heartbeat is alive' : 'Runtime heartbeat is not alive'); @@ -157,8 +167,13 @@ function buildRuntimeBackedDisplayRow( diagnostic: spawnDegradation && degradedReason ? withLiveProcessContext(spawnDegradation.diagnostic ?? degradedReason, runtime) - : runtime.runtimeDiagnostic, - diagnosticSeverity: spawnDegradation?.diagnosticSeverity ?? runtime.runtimeDiagnosticSeverity, + : spawnStoppedEvidence + ? spawnStoppedEvidence.diagnostic + : runtime.runtimeDiagnostic, + diagnosticSeverity: + spawnDegradation?.diagnosticSeverity ?? + spawnStoppedEvidence?.diagnosticSeverity ?? + runtime.runtimeDiagnosticSeverity, pidLabel: formatRuntimePidLabel(runtime), actionsAllowed: false, }; @@ -207,6 +222,24 @@ function getSpawnDegradation(spawn?: MemberSpawnStatusEntry): SpawnDegradation | return null; } +function getSpawnStoppedEvidence( + runtime: TeamAgentRuntimeEntry, + spawn?: MemberSpawnStatusEntry +): SpawnStoppedEvidence | null { + if (!spawn || spawn.runtimeAlive !== false || runtime.livenessKind !== 'confirmed_bootstrap') { + return null; + } + if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') { + return null; + } + const reason = spawn.runtimeDiagnostic ?? 'Spawn status reports runtime is not alive'; + return { + reason, + diagnostic: spawn.runtimeDiagnostic ?? reason, + diagnosticSeverity: spawn.runtimeDiagnosticSeverity ?? 'warning', + }; +} + function getRuntimeBackedState( runtime: TeamAgentRuntimeEntry, hasErrorDiagnostic: boolean, @@ -220,7 +253,11 @@ function getRuntimeBackedState( } function withLiveProcessContext(reason: string, runtime: TeamAgentRuntimeEntry): string { - if (runtime.alive !== true || /process is still alive/i.test(reason)) { + if ( + runtime.alive !== true || + runtime.livenessKind === 'confirmed_bootstrap' || + /process is still alive/i.test(reason) + ) { return reason; } return `${reason}. Process is still alive.`; @@ -245,6 +282,21 @@ function buildSpawnBackedDisplayRow( }; } + const spawnStoppedEvidence = getSpawnOnlyStoppedEvidence(spawn); + if (spawnStoppedEvidence) { + return { + memberName, + state: 'stopped', + stateReason: spawnStoppedEvidence.reason, + source: 'spawn-status', + updatedAt: spawn.livenessLastCheckedAt ?? spawn.updatedAt, + runtimeModel: spawn.runtimeModel, + diagnostic: spawnStoppedEvidence.diagnostic, + diagnosticSeverity: spawnStoppedEvidence.diagnosticSeverity, + actionsAllowed: false, + }; + } + if ( (spawn.status === 'online' && hasConfirmedSpawnLiveness(spawn)) || isConfirmedSpawnLaunch(spawn) @@ -306,6 +358,18 @@ function buildSpawnBackedDisplayRow( }; } +function getSpawnOnlyStoppedEvidence(spawn: MemberSpawnStatusEntry): SpawnStoppedEvidence | null { + if (spawn.runtimeAlive !== false) return null; + if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') return null; + + const reason = spawn.runtimeDiagnostic ?? 'Spawn status reports runtime is not alive'; + return { + reason, + diagnostic: spawn.runtimeDiagnostic ?? reason, + diagnosticSeverity: spawn.runtimeDiagnosticSeverity ?? 'warning', + }; +} + function hasConfirmedSpawnLiveness(spawn: MemberSpawnStatusEntry): boolean { return ( spawn.runtimeAlive === true || diff --git a/test/renderer/components/team/teamRuntimeDisplayRows.test.ts b/test/renderer/components/team/teamRuntimeDisplayRows.test.ts index 3298f44e..8ac8c479 100644 --- a/test/renderer/components/team/teamRuntimeDisplayRows.test.ts +++ b/test/renderer/components/team/teamRuntimeDisplayRows.test.ts @@ -1,6 +1,5 @@ -import { describe, expect, it } from 'vitest'; - import { buildTeamRuntimeDisplayRows } from '@renderer/components/team/teamRuntimeDisplayRows'; +import { describe, expect, it } from 'vitest'; import type { MemberSpawnStatusEntry, @@ -92,6 +91,95 @@ describe('buildTeamRuntimeDisplayRows', () => { }); }); + it('does not show bootstrap-only runtime evidence as running when spawn says runtime is stopped', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'bootstrap confirmed', + runtimeDiagnosticSeverity: 'info', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'online', + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + runtimeAlive: false, + runtimeDiagnostic: 'persisted runtime pid is not alive', + runtimeDiagnosticSeverity: 'warning', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'stopped', + source: 'mixed', + stateReason: 'persisted runtime pid is not alive', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + + it('keeps spawn degradation stronger than stopped evidence for mixed rows', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'bootstrap confirmed', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'online', + launchState: 'runtime_pending_permission', + bootstrapConfirmed: true, + runtimeAlive: false, + pendingPermissionRequestIds: ['perm-1'], + runtimeDiagnostic: 'Runtime is waiting for permission approval', + runtimeDiagnosticSeverity: 'warning', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'degraded', + source: 'mixed', + stateReason: 'Runtime is waiting for permission approval', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + + it('does not show spawn-only confirmed bootstrap as running when spawn says runtime is stopped', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + spawnStatuses: { + alice: createSpawnStatus({ + status: 'online', + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + runtimeAlive: false, + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'stopped', + source: 'spawn-status', + stateReason: 'Spawn status reports runtime is not alive', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + it('treats confirmed spawn bootstrap as running even if stale status is still waiting', () => { const rows = buildTeamRuntimeDisplayRows({ members: [{ name: 'alice' }],