fix(team): show stopped runtime from spawn status
This commit is contained in:
parent
4a4c67fcb9
commit
a324f6cc66
2 changed files with 158 additions and 6 deletions
|
|
@ -42,6 +42,12 @@ interface SpawnDegradation {
|
||||||
diagnosticSeverity: TeamAgentRuntimeDiagnosticSeverity;
|
diagnosticSeverity: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SpawnStoppedEvidence {
|
||||||
|
reason: string;
|
||||||
|
diagnostic?: string;
|
||||||
|
diagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
const ACTIVE_SPAWN_STATUSES = new Set(['waiting', 'spawning']);
|
const ACTIVE_SPAWN_STATUSES = new Set(['waiting', 'spawning']);
|
||||||
|
|
||||||
export function buildTeamRuntimeDisplayRows({
|
export function buildTeamRuntimeDisplayRows({
|
||||||
|
|
@ -134,12 +140,16 @@ function buildRuntimeBackedDisplayRow(
|
||||||
): TeamRuntimeDisplayRow {
|
): TeamRuntimeDisplayRow {
|
||||||
const hasErrorDiagnostic = runtime.runtimeDiagnosticSeverity === 'error';
|
const hasErrorDiagnostic = runtime.runtimeDiagnosticSeverity === 'error';
|
||||||
const spawnDegradation = getSpawnDegradation(spawn);
|
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
|
const degradedReason = spawnDegradation
|
||||||
? withLiveProcessContext(spawnDegradation.reason, runtime)
|
? withLiveProcessContext(spawnDegradation.reason, runtime)
|
||||||
: undefined;
|
: undefined;
|
||||||
const stateReason =
|
const stateReason =
|
||||||
degradedReason ??
|
degradedReason ??
|
||||||
|
spawnStoppedEvidence?.reason ??
|
||||||
runtime.runtimeDiagnostic ??
|
runtime.runtimeDiagnostic ??
|
||||||
(runtime.alive === true ? 'Runtime heartbeat is alive' : 'Runtime heartbeat is not alive');
|
(runtime.alive === true ? 'Runtime heartbeat is alive' : 'Runtime heartbeat is not alive');
|
||||||
|
|
||||||
|
|
@ -157,8 +167,13 @@ function buildRuntimeBackedDisplayRow(
|
||||||
diagnostic:
|
diagnostic:
|
||||||
spawnDegradation && degradedReason
|
spawnDegradation && degradedReason
|
||||||
? withLiveProcessContext(spawnDegradation.diagnostic ?? degradedReason, runtime)
|
? withLiveProcessContext(spawnDegradation.diagnostic ?? degradedReason, runtime)
|
||||||
: runtime.runtimeDiagnostic,
|
: spawnStoppedEvidence
|
||||||
diagnosticSeverity: spawnDegradation?.diagnosticSeverity ?? runtime.runtimeDiagnosticSeverity,
|
? spawnStoppedEvidence.diagnostic
|
||||||
|
: runtime.runtimeDiagnostic,
|
||||||
|
diagnosticSeverity:
|
||||||
|
spawnDegradation?.diagnosticSeverity ??
|
||||||
|
spawnStoppedEvidence?.diagnosticSeverity ??
|
||||||
|
runtime.runtimeDiagnosticSeverity,
|
||||||
pidLabel: formatRuntimePidLabel(runtime),
|
pidLabel: formatRuntimePidLabel(runtime),
|
||||||
actionsAllowed: false,
|
actionsAllowed: false,
|
||||||
};
|
};
|
||||||
|
|
@ -207,6 +222,24 @@ function getSpawnDegradation(spawn?: MemberSpawnStatusEntry): SpawnDegradation |
|
||||||
return null;
|
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(
|
function getRuntimeBackedState(
|
||||||
runtime: TeamAgentRuntimeEntry,
|
runtime: TeamAgentRuntimeEntry,
|
||||||
hasErrorDiagnostic: boolean,
|
hasErrorDiagnostic: boolean,
|
||||||
|
|
@ -220,7 +253,11 @@ function getRuntimeBackedState(
|
||||||
}
|
}
|
||||||
|
|
||||||
function withLiveProcessContext(reason: string, runtime: TeamAgentRuntimeEntry): string {
|
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;
|
||||||
}
|
}
|
||||||
return `${reason}. Process is still alive.`;
|
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 (
|
if (
|
||||||
(spawn.status === 'online' && hasConfirmedSpawnLiveness(spawn)) ||
|
(spawn.status === 'online' && hasConfirmedSpawnLiveness(spawn)) ||
|
||||||
isConfirmedSpawnLaunch(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 {
|
function hasConfirmedSpawnLiveness(spawn: MemberSpawnStatusEntry): boolean {
|
||||||
return (
|
return (
|
||||||
spawn.runtimeAlive === true ||
|
spawn.runtimeAlive === true ||
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { buildTeamRuntimeDisplayRows } from '@renderer/components/team/teamRuntimeDisplayRows';
|
import { buildTeamRuntimeDisplayRows } from '@renderer/components/team/teamRuntimeDisplayRows';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MemberSpawnStatusEntry,
|
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', () => {
|
it('treats confirmed spawn bootstrap as running even if stale status is still waiting', () => {
|
||||||
const rows = buildTeamRuntimeDisplayRows({
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
members: [{ name: 'alice' }],
|
members: [{ name: 'alice' }],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue