fix(team): show stopped runtime from spawn status

This commit is contained in:
777genius 2026-05-24 17:03:01 +03:00
parent 4a4c67fcb9
commit a324f6cc66
2 changed files with 158 additions and 6 deletions

View file

@ -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 ||

View file

@ -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' }],