fix(team): reconcile provisioned-but-not-alive bootstrap state
This commit is contained in:
parent
9d5f176597
commit
ebcc0e717f
38 changed files with 5114 additions and 418 deletions
|
|
@ -38,6 +38,10 @@ import {
|
||||||
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
|
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import {
|
import {
|
||||||
isTeamTaskActivelyWorked,
|
isTeamTaskActivelyWorked,
|
||||||
isTeamTaskNeedsFixActionable,
|
isTeamTaskNeedsFixActionable,
|
||||||
|
|
@ -560,6 +564,7 @@ export class TeamGraphAdapter {
|
||||||
member.runtimeAdvisory,
|
member.runtimeAdvisory,
|
||||||
member.providerId,
|
member.providerId,
|
||||||
spawn,
|
spawn,
|
||||||
|
runtimeEntry,
|
||||||
pendingApprovalAgents?.has(member.name) ?? false
|
pendingApprovalAgents?.has(member.name) ?? false
|
||||||
);
|
);
|
||||||
const currentTask = member.currentTaskId
|
const currentTask = member.currentTaskId
|
||||||
|
|
@ -581,7 +586,11 @@ export class TeamGraphAdapter {
|
||||||
spawnBootstrapStalled: spawn?.bootstrapStalled,
|
spawnBootstrapStalled: spawn?.bootstrapStalled,
|
||||||
spawnAgentToolAccepted: spawn?.agentToolAccepted,
|
spawnAgentToolAccepted: spawn?.agentToolAccepted,
|
||||||
spawnHardFailure: spawn?.hardFailure,
|
spawnHardFailure: spawn?.hardFailure,
|
||||||
|
spawnHardFailureReason: spawn?.hardFailureReason,
|
||||||
|
spawnError: spawn?.error,
|
||||||
|
spawnRuntimeDiagnostic: spawn?.runtimeDiagnostic,
|
||||||
spawnLivenessKind: spawn?.livenessKind,
|
spawnLivenessKind: spawn?.livenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity: spawn?.runtimeDiagnosticSeverity,
|
||||||
spawnFirstSpawnAcceptedAt: spawn?.firstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt: spawn?.firstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt: spawn?.updatedAt,
|
spawnUpdatedAt: spawn?.updatedAt,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
|
|
@ -599,7 +608,7 @@ export class TeamGraphAdapter {
|
||||||
? 'terminated'
|
? 'terminated'
|
||||||
: hasRunningTool
|
: hasRunningTool
|
||||||
? 'tool_calling'
|
? 'tool_calling'
|
||||||
: TeamGraphAdapter.#mapMemberStatus(member.status, spawn),
|
: TeamGraphAdapter.#mapMemberStatus(member.status, spawn, runtimeEntry),
|
||||||
color: isTeamVisualOnline ? (member.color ?? undefined) : undefined,
|
color: isTeamVisualOnline ? (member.color ?? undefined) : undefined,
|
||||||
role: member.role ?? undefined,
|
role: member.role ?? undefined,
|
||||||
runtimeLabel: TeamGraphAdapter.#getRuntimeLabel(
|
runtimeLabel: TeamGraphAdapter.#getRuntimeLabel(
|
||||||
|
|
@ -1269,9 +1278,15 @@ export class TeamGraphAdapter {
|
||||||
runtimeAdvisory: ResolvedTeamMember['runtimeAdvisory'],
|
runtimeAdvisory: ResolvedTeamMember['runtimeAdvisory'],
|
||||||
providerId: ResolvedTeamMember['providerId'],
|
providerId: ResolvedTeamMember['providerId'],
|
||||||
spawn: MemberSpawnStatusEntry | undefined,
|
spawn: MemberSpawnStatusEntry | undefined,
|
||||||
|
runtimeEntry: TeamAgentRuntimeEntry | undefined,
|
||||||
pendingApproval: boolean
|
pendingApproval: boolean
|
||||||
): Pick<GraphNode, 'exceptionTone' | 'exceptionLabel'> | undefined {
|
): Pick<GraphNode, 'exceptionTone' | 'exceptionLabel'> | undefined {
|
||||||
if (spawn?.launchState === 'failed_to_start' || spawn?.status === 'error') {
|
const hasUnsuppressedSpawnFailure =
|
||||||
|
TeamGraphAdapter.#hasUnsuppressedProvisionedButNotAliveFailure(spawn, runtimeEntry);
|
||||||
|
if (
|
||||||
|
hasUnsuppressedSpawnFailure &&
|
||||||
|
(spawn?.launchState === 'failed_to_start' || spawn?.status === 'error')
|
||||||
|
) {
|
||||||
return { exceptionTone: 'error', exceptionLabel: 'spawn failed' };
|
return { exceptionTone: 'error', exceptionLabel: 'spawn failed' };
|
||||||
}
|
}
|
||||||
if (pendingApproval || spawn?.launchState === 'runtime_pending_permission') {
|
if (pendingApproval || spawn?.launchState === 'runtime_pending_permission') {
|
||||||
|
|
@ -1290,10 +1305,19 @@ export class TeamGraphAdapter {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
static #mapMemberStatus(status: string, spawn?: MemberSpawnStatusEntry): GraphNodeState {
|
static #mapMemberStatus(
|
||||||
|
status: string,
|
||||||
|
spawn?: MemberSpawnStatusEntry,
|
||||||
|
runtimeEntry?: TeamAgentRuntimeEntry
|
||||||
|
): GraphNodeState {
|
||||||
if (spawn?.launchState === 'runtime_pending_permission') return 'waiting';
|
if (spawn?.launchState === 'runtime_pending_permission') return 'waiting';
|
||||||
if (spawn?.status === 'spawning') return 'thinking';
|
if (spawn?.status === 'spawning') return 'thinking';
|
||||||
if (spawn?.status === 'error') return 'error';
|
if (
|
||||||
|
spawn?.status === 'error' &&
|
||||||
|
TeamGraphAdapter.#hasUnsuppressedProvisionedButNotAliveFailure(spawn, runtimeEntry)
|
||||||
|
) {
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
if (spawn?.status === 'waiting') return 'waiting';
|
if (spawn?.status === 'waiting') return 'waiting';
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
|
|
@ -1307,6 +1331,16 @@ export class TeamGraphAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static #hasUnsuppressedProvisionedButNotAliveFailure(
|
||||||
|
spawn: MemberSpawnStatusEntry | undefined,
|
||||||
|
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
!isBootstrapConfirmedProvisionedButNotAliveFailure(spawn) ||
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawn, runtimeEntry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static #mapTaskStatus(status: string): GraphNodeState {
|
static #mapTaskStatus(status: string): GraphNodeState {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
|
|
|
||||||
|
|
@ -373,7 +373,11 @@ const MemberPopoverContent = ({
|
||||||
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
|
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
|
||||||
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
||||||
spawnHardFailure: spawnEntry?.hardFailure,
|
spawnHardFailure: spawnEntry?.hardFailure,
|
||||||
|
spawnHardFailureReason: spawnEntry?.hardFailureReason,
|
||||||
|
spawnError: spawnEntry?.error,
|
||||||
|
spawnRuntimeDiagnostic: spawnEntry?.runtimeDiagnostic,
|
||||||
spawnLivenessKind: spawnEntry?.livenessKind,
|
spawnLivenessKind: spawnEntry?.livenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity,
|
||||||
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt: spawnEntry?.updatedAt,
|
spawnUpdatedAt: spawnEntry?.updatedAt,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,213 @@ describe('buildMixedPersistedLaunchSnapshot', () => {
|
||||||
expect(snapshot.teamLaunchState).toBe('partial_failure');
|
expect(snapshot.teamLaunchState).toBe('partial_failure');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('heals bootstrap-confirmed provisioned-but-not-alive primary status while building snapshots', () => {
|
||||||
|
const snapshot = buildMixedPersistedLaunchSnapshot({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
leadDefaults: {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
providerBackendId: null,
|
||||||
|
selectedFastMode: 'off',
|
||||||
|
resolvedFastMode: false,
|
||||||
|
launchIdentity: null,
|
||||||
|
},
|
||||||
|
primaryMembers: [{ name: 'tom', providerId: 'anthropic', model: 'sonnet', effort: 'medium' }],
|
||||||
|
primaryStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
} as never,
|
||||||
|
},
|
||||||
|
secondaryMembers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.members.tom).toMatchObject({
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
hardFailure: false,
|
||||||
|
hardFailureReason: undefined,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
});
|
||||||
|
expect(snapshot.summary).toMatchObject({
|
||||||
|
confirmedCount: 1,
|
||||||
|
failedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
});
|
||||||
|
expect(snapshot.teamLaunchState).toBe('clean_success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('heals Windows process-table-unavailable provisioned-but-not-alive primary metadata', () => {
|
||||||
|
const snapshot = buildMixedPersistedLaunchSnapshot({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
leadDefaults: {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
providerBackendId: null,
|
||||||
|
selectedFastMode: 'off',
|
||||||
|
resolvedFastMode: false,
|
||||||
|
launchIdentity: null,
|
||||||
|
},
|
||||||
|
primaryMembers: [{ name: 'tom', providerId: 'anthropic', model: 'sonnet', effort: 'medium' }],
|
||||||
|
primaryStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'stale_metadata',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
} as never,
|
||||||
|
},
|
||||||
|
secondaryMembers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.members.tom).toMatchObject({
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
hardFailure: false,
|
||||||
|
});
|
||||||
|
expect(snapshot.summary).toMatchObject({
|
||||||
|
confirmedCount: 1,
|
||||||
|
failedCount: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps bootstrap-confirmed provisioned-but-not-alive primary status failed when diagnostics are errors', () => {
|
||||||
|
const snapshot = buildMixedPersistedLaunchSnapshot({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
leadDefaults: {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
providerBackendId: null,
|
||||||
|
selectedFastMode: 'off',
|
||||||
|
resolvedFastMode: false,
|
||||||
|
launchIdentity: null,
|
||||||
|
},
|
||||||
|
primaryMembers: [{ name: 'tom', providerId: 'anthropic', model: 'sonnet', effort: 'medium' }],
|
||||||
|
primaryStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
} as never,
|
||||||
|
},
|
||||||
|
secondaryMembers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.members.tom).toMatchObject({
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
});
|
||||||
|
expect(snapshot.summary).toMatchObject({
|
||||||
|
confirmedCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
pendingCount: 0,
|
||||||
|
});
|
||||||
|
expect(snapshot.teamLaunchState).toBe('partial_failure');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps bootstrap-confirmed provisioned-but-not-alive secondary status failed when liveness is stopped', () => {
|
||||||
|
const snapshot = buildMixedPersistedLaunchSnapshot({
|
||||||
|
teamName: 'mixed-team',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
leadDefaults: {
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
selectedFastMode: 'off',
|
||||||
|
resolvedFastMode: false,
|
||||||
|
launchIdentity: null,
|
||||||
|
},
|
||||||
|
primaryMembers: [],
|
||||||
|
primaryStatuses: {},
|
||||||
|
secondaryMembers: [
|
||||||
|
{
|
||||||
|
laneId: 'secondary:opencode:tom',
|
||||||
|
member: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'opencode',
|
||||||
|
model: 'minimax-m2.5-free',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
leadDefaults: {
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
selectedFastMode: 'off',
|
||||||
|
resolvedFastMode: false,
|
||||||
|
launchIdentity: null,
|
||||||
|
},
|
||||||
|
evidence: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
} as never,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.members.tom).toMatchObject({
|
||||||
|
laneKind: 'secondary',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
hardFailure: true,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
});
|
||||||
|
expect(snapshot.summary).toMatchObject({
|
||||||
|
confirmedCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
pendingCount: 0,
|
||||||
|
});
|
||||||
|
expect(snapshot.teamLaunchState).toBe('partial_failure');
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves permission-blocked side-lane members as runtime_pending_permission', () => {
|
it('preserves permission-blocked side-lane members as runtime_pending_permission', () => {
|
||||||
const snapshot = buildMixedPersistedLaunchSnapshot({
|
const snapshot = buildMixedPersistedLaunchSnapshot({
|
||||||
teamName: 'mixed-team',
|
teamName: 'mixed-team',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MemberLaunchState,
|
MemberLaunchState,
|
||||||
MemberSpawnLivenessSource,
|
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
OpenCodeAppManagedBootstrapCandidate,
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
OpenCodeBootstrapEvidenceSource,
|
OpenCodeBootstrapEvidenceSource,
|
||||||
|
|
@ -95,6 +98,20 @@ function preservesStrongRuntimeAlive(value: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canHealBootstrapConfirmedProvisionedButNotAliveFailure(
|
||||||
|
entry:
|
||||||
|
| (Parameters<typeof isBootstrapConfirmedProvisionedButNotAliveFailure>[0] & {
|
||||||
|
runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
|
})
|
||||||
|
| undefined
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(entry) &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasMaterializedOpenCodeRuntimeMarker(value: {
|
function hasMaterializedOpenCodeRuntimeMarker(value: {
|
||||||
runtimeAlive?: boolean;
|
runtimeAlive?: boolean;
|
||||||
runtimePid?: number;
|
runtimePid?: number;
|
||||||
|
|
@ -233,16 +250,22 @@ function createPrimaryLaneMemberState(params: {
|
||||||
const runtime = params.status;
|
const runtime = params.status;
|
||||||
const strongRuntimeAlive = preservesStrongRuntimeAlive(runtime ?? {});
|
const strongRuntimeAlive = preservesStrongRuntimeAlive(runtime ?? {});
|
||||||
const sources = runtime ? createSourcesFromStatus(runtime) : undefined;
|
const sources = runtime ? createSourcesFromStatus(runtime) : undefined;
|
||||||
const launchState =
|
const healBootstrapConfirmedProvisionedButNotAlive =
|
||||||
runtime?.launchState ??
|
canHealBootstrapConfirmedProvisionedButNotAliveFailure(runtime);
|
||||||
deriveMemberLaunchState({
|
const runtimeAlive = healBootstrapConfirmedProvisionedButNotAlive || strongRuntimeAlive;
|
||||||
hardFailure: runtime?.hardFailure,
|
const launchState = healBootstrapConfirmedProvisionedButNotAlive
|
||||||
bootstrapConfirmed: runtime?.bootstrapConfirmed,
|
? 'confirmed_alive'
|
||||||
runtimeAlive: strongRuntimeAlive,
|
: (runtime?.launchState ??
|
||||||
agentToolAccepted: runtime?.agentToolAccepted,
|
deriveMemberLaunchState({
|
||||||
pendingPermissionRequestIds: runtime?.pendingPermissionRequestIds,
|
hardFailure: runtime?.hardFailure,
|
||||||
});
|
bootstrapConfirmed: runtime?.bootstrapConfirmed,
|
||||||
const hardFailure = runtime?.hardFailure === true || launchState === 'failed_to_start';
|
runtimeAlive: strongRuntimeAlive,
|
||||||
|
agentToolAccepted: runtime?.agentToolAccepted,
|
||||||
|
pendingPermissionRequestIds: runtime?.pendingPermissionRequestIds,
|
||||||
|
}));
|
||||||
|
const hardFailure =
|
||||||
|
!healBootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
(runtime?.hardFailure === true || launchState === 'failed_to_start');
|
||||||
const base: PersistedTeamLaunchMemberState = {
|
const base: PersistedTeamLaunchMemberState = {
|
||||||
name: params.member.name.trim(),
|
name: params.member.name.trim(),
|
||||||
providerId,
|
providerId,
|
||||||
|
|
@ -272,7 +295,7 @@ function createPrimaryLaneMemberState(params: {
|
||||||
: undefined,
|
: undefined,
|
||||||
launchState,
|
launchState,
|
||||||
agentToolAccepted: runtime?.agentToolAccepted === true,
|
agentToolAccepted: runtime?.agentToolAccepted === true,
|
||||||
runtimeAlive: strongRuntimeAlive,
|
runtimeAlive,
|
||||||
bootstrapConfirmed: runtime?.bootstrapConfirmed === true,
|
bootstrapConfirmed: runtime?.bootstrapConfirmed === true,
|
||||||
hardFailure,
|
hardFailure,
|
||||||
hardFailureReason: hardFailure ? (runtime?.hardFailureReason ?? runtime?.error) : undefined,
|
hardFailureReason: hardFailure ? (runtime?.hardFailureReason ?? runtime?.error) : undefined,
|
||||||
|
|
@ -285,7 +308,7 @@ function createPrimaryLaneMemberState(params: {
|
||||||
firstSpawnAcceptedAt: runtime?.firstSpawnAcceptedAt,
|
firstSpawnAcceptedAt: runtime?.firstSpawnAcceptedAt,
|
||||||
lastHeartbeatAt: runtime?.lastHeartbeatAt,
|
lastHeartbeatAt: runtime?.lastHeartbeatAt,
|
||||||
runtimeLastSeenAt: runtime?.livenessLastCheckedAt,
|
runtimeLastSeenAt: runtime?.livenessLastCheckedAt,
|
||||||
lastRuntimeAliveAt: preservesStrongRuntimeAlive(runtime ?? {}) ? params.updatedAt : undefined,
|
lastRuntimeAliveAt: runtimeAlive ? params.updatedAt : undefined,
|
||||||
lastEvaluatedAt: runtime?.updatedAt ?? params.updatedAt,
|
lastEvaluatedAt: runtime?.updatedAt ?? params.updatedAt,
|
||||||
sources,
|
sources,
|
||||||
diagnostics: undefined,
|
diagnostics: undefined,
|
||||||
|
|
@ -301,16 +324,22 @@ function createSecondaryLaneMemberState(
|
||||||
normalizeOptionalTeamProviderId(params.member.providerId) ?? params.leadDefaults.providerId;
|
normalizeOptionalTeamProviderId(params.member.providerId) ?? params.leadDefaults.providerId;
|
||||||
const evidence = params.evidence;
|
const evidence = params.evidence;
|
||||||
const strongRuntimeAlive = preservesStrongRuntimeAlive(evidence ?? {});
|
const strongRuntimeAlive = preservesStrongRuntimeAlive(evidence ?? {});
|
||||||
const launchState =
|
const healBootstrapConfirmedProvisionedButNotAlive =
|
||||||
evidence?.launchState ??
|
canHealBootstrapConfirmedProvisionedButNotAliveFailure(evidence ?? undefined);
|
||||||
deriveMemberLaunchState({
|
const runtimeAlive = healBootstrapConfirmedProvisionedButNotAlive || strongRuntimeAlive;
|
||||||
hardFailure: evidence?.hardFailure,
|
const launchState = healBootstrapConfirmedProvisionedButNotAlive
|
||||||
bootstrapConfirmed: evidence?.bootstrapConfirmed,
|
? 'confirmed_alive'
|
||||||
runtimeAlive: strongRuntimeAlive,
|
: (evidence?.launchState ??
|
||||||
agentToolAccepted: evidence?.agentToolAccepted,
|
deriveMemberLaunchState({
|
||||||
pendingPermissionRequestIds: evidence?.pendingPermissionRequestIds,
|
hardFailure: evidence?.hardFailure,
|
||||||
});
|
bootstrapConfirmed: evidence?.bootstrapConfirmed,
|
||||||
const hardFailure = evidence?.hardFailure === true || launchState === 'failed_to_start';
|
runtimeAlive: strongRuntimeAlive,
|
||||||
|
agentToolAccepted: evidence?.agentToolAccepted,
|
||||||
|
pendingPermissionRequestIds: evidence?.pendingPermissionRequestIds,
|
||||||
|
}));
|
||||||
|
const hardFailure =
|
||||||
|
!healBootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
(evidence?.hardFailure === true || launchState === 'failed_to_start');
|
||||||
const hardFailureReason = hardFailure ? evidence?.hardFailureReason : undefined;
|
const hardFailureReason = hardFailure ? evidence?.hardFailureReason : undefined;
|
||||||
const firstSpawnAcceptedAt = evidence
|
const firstSpawnAcceptedAt = evidence
|
||||||
? resolveOpenCodeSecondaryFirstSpawnAcceptedAt(evidence, params.updatedAt)
|
? resolveOpenCodeSecondaryFirstSpawnAcceptedAt(evidence, params.updatedAt)
|
||||||
|
|
@ -340,7 +369,7 @@ function createSecondaryLaneMemberState(
|
||||||
laneOwnerProviderId: providerId,
|
laneOwnerProviderId: providerId,
|
||||||
launchState,
|
launchState,
|
||||||
agentToolAccepted: evidence?.agentToolAccepted === true,
|
agentToolAccepted: evidence?.agentToolAccepted === true,
|
||||||
runtimeAlive: strongRuntimeAlive,
|
runtimeAlive,
|
||||||
bootstrapConfirmed: evidence?.bootstrapConfirmed === true,
|
bootstrapConfirmed: evidence?.bootstrapConfirmed === true,
|
||||||
hardFailure,
|
hardFailure,
|
||||||
hardFailureReason,
|
hardFailureReason,
|
||||||
|
|
@ -373,7 +402,7 @@ function createSecondaryLaneMemberState(
|
||||||
firstSpawnAcceptedAt,
|
firstSpawnAcceptedAt,
|
||||||
lastHeartbeatAt: evidence?.bootstrapConfirmed ? params.updatedAt : undefined,
|
lastHeartbeatAt: evidence?.bootstrapConfirmed ? params.updatedAt : undefined,
|
||||||
runtimeLastSeenAt: strongRuntimeAlive ? params.updatedAt : undefined,
|
runtimeLastSeenAt: strongRuntimeAlive ? params.updatedAt : undefined,
|
||||||
lastRuntimeAliveAt: strongRuntimeAlive ? params.updatedAt : undefined,
|
lastRuntimeAliveAt: runtimeAlive ? params.updatedAt : undefined,
|
||||||
lastEvaluatedAt: params.updatedAt,
|
lastEvaluatedAt: params.updatedAt,
|
||||||
sources: strongRuntimeAlive
|
sources: strongRuntimeAlive
|
||||||
? {
|
? {
|
||||||
|
|
@ -412,7 +441,10 @@ function summarizeMembers(
|
||||||
pendingCount += 1;
|
pendingCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (entry.launchState === 'confirmed_alive') {
|
if (
|
||||||
|
entry.launchState === 'confirmed_alive' ||
|
||||||
|
canHealBootstrapConfirmedProvisionedButNotAliveFailure(entry)
|
||||||
|
) {
|
||||||
confirmedCount += 1;
|
confirmedCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
import { isMixedOpenCodeSideLanePlan, planTeamRuntimeLanes } from '@features/team-runtime-lanes';
|
import { isMixedOpenCodeSideLanePlan, planTeamRuntimeLanes } from '@features/team-runtime-lanes';
|
||||||
|
import {
|
||||||
|
hasBootstrapConfirmationProofForLaunchFailure,
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
isProvisionedButNotAliveLaunchFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||||
|
|
||||||
|
import { isBootstrapMemberEvidenceCurrentForMember } from './provisioning/TeamProvisioningOpenCodeRuntimeEvidencePolicy';
|
||||||
import { shouldIgnoreTerminalBootstrapOnlyPendingSnapshot } from './TeamBootstrapStateReader';
|
import { shouldIgnoreTerminalBootstrapOnlyPendingSnapshot } from './TeamBootstrapStateReader';
|
||||||
import { hasMixedPersistedLaunchMetadata } from './TeamLaunchStateEvaluator';
|
import {
|
||||||
|
deriveTeamLaunchAggregateState,
|
||||||
|
hasMixedPersistedLaunchMetadata,
|
||||||
|
summarizePersistedLaunchMembers,
|
||||||
|
} from './TeamLaunchStateEvaluator';
|
||||||
|
|
||||||
import type { PersistedTeamLaunchSnapshot, TeamProviderId, TeamSummary } from '@shared/types';
|
import type {
|
||||||
|
PersistedTeamLaunchMemberState,
|
||||||
|
PersistedTeamLaunchSnapshot,
|
||||||
|
PersistedTeamLaunchSummary,
|
||||||
|
TeamProviderId,
|
||||||
|
TeamSummary,
|
||||||
|
} from '@shared/types';
|
||||||
|
|
||||||
export const TEAM_LAUNCH_SUMMARY_FILE = 'launch-summary.json';
|
export const TEAM_LAUNCH_SUMMARY_FILE = 'launch-summary.json';
|
||||||
const STALE_PENDING_SUMMARY_GRACE_MS = 5 * 60 * 1000;
|
const STALE_PENDING_SUMMARY_GRACE_MS = 5 * 60 * 1000;
|
||||||
|
|
@ -41,6 +57,71 @@ function getPersistedLaunchMemberNames(snapshot: PersistedTeamLaunchSnapshot): s
|
||||||
return Array.from(new Set([...snapshot.expectedMembers, ...Object.keys(snapshot.members)]));
|
return Array.from(new Set([...snapshot.expectedMembers, ...Object.keys(snapshot.members)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasBootstrapConfirmationProof(
|
||||||
|
member: PersistedTeamLaunchMemberState,
|
||||||
|
bootstrapMember: PersistedTeamLaunchMemberState | undefined
|
||||||
|
): boolean {
|
||||||
|
if (hasBootstrapConfirmationProofForLaunchFailure(member)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
bootstrapMember != null &&
|
||||||
|
hasBootstrapConfirmationProofForLaunchFailure(bootstrapMember) &&
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(member, bootstrapMember, 'confirmation')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldProjectProvisionedButNotAliveAsConfirmed(params: {
|
||||||
|
member: PersistedTeamLaunchMemberState | undefined;
|
||||||
|
bootstrapMember?: PersistedTeamLaunchMemberState;
|
||||||
|
}): params is { member: PersistedTeamLaunchMemberState } {
|
||||||
|
const member = params.member;
|
||||||
|
if (member?.launchState !== 'failed_to_start' || member.hardFailure !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(member) ||
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(params.bootstrapMember)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
isProvisionedButNotAliveLaunchFailure(member) &&
|
||||||
|
hasBootstrapConfirmationProof(member, params.bootstrapMember)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProjectedMembersForSummary(
|
||||||
|
snapshot: PersistedTeamLaunchSnapshot,
|
||||||
|
bootstrapSnapshot?: PersistedTeamLaunchSnapshot | null
|
||||||
|
): Record<string, PersistedTeamLaunchMemberState> | null {
|
||||||
|
let changed = false;
|
||||||
|
const projectedMembers: Record<string, PersistedTeamLaunchMemberState> = {};
|
||||||
|
for (const [memberName, member] of Object.entries(snapshot.members)) {
|
||||||
|
if (
|
||||||
|
shouldProjectProvisionedButNotAliveAsConfirmed({
|
||||||
|
member,
|
||||||
|
bootstrapMember: bootstrapSnapshot?.members[memberName],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
changed = true;
|
||||||
|
projectedMembers[memberName] = {
|
||||||
|
...member,
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
hardFailureReason: undefined,
|
||||||
|
runtimeDiagnostic: undefined,
|
||||||
|
runtimeDiagnosticSeverity: undefined,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
projectedMembers[memberName] = member;
|
||||||
|
}
|
||||||
|
return changed ? projectedMembers : null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeIsoDate(value: unknown): string | null {
|
function normalizeIsoDate(value: unknown): string | null {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -57,42 +138,47 @@ function toMillis(value: string | undefined | null): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLaunchStateSummary(
|
export function createLaunchStateSummary(
|
||||||
snapshot: PersistedTeamLaunchSnapshot
|
snapshot: PersistedTeamLaunchSnapshot,
|
||||||
|
options: { bootstrapSnapshot?: PersistedTeamLaunchSnapshot | null } = {}
|
||||||
): LaunchStateSummary {
|
): LaunchStateSummary {
|
||||||
const persistedMemberNames = getPersistedLaunchMemberNames(snapshot);
|
const persistedMemberNames = getPersistedLaunchMemberNames(snapshot);
|
||||||
|
const projectedMembers = buildProjectedMembersForSummary(snapshot, options.bootstrapSnapshot);
|
||||||
|
const members = projectedMembers ?? snapshot.members;
|
||||||
|
const summary = projectedMembers
|
||||||
|
? summarizePersistedLaunchMembers(snapshot.expectedMembers, projectedMembers)
|
||||||
|
: snapshot.summary;
|
||||||
|
const teamLaunchState = projectedMembers
|
||||||
|
? deriveTeamLaunchAggregateState(summary)
|
||||||
|
: snapshot.teamLaunchState;
|
||||||
const missingMembers = persistedMemberNames.filter((name) => {
|
const missingMembers = persistedMemberNames.filter((name) => {
|
||||||
const member = snapshot.members[name];
|
const member = members[name];
|
||||||
return member?.launchState === 'failed_to_start';
|
return member?.launchState === 'failed_to_start';
|
||||||
});
|
});
|
||||||
const skippedMembers = persistedMemberNames.filter((name) => {
|
const skippedMembers = persistedMemberNames.filter((name) => {
|
||||||
const member = snapshot.members[name];
|
const member = members[name];
|
||||||
return member?.launchState === 'skipped_for_launch' || member?.skippedForLaunch === true;
|
return member?.launchState === 'skipped_for_launch' || member?.skippedForLaunch === true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(snapshot.teamLaunchState === 'partial_failure'
|
...(teamLaunchState === 'partial_failure' ? { partialLaunchFailure: true as const } : {}),
|
||||||
? { partialLaunchFailure: true as const }
|
|
||||||
: {}),
|
|
||||||
...(persistedMemberNames.length > 0
|
...(persistedMemberNames.length > 0
|
||||||
? { expectedMemberCount: persistedMemberNames.length }
|
? { expectedMemberCount: persistedMemberNames.length }
|
||||||
: {}),
|
: {}),
|
||||||
...(snapshot.summary.confirmedCount > 0
|
...(summary.confirmedCount > 0 ? { confirmedMemberCount: summary.confirmedCount } : {}),
|
||||||
? { confirmedMemberCount: snapshot.summary.confirmedCount }
|
|
||||||
: {}),
|
|
||||||
...(missingMembers.length > 0 ? { missingMembers } : {}),
|
...(missingMembers.length > 0 ? { missingMembers } : {}),
|
||||||
...(skippedMembers.length > 0 ? { skippedMembers } : {}),
|
...(skippedMembers.length > 0 ? { skippedMembers } : {}),
|
||||||
teamLaunchState: snapshot.teamLaunchState,
|
teamLaunchState,
|
||||||
launchUpdatedAt: snapshot.updatedAt,
|
launchUpdatedAt: snapshot.updatedAt,
|
||||||
confirmedCount: snapshot.summary.confirmedCount,
|
confirmedCount: summary.confirmedCount,
|
||||||
pendingCount: snapshot.summary.pendingCount,
|
pendingCount: summary.pendingCount,
|
||||||
failedCount: snapshot.summary.failedCount,
|
failedCount: summary.failedCount,
|
||||||
skippedCount: snapshot.summary.skippedCount,
|
skippedCount: summary.skippedCount,
|
||||||
runtimeAlivePendingCount: snapshot.summary.runtimeAlivePendingCount,
|
runtimeAlivePendingCount: summary.runtimeAlivePendingCount,
|
||||||
shellOnlyPendingCount: snapshot.summary.shellOnlyPendingCount,
|
shellOnlyPendingCount: summary.shellOnlyPendingCount,
|
||||||
runtimeProcessPendingCount: snapshot.summary.runtimeProcessPendingCount,
|
runtimeProcessPendingCount: summary.runtimeProcessPendingCount,
|
||||||
runtimeCandidatePendingCount: snapshot.summary.runtimeCandidatePendingCount,
|
runtimeCandidatePendingCount: summary.runtimeCandidatePendingCount,
|
||||||
noRuntimePendingCount: snapshot.summary.noRuntimePendingCount,
|
noRuntimePendingCount: summary.noRuntimePendingCount,
|
||||||
permissionPendingCount: snapshot.summary.permissionPendingCount,
|
permissionPendingCount: summary.permissionPendingCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,6 +328,83 @@ function shouldIgnoreStalePendingLaunchSnapshotSummary(
|
||||||
return Number.isFinite(updatedAtMs) && nowMs - updatedAtMs >= STALE_PENDING_SUMMARY_GRACE_MS;
|
return Number.isFinite(updatedAtMs) && nowMs - updatedAtMs >= STALE_PENDING_SUMMARY_GRACE_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reconcileSummaryProjectionWithBootstrap(
|
||||||
|
projection: PersistedTeamLaunchSummaryProjection,
|
||||||
|
bootstrapSnapshot: PersistedTeamLaunchSnapshot
|
||||||
|
): PersistedTeamLaunchSummaryProjection {
|
||||||
|
const missingMembers = projection.missingMembers ?? [];
|
||||||
|
if (missingMembers.length === 0) {
|
||||||
|
return projection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectionBoundary = projection.launchUpdatedAt ?? projection.updatedAt;
|
||||||
|
const healedMembers = missingMembers.filter((memberName) => {
|
||||||
|
const bootstrapMember = bootstrapSnapshot.members[memberName];
|
||||||
|
return (
|
||||||
|
bootstrapMember != null &&
|
||||||
|
hasBootstrapConfirmationProofForLaunchFailure(bootstrapMember) &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveRuntimeEvidence(bootstrapMember) &&
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(
|
||||||
|
{ firstSpawnAcceptedAt: projectionBoundary, lastEvaluatedAt: projectionBoundary },
|
||||||
|
bootstrapMember,
|
||||||
|
'confirmation'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (healedMembers.length === 0) {
|
||||||
|
return projection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const healedMemberNames = new Set(healedMembers);
|
||||||
|
const nextMissingMembers = missingMembers.filter(
|
||||||
|
(memberName) => !healedMemberNames.has(memberName)
|
||||||
|
);
|
||||||
|
const summary: PersistedTeamLaunchSummary = {
|
||||||
|
confirmedCount:
|
||||||
|
(projection.confirmedCount ?? projection.confirmedMemberCount ?? 0) + healedMembers.length,
|
||||||
|
pendingCount: projection.pendingCount ?? 0,
|
||||||
|
failedCount: Math.max(
|
||||||
|
0,
|
||||||
|
(projection.failedCount ?? missingMembers.length) - healedMembers.length
|
||||||
|
),
|
||||||
|
skippedCount: projection.skippedCount ?? projection.skippedMembers?.length ?? 0,
|
||||||
|
runtimeAlivePendingCount: projection.runtimeAlivePendingCount ?? 0,
|
||||||
|
shellOnlyPendingCount: projection.shellOnlyPendingCount,
|
||||||
|
runtimeProcessPendingCount: projection.runtimeProcessPendingCount,
|
||||||
|
runtimeCandidatePendingCount: projection.runtimeCandidatePendingCount,
|
||||||
|
noRuntimePendingCount: projection.noRuntimePendingCount,
|
||||||
|
permissionPendingCount: projection.permissionPendingCount,
|
||||||
|
};
|
||||||
|
const teamLaunchState = deriveTeamLaunchAggregateState(summary);
|
||||||
|
|
||||||
|
const reconciled: PersistedTeamLaunchSummaryProjection = {
|
||||||
|
...projection,
|
||||||
|
teamLaunchState,
|
||||||
|
confirmedMemberCount: summary.confirmedCount,
|
||||||
|
confirmedCount: summary.confirmedCount,
|
||||||
|
pendingCount: summary.pendingCount,
|
||||||
|
failedCount: summary.failedCount,
|
||||||
|
skippedCount: summary.skippedCount,
|
||||||
|
runtimeAlivePendingCount: summary.runtimeAlivePendingCount,
|
||||||
|
shellOnlyPendingCount: summary.shellOnlyPendingCount,
|
||||||
|
runtimeProcessPendingCount: summary.runtimeProcessPendingCount,
|
||||||
|
runtimeCandidatePendingCount: summary.runtimeCandidatePendingCount,
|
||||||
|
noRuntimePendingCount: summary.noRuntimePendingCount,
|
||||||
|
permissionPendingCount: summary.permissionPendingCount,
|
||||||
|
};
|
||||||
|
if (nextMissingMembers.length > 0) {
|
||||||
|
reconciled.missingMembers = nextMissingMembers;
|
||||||
|
} else {
|
||||||
|
delete reconciled.missingMembers;
|
||||||
|
}
|
||||||
|
if (teamLaunchState === 'partial_failure') {
|
||||||
|
reconciled.partialLaunchFailure = true;
|
||||||
|
} else {
|
||||||
|
delete reconciled.partialLaunchFailure;
|
||||||
|
}
|
||||||
|
return reconciled;
|
||||||
|
}
|
||||||
|
|
||||||
export function choosePreferredLaunchStateSummary(params: {
|
export function choosePreferredLaunchStateSummary(params: {
|
||||||
bootstrapSnapshot?: PersistedTeamLaunchSnapshot | null;
|
bootstrapSnapshot?: PersistedTeamLaunchSnapshot | null;
|
||||||
launchSnapshot?: PersistedTeamLaunchSnapshot | null;
|
launchSnapshot?: PersistedTeamLaunchSnapshot | null;
|
||||||
|
|
@ -252,7 +415,9 @@ export function choosePreferredLaunchStateSummary(params: {
|
||||||
? null
|
? null
|
||||||
: (params.launchSnapshot ?? null);
|
: (params.launchSnapshot ?? null);
|
||||||
if (launchSnapshot) {
|
if (launchSnapshot) {
|
||||||
return createLaunchStateSummary(launchSnapshot);
|
return createLaunchStateSummary(launchSnapshot, {
|
||||||
|
bootstrapSnapshot: params.bootstrapSnapshot ?? null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const bootstrapSnapshot = params.bootstrapSnapshot ?? null;
|
const bootstrapSnapshot = params.bootstrapSnapshot ?? null;
|
||||||
|
|
@ -271,22 +436,28 @@ export function choosePreferredLaunchStateSummary(params: {
|
||||||
return createLaunchStateSummary(bootstrapSnapshot);
|
return createLaunchStateSummary(bootstrapSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reconciledProjection = reconcileSummaryProjectionWithBootstrap(
|
||||||
|
projection,
|
||||||
|
bootstrapSnapshot
|
||||||
|
);
|
||||||
const bootstrapMixedAware = hasMixedPersistedLaunchMetadata(bootstrapSnapshot);
|
const bootstrapMixedAware = hasMixedPersistedLaunchMetadata(bootstrapSnapshot);
|
||||||
const projectionMixedAware = projection.mixedAware === true;
|
const projectionMixedAware = reconciledProjection.mixedAware === true;
|
||||||
if (projectionMixedAware !== bootstrapMixedAware) {
|
if (projectionMixedAware !== bootstrapMixedAware) {
|
||||||
return projectionMixedAware ? projection : createLaunchStateSummary(bootstrapSnapshot);
|
return projectionMixedAware
|
||||||
|
? reconciledProjection
|
||||||
|
: createLaunchStateSummary(bootstrapSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectionUpdatedAtMs = toMillis(projection.updatedAt);
|
const projectionUpdatedAtMs = toMillis(reconciledProjection.updatedAt);
|
||||||
const bootstrapUpdatedAtMs = toMillis(bootstrapSnapshot.updatedAt);
|
const bootstrapUpdatedAtMs = toMillis(bootstrapSnapshot.updatedAt);
|
||||||
if (!Number.isFinite(bootstrapUpdatedAtMs)) {
|
if (!Number.isFinite(bootstrapUpdatedAtMs)) {
|
||||||
return projection;
|
return reconciledProjection;
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(projectionUpdatedAtMs)) {
|
if (!Number.isFinite(projectionUpdatedAtMs)) {
|
||||||
return createLaunchStateSummary(bootstrapSnapshot);
|
return createLaunchStateSummary(bootstrapSnapshot);
|
||||||
}
|
}
|
||||||
return projectionUpdatedAtMs >= bootstrapUpdatedAtMs
|
return projectionUpdatedAtMs >= bootstrapUpdatedAtMs
|
||||||
? projection
|
? reconciledProjection
|
||||||
: createLaunchStateSummary(bootstrapSnapshot);
|
: createLaunchStateSummary(bootstrapSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ import {
|
||||||
isTeamInternalControlMessageText,
|
isTeamInternalControlMessageText,
|
||||||
stripExactInternalControlEchoPrefix,
|
stripExactInternalControlEchoPrefix,
|
||||||
} from '@shared/utils/teamInternalControlMessages';
|
} from '@shared/utils/teamInternalControlMessages';
|
||||||
|
import { hasUnsafeProvisionedButNotAliveRuntimeEvidence } from '@shared/utils/teamLaunchFailureReason';
|
||||||
import {
|
import {
|
||||||
parseAllTeammateMessages,
|
parseAllTeammateMessages,
|
||||||
type ParsedTeammateContent,
|
type ParsedTeammateContent,
|
||||||
|
|
@ -231,6 +232,7 @@ import {
|
||||||
isAutoClearableLaunchFailureReason,
|
isAutoClearableLaunchFailureReason,
|
||||||
isCliProvisionedButNotAliveFailureReason,
|
isCliProvisionedButNotAliveFailureReason,
|
||||||
isNeverSpawnedDuringLaunchReason,
|
isNeverSpawnedDuringLaunchReason,
|
||||||
|
isProvisionedButNotAliveFailureReason,
|
||||||
} from './provisioning/TeamProvisioningLaunchFailurePolicy';
|
} from './provisioning/TeamProvisioningLaunchFailurePolicy';
|
||||||
import {
|
import {
|
||||||
isOpenCodeOverlayMemberRemoved,
|
isOpenCodeOverlayMemberRemoved,
|
||||||
|
|
@ -959,9 +961,16 @@ function isConfirmedBootstrapStaleRuntimeDiagnostic(reason?: string): boolean {
|
||||||
return text === 'persisted runtime pid is not alive';
|
return text === 'persisted runtime pid is not alive';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBootstrapProofClearableLaunchFailureReason(reason?: string): boolean {
|
||||||
|
return (
|
||||||
|
isAutoClearableLaunchFailureReason(reason) || isProvisionedButNotAliveFailureReason(reason)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(reason?: string): boolean {
|
function shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(reason?: string): boolean {
|
||||||
return (
|
return (
|
||||||
isAutoClearableLaunchFailureReason(reason) || isConfirmedBootstrapStaleRuntimeDiagnostic(reason)
|
isBootstrapProofClearableLaunchFailureReason(reason) ||
|
||||||
|
isConfirmedBootstrapStaleRuntimeDiagnostic(reason)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13666,13 +13675,14 @@ export class TeamProvisioningService {
|
||||||
status: 'online',
|
status: 'online',
|
||||||
updatedAt,
|
updatedAt,
|
||||||
agentToolAccepted: true,
|
agentToolAccepted: true,
|
||||||
runtimeAlive: source === 'runtime-proof' ? true : prev.runtimeAlive === true,
|
runtimeAlive: source === 'runtime-proof' ? true : prev.runtimeAlive,
|
||||||
bootstrapConfirmed: true,
|
bootstrapConfirmed: true,
|
||||||
hardFailure: false,
|
hardFailure: false,
|
||||||
bootstrapStalled: undefined,
|
bootstrapStalled: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
hardFailureReason: undefined,
|
hardFailureReason: undefined,
|
||||||
livenessSource: prev.livenessSource ?? 'process',
|
livenessSource:
|
||||||
|
source === 'runtime-proof' ? (prev.livenessSource ?? 'process') : prev.livenessSource,
|
||||||
firstSpawnAcceptedAt: prev.firstSpawnAcceptedAt ?? observedAt,
|
firstSpawnAcceptedAt: prev.firstSpawnAcceptedAt ?? observedAt,
|
||||||
lastHeartbeatAt: isMemberSpawnHeartbeatTimestampNewer(prev.lastHeartbeatAt, observedAt)
|
lastHeartbeatAt: isMemberSpawnHeartbeatTimestampNewer(prev.lastHeartbeatAt, observedAt)
|
||||||
? observedAt
|
? observedAt
|
||||||
|
|
@ -17128,7 +17138,7 @@ export class TeamProvisioningService {
|
||||||
const canClearFailedBootstrap =
|
const canClearFailedBootstrap =
|
||||||
current?.launchState === 'failed_to_start' &&
|
current?.launchState === 'failed_to_start' &&
|
||||||
current.agentToolAccepted === true &&
|
current.agentToolAccepted === true &&
|
||||||
isAutoClearableLaunchFailureReason(failureReason);
|
isBootstrapProofClearableLaunchFailureReason(failureReason);
|
||||||
if (
|
if (
|
||||||
!current ||
|
!current ||
|
||||||
(current.launchState === 'failed_to_start' && !canClearFailedBootstrap) ||
|
(current.launchState === 'failed_to_start' && !canClearFailedBootstrap) ||
|
||||||
|
|
@ -24243,6 +24253,39 @@ export class TeamProvisioningService {
|
||||||
current.bootstrapConfirmed === true || current.launchState === 'confirmed_alive';
|
current.bootstrapConfirmed === true || current.launchState === 'confirmed_alive';
|
||||||
const shouldSuppressWeakRuntimeMetadataForConfirmedBootstrap =
|
const shouldSuppressWeakRuntimeMetadataForConfirmedBootstrap =
|
||||||
hasConfirmedBootstrap && !hasStrongEvidence;
|
hasConfirmedBootstrap && !hasStrongEvidence;
|
||||||
|
const failureReason = current.hardFailureReason ?? current.error ?? current.runtimeDiagnostic;
|
||||||
|
const bootstrapProofClearableFailure =
|
||||||
|
isBootstrapProofClearableLaunchFailureReason(failureReason);
|
||||||
|
const metadataRuntimeDiagnosticForUnsafe = buildRuntimeDiagnosticForSpawn(metadata);
|
||||||
|
const unsafeRuntimeDiagnosticEvidence =
|
||||||
|
metadataRuntimeDiagnosticForUnsafe &&
|
||||||
|
current.runtimeDiagnostic &&
|
||||||
|
metadataRuntimeDiagnosticForUnsafe !== current.runtimeDiagnostic
|
||||||
|
? `${metadataRuntimeDiagnosticForUnsafe}; ${current.runtimeDiagnostic}`
|
||||||
|
: (metadataRuntimeDiagnosticForUnsafe ?? current.runtimeDiagnostic);
|
||||||
|
const hasUnsafeProvisionedButNotAliveFailure =
|
||||||
|
isProvisionedButNotAliveFailureReason(failureReason) &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
...current,
|
||||||
|
runtimeDiagnostic: unsafeRuntimeDiagnosticEvidence,
|
||||||
|
runtimeDiagnosticSeverity:
|
||||||
|
metadata.runtimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity,
|
||||||
|
livenessKind: metadata.livenessKind ?? current.livenessKind,
|
||||||
|
});
|
||||||
|
const shouldPreserveConfirmedBootstrapRuntimeError =
|
||||||
|
hasConfirmedBootstrap &&
|
||||||
|
metadata.alive === false &&
|
||||||
|
metadata.runtimeDiagnosticSeverity === 'error';
|
||||||
|
const shouldPreserveUnsafeMetadataLivenessKind =
|
||||||
|
hasUnsafeProvisionedButNotAliveFailure &&
|
||||||
|
(metadata.livenessKind === 'not_found' ||
|
||||||
|
metadata.livenessKind === 'shell_only' ||
|
||||||
|
metadata.livenessKind === 'runtime_process_candidate' ||
|
||||||
|
((metadata.livenessKind === 'registered_only' ||
|
||||||
|
metadata.livenessKind === 'stale_metadata') &&
|
||||||
|
(metadata.runtimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity) !== 'error' &&
|
||||||
|
!mentionsProcessTableUnavailable(unsafeRuntimeDiagnosticEvidence) &&
|
||||||
|
!mentionsProcessTableUnavailable(failureReason)));
|
||||||
let runtimeDiagnostic: string | undefined;
|
let runtimeDiagnostic: string | undefined;
|
||||||
let runtimeDiagnosticSeverity: TeamAgentRuntimeDiagnosticSeverity | undefined;
|
let runtimeDiagnosticSeverity: TeamAgentRuntimeDiagnosticSeverity | undefined;
|
||||||
if (shouldPreserveProcessBootstrapTransportDiagnostic) {
|
if (shouldPreserveProcessBootstrapTransportDiagnostic) {
|
||||||
|
|
@ -24256,7 +24299,7 @@ export class TeamProvisioningService {
|
||||||
runtimeDiagnostic = current.runtimeDiagnostic;
|
runtimeDiagnostic = current.runtimeDiagnostic;
|
||||||
runtimeDiagnosticSeverity = current.runtimeDiagnosticSeverity;
|
runtimeDiagnosticSeverity = current.runtimeDiagnosticSeverity;
|
||||||
} else {
|
} else {
|
||||||
const metadataRuntimeDiagnostic = buildRuntimeDiagnosticForSpawn(metadata);
|
const metadataRuntimeDiagnostic = metadataRuntimeDiagnosticForUnsafe;
|
||||||
if (
|
if (
|
||||||
metadataRuntimeDiagnostic &&
|
metadataRuntimeDiagnostic &&
|
||||||
!shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(metadataRuntimeDiagnostic)
|
!shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(metadataRuntimeDiagnostic)
|
||||||
|
|
@ -24271,7 +24314,9 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
const metadataLivenessKind = hasConfirmedBootstrap
|
const metadataLivenessKind = hasConfirmedBootstrap
|
||||||
? metadata.livenessKind === 'runtime_process' ||
|
? metadata.livenessKind === 'runtime_process' ||
|
||||||
metadata.livenessKind === 'confirmed_bootstrap'
|
metadata.livenessKind === 'confirmed_bootstrap' ||
|
||||||
|
shouldPreserveConfirmedBootstrapRuntimeError ||
|
||||||
|
shouldPreserveUnsafeMetadataLivenessKind
|
||||||
? metadata.livenessKind
|
? metadata.livenessKind
|
||||||
: current.livenessKind === 'stale_metadata' || current.livenessKind === 'registered_only'
|
: current.livenessKind === 'stale_metadata' || current.livenessKind === 'registered_only'
|
||||||
? 'confirmed_bootstrap'
|
? 'confirmed_bootstrap'
|
||||||
|
|
@ -24291,7 +24336,6 @@ export class TeamProvisioningService {
|
||||||
: {}),
|
: {}),
|
||||||
livenessLastCheckedAt: nowIso(),
|
livenessLastCheckedAt: nowIso(),
|
||||||
};
|
};
|
||||||
const failureReason = current.hardFailureReason ?? current.error;
|
|
||||||
const hasWeakEvidence =
|
const hasWeakEvidence =
|
||||||
metadata.livenessKind != null && !hasStrongEvidence && current.bootstrapConfirmed !== true;
|
metadata.livenessKind != null && !hasStrongEvidence && current.bootstrapConfirmed !== true;
|
||||||
if (
|
if (
|
||||||
|
|
@ -24335,7 +24379,8 @@ export class TeamProvisioningService {
|
||||||
if (
|
if (
|
||||||
hasStrongEvidence &&
|
hasStrongEvidence &&
|
||||||
current.launchState === 'failed_to_start' &&
|
current.launchState === 'failed_to_start' &&
|
||||||
isAutoClearableLaunchFailureReason(failureReason)
|
bootstrapProofClearableFailure &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveFailure
|
||||||
) {
|
) {
|
||||||
nextEntry.status = 'online';
|
nextEntry.status = 'online';
|
||||||
nextEntry.agentToolAccepted = true;
|
nextEntry.agentToolAccepted = true;
|
||||||
|
|
@ -24346,7 +24391,34 @@ export class TeamProvisioningService {
|
||||||
nextEntry.livenessSource = current.bootstrapConfirmed ? current.livenessSource : 'process';
|
nextEntry.livenessSource = current.bootstrapConfirmed ? current.livenessSource : 'process';
|
||||||
nextEntry.launchState = deriveMemberLaunchState(nextEntry);
|
nextEntry.launchState = deriveMemberLaunchState(nextEntry);
|
||||||
}
|
}
|
||||||
if (hasWeakEvidence) {
|
if (
|
||||||
|
hasConfirmedBootstrap &&
|
||||||
|
current.hardFailure === true &&
|
||||||
|
bootstrapProofClearableFailure &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveFailure
|
||||||
|
) {
|
||||||
|
nextEntry.status = 'online';
|
||||||
|
nextEntry.agentToolAccepted = true;
|
||||||
|
nextEntry.runtimeAlive = true;
|
||||||
|
nextEntry.bootstrapConfirmed = true;
|
||||||
|
nextEntry.hardFailure = false;
|
||||||
|
nextEntry.hardFailureReason = undefined;
|
||||||
|
nextEntry.error = undefined;
|
||||||
|
nextEntry.bootstrapStalled = undefined;
|
||||||
|
nextEntry.launchState = deriveMemberLaunchState(nextEntry);
|
||||||
|
}
|
||||||
|
const healedConfirmedBootstrapFailure =
|
||||||
|
hasConfirmedBootstrap &&
|
||||||
|
current.hardFailure === true &&
|
||||||
|
bootstrapProofClearableFailure &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveFailure;
|
||||||
|
if (shouldPreserveConfirmedBootstrapRuntimeError) {
|
||||||
|
nextEntry.runtimeAlive = false;
|
||||||
|
if (nextEntry.livenessSource === 'process') {
|
||||||
|
nextEntry.livenessSource = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasWeakEvidence && !healedConfirmedBootstrapFailure) {
|
||||||
nextEntry.runtimeAlive = false;
|
nextEntry.runtimeAlive = false;
|
||||||
if (nextEntry.livenessSource === 'process') {
|
if (nextEntry.livenessSource === 'process') {
|
||||||
nextEntry.livenessSource = undefined;
|
nextEntry.livenessSource = undefined;
|
||||||
|
|
@ -26714,10 +26786,17 @@ export class TeamProvisioningService {
|
||||||
if (current.launchState === 'skipped_for_launch' || current.skippedForLaunch === true) {
|
if (current.launchState === 'skipped_for_launch' || current.skippedForLaunch === true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const failureReason = current.hardFailureReason ?? current.error;
|
const failureReason = current.hardFailureReason ?? current.error ?? current.runtimeDiagnostic;
|
||||||
|
const provisionedButNotAliveFailure = isProvisionedButNotAliveFailureReason(failureReason);
|
||||||
|
if (
|
||||||
|
provisionedButNotAliveFailure &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(current)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
current.launchState === 'failed_to_start' &&
|
current.launchState === 'failed_to_start' &&
|
||||||
!isAutoClearableLaunchFailureReason(failureReason)
|
!isBootstrapProofClearableLaunchFailureReason(failureReason)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -26827,12 +26906,19 @@ export class TeamProvisioningService {
|
||||||
: undefined;
|
: undefined;
|
||||||
const failureReason =
|
const failureReason =
|
||||||
current.hardFailureReason ?? persistedError ?? current.runtimeDiagnostic;
|
current.hardFailureReason ?? persistedError ?? current.runtimeDiagnostic;
|
||||||
|
const provisionedButNotAliveFailure = isProvisionedButNotAliveFailureReason(failureReason);
|
||||||
|
if (
|
||||||
|
provisionedButNotAliveFailure &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(current)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const hasFailure =
|
const hasFailure =
|
||||||
current.launchState === 'failed_to_start' ||
|
current.launchState === 'failed_to_start' ||
|
||||||
current.hardFailure === true ||
|
current.hardFailure === true ||
|
||||||
typeof current.hardFailureReason === 'string' ||
|
typeof current.hardFailureReason === 'string' ||
|
||||||
typeof persistedError === 'string';
|
typeof persistedError === 'string';
|
||||||
if (hasFailure && !isAutoClearableLaunchFailureReason(failureReason)) {
|
if (hasFailure && !isBootstrapProofClearableLaunchFailureReason(failureReason)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26845,14 +26931,21 @@ export class TeamProvisioningService {
|
||||||
...current,
|
...current,
|
||||||
launchState: 'confirmed_alive',
|
launchState: 'confirmed_alive',
|
||||||
agentToolAccepted: true,
|
agentToolAccepted: true,
|
||||||
runtimeAlive: current.runtimeAlive === true || bootstrapMember.runtimeAlive === true,
|
runtimeAlive:
|
||||||
|
current.runtimeAlive === true ||
|
||||||
|
bootstrapMember.runtimeAlive === true ||
|
||||||
|
provisionedButNotAliveFailure,
|
||||||
bootstrapConfirmed: true,
|
bootstrapConfirmed: true,
|
||||||
hardFailure: false,
|
hardFailure: false,
|
||||||
hardFailureReason: undefined,
|
hardFailureReason: undefined,
|
||||||
runtimeDiagnostic: isAutoClearableLaunchFailureReason(current.runtimeDiagnostic)
|
runtimeDiagnostic: shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(
|
||||||
|
current.runtimeDiagnostic
|
||||||
|
)
|
||||||
? undefined
|
? undefined
|
||||||
: current.runtimeDiagnostic,
|
: current.runtimeDiagnostic,
|
||||||
runtimeDiagnosticSeverity: isAutoClearableLaunchFailureReason(current.runtimeDiagnostic)
|
runtimeDiagnosticSeverity: shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(
|
||||||
|
current.runtimeDiagnostic
|
||||||
|
)
|
||||||
? undefined
|
? undefined
|
||||||
: current.runtimeDiagnosticSeverity,
|
: current.runtimeDiagnosticSeverity,
|
||||||
bootstrapStalled: undefined,
|
bootstrapStalled: undefined,
|
||||||
|
|
@ -28638,7 +28731,9 @@ export class TeamProvisioningService {
|
||||||
current.firstSpawnAcceptedAt != null ? Date.parse(current.firstSpawnAcceptedAt) : NaN;
|
current.firstSpawnAcceptedAt != null ? Date.parse(current.firstSpawnAcceptedAt) : NaN;
|
||||||
if (
|
if (
|
||||||
current.launchState !== 'failed_to_start' ||
|
current.launchState !== 'failed_to_start' ||
|
||||||
isAutoClearableLaunchFailureReason(current.hardFailureReason ?? current.runtimeDiagnostic)
|
isBootstrapProofClearableLaunchFailureReason(
|
||||||
|
current.hardFailureReason ?? current.runtimeDiagnostic
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
const runtimeProofObservedAt = await this.findBootstrapRuntimeProofObservedAt(
|
const runtimeProofObservedAt = await this.findBootstrapRuntimeProofObservedAt(
|
||||||
snapshot.teamName,
|
snapshot.teamName,
|
||||||
|
|
@ -29053,9 +29148,16 @@ export class TeamProvisioningService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const failureReason = current.hardFailureReason ?? current.runtimeDiagnostic;
|
const failureReason = current.hardFailureReason ?? current.runtimeDiagnostic;
|
||||||
|
const provisionedButNotAliveFailure = isProvisionedButNotAliveFailureReason(failureReason);
|
||||||
|
if (
|
||||||
|
provisionedButNotAliveFailure &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(current)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const canClearFailedBootstrap =
|
const canClearFailedBootstrap =
|
||||||
current.launchState !== 'failed_to_start' ||
|
current.launchState !== 'failed_to_start' ||
|
||||||
isAutoClearableLaunchFailureReason(failureReason);
|
isBootstrapProofClearableLaunchFailureReason(failureReason);
|
||||||
if (!canClearFailedBootstrap) {
|
if (!canClearFailedBootstrap) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -29083,7 +29185,9 @@ export class TeamProvisioningService {
|
||||||
...current,
|
...current,
|
||||||
agentToolAccepted: true,
|
agentToolAccepted: true,
|
||||||
bootstrapConfirmed: true,
|
bootstrapConfirmed: true,
|
||||||
runtimeAlive: runtimeProofObservedAt ? true : current.runtimeAlive === true,
|
runtimeAlive: runtimeProofObservedAt
|
||||||
|
? true
|
||||||
|
: current.runtimeAlive === true || provisionedButNotAliveFailure,
|
||||||
hardFailure: false,
|
hardFailure: false,
|
||||||
hardFailureReason: undefined,
|
hardFailureReason: undefined,
|
||||||
lastHeartbeatAt: current.lastHeartbeatAt ?? observedAt,
|
lastHeartbeatAt: current.lastHeartbeatAt ?? observedAt,
|
||||||
|
|
@ -29140,7 +29244,7 @@ export class TeamProvisioningService {
|
||||||
const failureReason = current.hardFailureReason ?? current.runtimeDiagnostic;
|
const failureReason = current.hardFailureReason ?? current.runtimeDiagnostic;
|
||||||
const hasAutoClearableFailure =
|
const hasAutoClearableFailure =
|
||||||
(current.launchState === 'failed_to_start' || current.hardFailure === true) &&
|
(current.launchState === 'failed_to_start' || current.hardFailure === true) &&
|
||||||
isAutoClearableLaunchFailureReason(failureReason);
|
isBootstrapProofClearableLaunchFailureReason(failureReason);
|
||||||
if (!currentConfirmed || hasAutoClearableFailure) {
|
if (!currentConfirmed || hasAutoClearableFailure) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -29537,12 +29641,58 @@ export class TeamProvisioningService {
|
||||||
const hadAutoClearableFailure = isAutoClearableLaunchFailureReason(initialFailureReason);
|
const hadAutoClearableFailure = isAutoClearableLaunchFailureReason(initialFailureReason);
|
||||||
const requiresConfirmedBootstrapToClearFailure =
|
const requiresConfirmedBootstrapToClearFailure =
|
||||||
isCliProvisionedButNotAliveFailureReason(initialFailureReason);
|
isCliProvisionedButNotAliveFailureReason(initialFailureReason);
|
||||||
|
const metadataRuntimeDiagnostic = runtimeMetadata?.[1].runtimeDiagnostic;
|
||||||
|
const metadataRuntimeDiagnosticSeverity = runtimeMetadata?.[1].runtimeDiagnosticSeverity;
|
||||||
|
const metadataLivenessKind = runtimeMetadata?.[1].livenessKind;
|
||||||
|
const refreshedRuntimeDiagnosticEvidence =
|
||||||
|
metadataRuntimeDiagnostic &&
|
||||||
|
current.runtimeDiagnostic &&
|
||||||
|
metadataRuntimeDiagnostic !== current.runtimeDiagnostic
|
||||||
|
? `${metadataRuntimeDiagnostic}; ${current.runtimeDiagnostic}`
|
||||||
|
: (metadataRuntimeDiagnostic ?? current.runtimeDiagnostic);
|
||||||
|
const hasUnsafeProvisionedButNotAliveFailure =
|
||||||
|
requiresConfirmedBootstrapToClearFailure &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
...current,
|
||||||
|
runtimeDiagnostic: refreshedRuntimeDiagnosticEvidence,
|
||||||
|
runtimeDiagnosticSeverity:
|
||||||
|
metadataRuntimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity,
|
||||||
|
livenessKind: metadataLivenessKind ?? current.livenessKind,
|
||||||
|
});
|
||||||
|
const shouldPreserveUnsafeMetadataLivenessKind =
|
||||||
|
hasUnsafeProvisionedButNotAliveFailure &&
|
||||||
|
(metadataLivenessKind === 'not_found' ||
|
||||||
|
metadataLivenessKind === 'shell_only' ||
|
||||||
|
metadataLivenessKind === 'runtime_process_candidate' ||
|
||||||
|
((metadataLivenessKind === 'registered_only' ||
|
||||||
|
metadataLivenessKind === 'stale_metadata') &&
|
||||||
|
(metadataRuntimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity) !== 'error' &&
|
||||||
|
!mentionsProcessTableUnavailable(refreshedRuntimeDiagnosticEvidence) &&
|
||||||
|
!mentionsProcessTableUnavailable(initialFailureReason)));
|
||||||
|
const nextLivenessKind = current.bootstrapConfirmed
|
||||||
|
? metadataLivenessKind === 'runtime_process' ||
|
||||||
|
metadataLivenessKind === 'confirmed_bootstrap' ||
|
||||||
|
shouldPreserveUnsafeMetadataLivenessKind
|
||||||
|
? metadataLivenessKind
|
||||||
|
: current.livenessKind === 'stale_metadata' || current.livenessKind === 'registered_only'
|
||||||
|
? 'confirmed_bootstrap'
|
||||||
|
: (current.livenessKind ?? 'confirmed_bootstrap')
|
||||||
|
: (metadataLivenessKind ?? current.livenessKind);
|
||||||
current.runtimeAlive = observedRuntimeAlive;
|
current.runtimeAlive = observedRuntimeAlive;
|
||||||
current.lastRuntimeAliveAt = observedRuntimeAlive ? now : current.lastRuntimeAliveAt;
|
current.lastRuntimeAliveAt = observedRuntimeAlive ? now : current.lastRuntimeAliveAt;
|
||||||
current.livenessKind = runtimeMetadata?.[1].livenessKind;
|
current.livenessKind = nextLivenessKind;
|
||||||
current.pidSource = runtimeMetadata?.[1].pidSource;
|
current.pidSource = runtimeMetadata?.[1].pidSource;
|
||||||
current.runtimeDiagnostic = runtimeMetadata?.[1].runtimeDiagnostic;
|
const shouldKeepUnsafeRuntimeDiagnostic =
|
||||||
current.runtimeDiagnosticSeverity = runtimeMetadata?.[1].runtimeDiagnosticSeverity;
|
hasUnsafeProvisionedButNotAliveFailure &&
|
||||||
|
(metadataRuntimeDiagnostic == null ||
|
||||||
|
(current.runtimeDiagnosticSeverity === 'error' &&
|
||||||
|
metadataRuntimeDiagnosticSeverity !== 'error'));
|
||||||
|
current.runtimeDiagnostic = shouldKeepUnsafeRuntimeDiagnostic
|
||||||
|
? current.runtimeDiagnostic
|
||||||
|
: metadataRuntimeDiagnostic;
|
||||||
|
current.runtimeDiagnosticSeverity = shouldKeepUnsafeRuntimeDiagnostic
|
||||||
|
? current.runtimeDiagnosticSeverity
|
||||||
|
: metadataRuntimeDiagnosticSeverity;
|
||||||
current.sources = {
|
current.sources = {
|
||||||
...(current.sources ?? {}),
|
...(current.sources ?? {}),
|
||||||
processAlive: observedRuntimeAlive || undefined,
|
processAlive: observedRuntimeAlive || undefined,
|
||||||
|
|
@ -29572,8 +29722,12 @@ export class TeamProvisioningService {
|
||||||
if (
|
if (
|
||||||
current.bootstrapConfirmed &&
|
current.bootstrapConfirmed &&
|
||||||
!isOpenCodeSecondaryLaneMember &&
|
!isOpenCodeSecondaryLaneMember &&
|
||||||
isAutoClearableLaunchFailureReason(current.hardFailureReason)
|
!hasUnsafeProvisionedButNotAliveFailure &&
|
||||||
|
isBootstrapProofClearableLaunchFailureReason(current.hardFailureReason)
|
||||||
) {
|
) {
|
||||||
|
if (isProvisionedButNotAliveFailureReason(current.hardFailureReason)) {
|
||||||
|
current.runtimeAlive = true;
|
||||||
|
}
|
||||||
current.hardFailure = false;
|
current.hardFailure = false;
|
||||||
current.hardFailureReason = undefined;
|
current.hardFailureReason = undefined;
|
||||||
if (current.sources) {
|
if (current.sources) {
|
||||||
|
|
@ -29592,9 +29746,10 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
const canApplyBootstrapSuccess =
|
const canApplyBootstrapSuccess =
|
||||||
!heartbeatReason &&
|
!heartbeatReason &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveFailure &&
|
||||||
(current.launchState !== 'failed_to_start' ||
|
(current.launchState !== 'failed_to_start' ||
|
||||||
hadAutoClearableFailure ||
|
hadAutoClearableFailure ||
|
||||||
isAutoClearableLaunchFailureReason(
|
isBootstrapProofClearableLaunchFailureReason(
|
||||||
current.hardFailureReason ?? current.runtimeDiagnostic
|
current.hardFailureReason ?? current.runtimeDiagnostic
|
||||||
));
|
));
|
||||||
if (!current.bootstrapConfirmed && canApplyBootstrapSuccess) {
|
if (!current.bootstrapConfirmed && canApplyBootstrapSuccess) {
|
||||||
|
|
@ -29614,7 +29769,9 @@ export class TeamProvisioningService {
|
||||||
if (bootstrapObservedAt && !isOpenCodeSecondaryLaneMember) {
|
if (bootstrapObservedAt && !isOpenCodeSecondaryLaneMember) {
|
||||||
current.bootstrapConfirmed = true;
|
current.bootstrapConfirmed = true;
|
||||||
current.lastHeartbeatAt = current.lastHeartbeatAt ?? bootstrapObservedAt;
|
current.lastHeartbeatAt = current.lastHeartbeatAt ?? bootstrapObservedAt;
|
||||||
current.runtimeAlive = runtimeProofObservedAt ? true : current.runtimeAlive === true;
|
current.runtimeAlive = runtimeProofObservedAt
|
||||||
|
? true
|
||||||
|
: current.runtimeAlive === true || requiresConfirmedBootstrapToClearFailure;
|
||||||
current.lastRuntimeAliveAt = runtimeProofObservedAt
|
current.lastRuntimeAliveAt = runtimeProofObservedAt
|
||||||
? (current.lastRuntimeAliveAt ?? bootstrapObservedAt)
|
? (current.lastRuntimeAliveAt ?? bootstrapObservedAt)
|
||||||
: current.lastRuntimeAliveAt;
|
: current.lastRuntimeAliveAt;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
mentionsProcessTableUnavailable,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
import type { WorkspaceTrustExecutionResult } from '@features/workspace-trust/main';
|
import type { WorkspaceTrustExecutionResult } from '@features/workspace-trust/main';
|
||||||
import type { MemberSpawnStatusEntry, TeamLaunchDiagnosticItem } from '@shared/types';
|
import type { MemberSpawnStatusEntry, TeamLaunchDiagnosticItem } from '@shared/types';
|
||||||
|
|
||||||
|
export { mentionsProcessTableUnavailable };
|
||||||
|
|
||||||
export interface TeamProvisioningLaunchDiagnosticsRun {
|
export interface TeamProvisioningLaunchDiagnosticsRun {
|
||||||
isLaunch: boolean;
|
isLaunch: boolean;
|
||||||
memberSpawnStatuses?: ReadonlyMap<string, MemberSpawnStatusEntry> | null;
|
memberSpawnStatuses?: ReadonlyMap<string, MemberSpawnStatusEntry> | null;
|
||||||
|
|
@ -12,10 +20,6 @@ interface LaunchDiagnosticsClockOptions {
|
||||||
|
|
||||||
const defaultNowIso = (): string => new Date().toISOString();
|
const defaultNowIso = (): string => new Date().toISOString();
|
||||||
|
|
||||||
export function mentionsProcessTableUnavailable(value: string | undefined): boolean {
|
|
||||||
return /\bprocess table\b.*\bunavailable\b/i.test(value ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildLaunchDiagnosticsFromRun(
|
export function buildLaunchDiagnosticsFromRun(
|
||||||
run: TeamProvisioningLaunchDiagnosticsRun,
|
run: TeamProvisioningLaunchDiagnosticsRun,
|
||||||
options: LaunchDiagnosticsClockOptions = {}
|
options: LaunchDiagnosticsClockOptions = {}
|
||||||
|
|
@ -28,7 +32,24 @@ export function buildLaunchDiagnosticsFromRun(
|
||||||
const observedAt = (options.nowIso ?? defaultNowIso)();
|
const observedAt = (options.nowIso ?? defaultNowIso)();
|
||||||
const items: TeamLaunchDiagnosticItem[] = [];
|
const items: TeamLaunchDiagnosticItem[] = [];
|
||||||
for (const [memberName, entry] of memberSpawnStatuses.entries()) {
|
for (const [memberName, entry] of memberSpawnStatuses.entries()) {
|
||||||
if (entry.launchState === 'confirmed_alive') {
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(entry);
|
||||||
|
if (
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry)
|
||||||
|
) {
|
||||||
|
items.push({
|
||||||
|
id: `${memberName}:bootstrap_stalled`,
|
||||||
|
memberName,
|
||||||
|
severity: 'error',
|
||||||
|
code: 'bootstrap_stalled',
|
||||||
|
label: `${memberName} - launch diagnostic error`,
|
||||||
|
detail: entry.runtimeDiagnostic ?? entry.hardFailureReason ?? entry.error,
|
||||||
|
observedAt,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.launchState === 'confirmed_alive' || bootstrapConfirmedProvisionedButNotAlive) {
|
||||||
items.push({
|
items.push({
|
||||||
id: `${memberName}:bootstrap_confirmed`,
|
id: `${memberName}:bootstrap_confirmed`,
|
||||||
memberName,
|
memberName,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
|
import {
|
||||||
|
isProvisionedButNotAliveFailureReason,
|
||||||
|
stripProcessTableUnavailableDiagnosticSuffix,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
import { mentionsProcessTableUnavailable } from './TeamProvisioningLaunchDiagnostics';
|
import { mentionsProcessTableUnavailable } from './TeamProvisioningLaunchDiagnostics';
|
||||||
import { isBootstrapInstructionPrompt } from './TeamProvisioningPromptBuilders';
|
import { isBootstrapInstructionPrompt } from './TeamProvisioningPromptBuilders';
|
||||||
|
|
||||||
|
export {
|
||||||
|
isCliProvisionedButNotAliveFailureReason,
|
||||||
|
isProvisionedButNotAliveFailureReason,
|
||||||
|
stripProcessTableUnavailableDiagnosticSuffix,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
import type { MemberLaunchState } from '@shared/types';
|
import type { MemberLaunchState } from '@shared/types';
|
||||||
|
|
||||||
export function isNeverSpawnedDuringLaunchReason(reason?: string): boolean {
|
export function isNeverSpawnedDuringLaunchReason(reason?: string): boolean {
|
||||||
|
|
@ -37,22 +48,6 @@ export function isProcessTableUnavailableFailureReason(reason?: string): boolean
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCliProvisionedButNotAliveFailureReason(reason?: string): boolean {
|
|
||||||
const text = reason?.trim();
|
|
||||||
if (!text) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /^CLI process exited \(code (?:unknown|\d+|\?)\) [\u2014-] team provisioned but not alive$/i.test(
|
|
||||||
text
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripProcessTableUnavailableDiagnosticSuffix(reason: string): string | null {
|
|
||||||
const match = /^(.*?);\s*process table (?:is )?unavailable$/i.exec(reason.trim());
|
|
||||||
const baseReason = match?.[1]?.trim();
|
|
||||||
return baseReason && baseReason.length > 0 ? baseReason : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean {
|
function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean {
|
||||||
return (
|
return (
|
||||||
isNeverSpawnedDuringLaunchReason(reason) ||
|
isNeverSpawnedDuringLaunchReason(reason) ||
|
||||||
|
|
@ -63,8 +58,7 @@ function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean {
|
||||||
isBootstrapMcpResourceReadFailureReason(reason) ||
|
isBootstrapMcpResourceReadFailureReason(reason) ||
|
||||||
isBootstrapCheckInTimeoutFailureReason(reason) ||
|
isBootstrapCheckInTimeoutFailureReason(reason) ||
|
||||||
isBootstrapInstructionPromptFailureReason(reason) ||
|
isBootstrapInstructionPromptFailureReason(reason) ||
|
||||||
isLaunchCleanupBootstrapIncompleteFailureReason(reason) ||
|
isLaunchCleanupBootstrapIncompleteFailureReason(reason)
|
||||||
isCliProvisionedButNotAliveFailureReason(reason)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import { resolveTeamProviderId } from '@main/services/runtime/providerRuntimeEnv
|
||||||
import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN, wrapAgentBlock } from '@shared/constants/agentBlocks';
|
import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN, wrapAgentBlock } from '@shared/constants/agentBlocks';
|
||||||
import { CROSS_TEAM_PREFIX_TAG } from '@shared/constants/crossTeam';
|
import { CROSS_TEAM_PREFIX_TAG } from '@shared/constants/crossTeam';
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import {
|
import {
|
||||||
getTeamTaskWorkflowColumn,
|
getTeamTaskWorkflowColumn,
|
||||||
isTeamTaskActivelyWorked,
|
isTeamTaskActivelyWorked,
|
||||||
|
|
@ -44,6 +48,57 @@ interface CanonicalSendMessageExample {
|
||||||
const SEND_MESSAGE_CANONICAL_FIELDS = ['to', 'summary', 'message'] as const;
|
const SEND_MESSAGE_CANONICAL_FIELDS = ['to', 'summary', 'message'] as const;
|
||||||
const SEND_MESSAGE_FORBIDDEN_ALIAS_FIELDS = ['recipient', 'content'] as const;
|
const SEND_MESSAGE_FORBIDDEN_ALIAS_FIELDS = ['recipient', 'content'] as const;
|
||||||
|
|
||||||
|
function isUnsafeProvisionedButNotAliveStatus(status: MemberSpawnStatusEntry | undefined) {
|
||||||
|
return (
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(status) &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(status)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafelyHealedProvisionedButNotAliveStatus(status: MemberSpawnStatusEntry | undefined) {
|
||||||
|
return (
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(status) &&
|
||||||
|
!isUnsafeProvisionedButNotAliveStatus(status)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFailedLaunchStatus(status: MemberSpawnStatusEntry): string {
|
||||||
|
return `failed to start${status.hardFailureReason ? ` - ${status.hardFailureReason}` : status.error ? ` - ${status.error}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTeammateLaunchStatusLabel(status: MemberSpawnStatusEntry | undefined): string {
|
||||||
|
if (!status) {
|
||||||
|
return 'runtime state unclear';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
status.launchState === 'failed_to_start' &&
|
||||||
|
!isSafelyHealedProvisionedButNotAliveStatus(status)
|
||||||
|
) {
|
||||||
|
return formatFailedLaunchStatus(status);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
status.launchState === 'confirmed_alive' ||
|
||||||
|
isSafelyHealedProvisionedButNotAliveStatus(status)
|
||||||
|
) {
|
||||||
|
return 'bootstrap confirmed';
|
||||||
|
}
|
||||||
|
if (status.launchState === 'runtime_pending_permission') {
|
||||||
|
return status.runtimeAlive
|
||||||
|
? 'runtime online and waiting for permission approval'
|
||||||
|
: 'waiting for permission approval';
|
||||||
|
}
|
||||||
|
if (status.runtimeAlive) {
|
||||||
|
return 'runtime online and ready for instructions';
|
||||||
|
}
|
||||||
|
if (status.launchState === 'runtime_pending_bootstrap') {
|
||||||
|
return 'spawn accepted, runtime not confirmed yet';
|
||||||
|
}
|
||||||
|
if (status.status === 'spawning') {
|
||||||
|
return 'spawn in progress';
|
||||||
|
}
|
||||||
|
return 'runtime state unclear';
|
||||||
|
}
|
||||||
|
|
||||||
export function buildCanonicalSendMessageExample(example: CanonicalSendMessageExample): string {
|
export function buildCanonicalSendMessageExample(example: CanonicalSendMessageExample): string {
|
||||||
return `{ ${SEND_MESSAGE_CANONICAL_FIELDS.map((field) => `${field}: "${example[field]}"`).join(', ')} }`;
|
return `{ ${SEND_MESSAGE_CANONICAL_FIELDS.map((field) => `${field}: "${example[field]}"`).join(', ')} }`;
|
||||||
}
|
}
|
||||||
|
|
@ -1037,22 +1092,7 @@ export function buildGeminiPostLaunchHydrationPrompt(
|
||||||
? `Current teammate launch status:\n${members
|
? `Current teammate launch status:\n${members
|
||||||
.map((member) => {
|
.map((member) => {
|
||||||
const status = run.memberSpawnStatuses.get(member.name);
|
const status = run.memberSpawnStatuses.get(member.name);
|
||||||
const label =
|
const label = buildTeammateLaunchStatusLabel(status);
|
||||||
status?.launchState === 'failed_to_start'
|
|
||||||
? `failed to start${status.hardFailureReason ? ` - ${status.hardFailureReason}` : status.error ? ` - ${status.error}` : ''}`
|
|
||||||
: status?.launchState === 'confirmed_alive'
|
|
||||||
? 'bootstrap confirmed'
|
|
||||||
: status?.launchState === 'runtime_pending_permission'
|
|
||||||
? status?.runtimeAlive
|
|
||||||
? 'runtime online and waiting for permission approval'
|
|
||||||
: 'waiting for permission approval'
|
|
||||||
: status?.runtimeAlive
|
|
||||||
? 'runtime online and ready for instructions'
|
|
||||||
: status?.launchState === 'runtime_pending_bootstrap'
|
|
||||||
? 'spawn accepted, runtime not confirmed yet'
|
|
||||||
: status?.status === 'spawning'
|
|
||||||
? 'spawn in progress'
|
|
||||||
: 'runtime state unclear';
|
|
||||||
return `- @${member.name}: ${label}`;
|
return `- @${member.name}: ${label}`;
|
||||||
})
|
})
|
||||||
.join('\n')}\n`
|
.join('\n')}\n`
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ import {
|
||||||
import { getRuntimeMemorySourceLabel } from '@renderer/utils/memberRuntimeSummary';
|
import { getRuntimeMemorySourceLabel } from '@renderer/utils/memberRuntimeSummary';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
|
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
|
@ -661,12 +665,20 @@ export const MemberCard = memo(function MemberCard({
|
||||||
selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : []
|
selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : []
|
||||||
);
|
);
|
||||||
const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]);
|
const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]);
|
||||||
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
||||||
|
const hasUnsafeBootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry);
|
||||||
|
const effectiveSpawnStatus = spawnStatus;
|
||||||
|
const effectiveSpawnLaunchState = spawnLaunchState;
|
||||||
const showTaskActivity = shouldDisplayMemberCurrentTask({
|
const showTaskActivity = shouldDisplayMemberCurrentTask({
|
||||||
member,
|
member,
|
||||||
isTeamAlive,
|
isTeamAlive,
|
||||||
spawnStatus,
|
spawnStatus: effectiveSpawnStatus,
|
||||||
spawnLaunchState,
|
spawnLaunchState: effectiveSpawnLaunchState,
|
||||||
spawnRuntimeAlive,
|
spawnRuntimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
});
|
});
|
||||||
const visibleCurrentTask = showTaskActivity ? currentTask : null;
|
const visibleCurrentTask = showTaskActivity ? currentTask : null;
|
||||||
|
|
@ -680,15 +692,19 @@ export const MemberCard = memo(function MemberCard({
|
||||||
: member;
|
: member;
|
||||||
const launchPresentation = buildMemberLaunchPresentation({
|
const launchPresentation = buildMemberLaunchPresentation({
|
||||||
member: presentationMember,
|
member: presentationMember,
|
||||||
spawnStatus,
|
spawnStatus: effectiveSpawnStatus,
|
||||||
spawnLaunchState,
|
spawnLaunchState: effectiveSpawnLaunchState,
|
||||||
spawnLivenessSource,
|
spawnLivenessSource,
|
||||||
spawnRuntimeAlive,
|
spawnRuntimeAlive,
|
||||||
spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
|
spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
|
||||||
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
|
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
|
||||||
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
||||||
spawnHardFailure: spawnEntry?.hardFailure,
|
spawnHardFailure: spawnEntry?.hardFailure,
|
||||||
|
spawnHardFailureReason: spawnEntry?.hardFailureReason,
|
||||||
|
spawnError: spawnEntry?.error,
|
||||||
|
spawnRuntimeDiagnostic: spawnEntry?.runtimeDiagnostic,
|
||||||
spawnLivenessKind: spawnEntry?.livenessKind,
|
spawnLivenessKind: spawnEntry?.livenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity,
|
||||||
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt: spawnEntry?.updatedAt,
|
spawnUpdatedAt: spawnEntry?.updatedAt,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
|
|
@ -844,7 +860,7 @@ export const MemberCard = memo(function MemberCard({
|
||||||
const showStartingSkeleton =
|
const showStartingSkeleton =
|
||||||
!isRemoved &&
|
!isRemoved &&
|
||||||
presenceLabel === 'starting' &&
|
presenceLabel === 'starting' &&
|
||||||
spawnLaunchState !== 'failed_to_start' &&
|
effectiveSpawnLaunchState !== 'failed_to_start' &&
|
||||||
!activityTask &&
|
!activityTask &&
|
||||||
!runtimeSummary;
|
!runtimeSummary;
|
||||||
const usesLaunchSkeletonSurface = spawnCardClass.includes('member-waiting-shimmer');
|
const usesLaunchSkeletonSurface = spawnCardClass.includes('member-waiting-shimmer');
|
||||||
|
|
@ -869,8 +885,8 @@ export const MemberCard = memo(function MemberCard({
|
||||||
runId: runtimeRunId,
|
runId: runtimeRunId,
|
||||||
memberName: member.name,
|
memberName: member.name,
|
||||||
member,
|
member,
|
||||||
spawnStatus,
|
spawnStatus: effectiveSpawnStatus,
|
||||||
launchState: spawnLaunchState,
|
launchState: effectiveSpawnLaunchState,
|
||||||
livenessSource: spawnLivenessSource,
|
livenessSource: spawnLivenessSource,
|
||||||
spawnEntry,
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
|
|
@ -886,9 +902,9 @@ export const MemberCard = memo(function MemberCard({
|
||||||
runtimeRunId,
|
runtimeRunId,
|
||||||
selectedTeamName,
|
selectedTeamName,
|
||||||
spawnEntry,
|
spawnEntry,
|
||||||
spawnLaunchState,
|
effectiveSpawnLaunchState,
|
||||||
spawnLivenessSource,
|
spawnLivenessSource,
|
||||||
spawnStatus,
|
effectiveSpawnStatus,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const showCopyDiagnostics =
|
const showCopyDiagnostics =
|
||||||
|
|
@ -900,7 +916,10 @@ export const MemberCard = memo(function MemberCard({
|
||||||
Boolean(runtimeAdvisoryLabel) &&
|
Boolean(runtimeAdvisoryLabel) &&
|
||||||
runtimeAdvisoryTone === 'error' &&
|
runtimeAdvisoryTone === 'error' &&
|
||||||
hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload);
|
hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload);
|
||||||
const isFailedLaunch = spawnStatus === 'error' || spawnLaunchState === 'failed_to_start';
|
const isFailedLaunch =
|
||||||
|
(!bootstrapConfirmedProvisionedButNotAlive ||
|
||||||
|
hasUnsafeBootstrapConfirmedProvisionedButNotAlive) &&
|
||||||
|
(spawnStatus === 'error' || spawnLaunchState === 'failed_to_start');
|
||||||
const isSkippedLaunch =
|
const isSkippedLaunch =
|
||||||
spawnStatus === 'skipped' ||
|
spawnStatus === 'skipped' ||
|
||||||
spawnLaunchState === 'skipped_for_launch' ||
|
spawnLaunchState === 'skipped_for_launch' ||
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ import {
|
||||||
} from '@renderer/utils/memberRuntimeSummary';
|
} from '@renderer/utils/memberRuntimeSummary';
|
||||||
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import { isTeamTaskFinishedForDependency } from '@shared/utils/teamTaskState';
|
import { isTeamTaskFinishedForDependency } from '@shared/utils/teamTaskState';
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
|
|
@ -83,7 +87,14 @@ function isOpenCodeNoRuntimeEvidenceFailure(
|
||||||
spawnEntry: MemberSpawnStatusEntry | undefined,
|
spawnEntry: MemberSpawnStatusEntry | undefined,
|
||||||
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
||||||
): boolean {
|
): boolean {
|
||||||
const failed = spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.status === 'error';
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
||||||
|
const unsafeProvisionedButNotAlive =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry);
|
||||||
|
const failed =
|
||||||
|
(!bootstrapConfirmedProvisionedButNotAlive || unsafeProvisionedButNotAlive) &&
|
||||||
|
(spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.status === 'error');
|
||||||
return member.providerId === 'opencode' && failed && !hasOpenCodeRuntimeEvidence(runtimeEntry);
|
return member.providerId === 'opencode' && failed && !hasOpenCodeRuntimeEvidence(runtimeEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,6 +191,7 @@ export const MemberDetailDialog = ({
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: spawnEntry?.status,
|
||||||
spawnLaunchState: spawnEntry?.launchState,
|
spawnLaunchState: spawnEntry?.launchState,
|
||||||
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
});
|
});
|
||||||
const displayableCurrentTask =
|
const displayableCurrentTask =
|
||||||
|
|
@ -303,7 +315,11 @@ export const MemberDetailDialog = ({
|
||||||
spawnBootstrapStalled={spawnEntry?.bootstrapStalled}
|
spawnBootstrapStalled={spawnEntry?.bootstrapStalled}
|
||||||
spawnAgentToolAccepted={spawnEntry?.agentToolAccepted}
|
spawnAgentToolAccepted={spawnEntry?.agentToolAccepted}
|
||||||
spawnHardFailure={spawnEntry?.hardFailure}
|
spawnHardFailure={spawnEntry?.hardFailure}
|
||||||
|
spawnHardFailureReason={spawnEntry?.hardFailureReason}
|
||||||
|
spawnError={spawnEntry?.error}
|
||||||
|
spawnRuntimeDiagnostic={spawnEntry?.runtimeDiagnostic}
|
||||||
spawnLivenessKind={spawnEntry?.livenessKind}
|
spawnLivenessKind={spawnEntry?.livenessKind}
|
||||||
|
spawnRuntimeDiagnosticSeverity={spawnEntry?.runtimeDiagnosticSeverity}
|
||||||
spawnFirstSpawnAcceptedAt={spawnEntry?.firstSpawnAcceptedAt}
|
spawnFirstSpawnAcceptedAt={spawnEntry?.firstSpawnAcceptedAt}
|
||||||
spawnUpdatedAt={spawnEntry?.updatedAt}
|
spawnUpdatedAt={spawnEntry?.updatedAt}
|
||||||
runtimeEntry={runtimeEntry}
|
runtimeEntry={runtimeEntry}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import type {
|
||||||
MemberSpawnLivenessSource,
|
MemberSpawnLivenessSource,
|
||||||
MemberSpawnStatus,
|
MemberSpawnStatus,
|
||||||
ResolvedTeamMember,
|
ResolvedTeamMember,
|
||||||
|
TeamAgentRuntimeDiagnosticSeverity,
|
||||||
TeamAgentRuntimeEntry,
|
TeamAgentRuntimeEntry,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -43,7 +44,11 @@ interface MemberDetailHeaderProps {
|
||||||
spawnBootstrapStalled?: boolean;
|
spawnBootstrapStalled?: boolean;
|
||||||
spawnAgentToolAccepted?: boolean;
|
spawnAgentToolAccepted?: boolean;
|
||||||
spawnHardFailure?: boolean;
|
spawnHardFailure?: boolean;
|
||||||
|
spawnHardFailureReason?: string;
|
||||||
|
spawnError?: string;
|
||||||
|
spawnRuntimeDiagnostic?: string;
|
||||||
spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind'];
|
spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind'];
|
||||||
|
spawnRuntimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
spawnFirstSpawnAcceptedAt?: string;
|
spawnFirstSpawnAcceptedAt?: string;
|
||||||
spawnUpdatedAt?: string;
|
spawnUpdatedAt?: string;
|
||||||
isLaunchSettling?: boolean;
|
isLaunchSettling?: boolean;
|
||||||
|
|
@ -66,7 +71,11 @@ export const MemberDetailHeader = ({
|
||||||
spawnBootstrapStalled,
|
spawnBootstrapStalled,
|
||||||
spawnAgentToolAccepted,
|
spawnAgentToolAccepted,
|
||||||
spawnHardFailure,
|
spawnHardFailure,
|
||||||
|
spawnHardFailureReason,
|
||||||
|
spawnError,
|
||||||
|
spawnRuntimeDiagnostic,
|
||||||
spawnLivenessKind,
|
spawnLivenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity,
|
||||||
spawnFirstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt,
|
spawnUpdatedAt,
|
||||||
isLaunchSettling,
|
isLaunchSettling,
|
||||||
|
|
@ -99,7 +108,11 @@ export const MemberDetailHeader = ({
|
||||||
spawnBootstrapStalled,
|
spawnBootstrapStalled,
|
||||||
spawnAgentToolAccepted,
|
spawnAgentToolAccepted,
|
||||||
spawnHardFailure,
|
spawnHardFailure,
|
||||||
|
spawnHardFailureReason,
|
||||||
|
spawnError,
|
||||||
|
spawnRuntimeDiagnostic,
|
||||||
spawnLivenessKind,
|
spawnLivenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity,
|
||||||
spawnFirstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt,
|
spawnUpdatedAt,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,7 @@ export const MemberHoverCard = memo(function MemberHoverCard({
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: spawnEntry?.status,
|
||||||
spawnLaunchState: spawnEntry?.launchState,
|
spawnLaunchState: spawnEntry?.launchState,
|
||||||
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
})
|
})
|
||||||
? currentTaskCandidate
|
? currentTaskCandidate
|
||||||
|
|
@ -168,7 +169,11 @@ export const MemberHoverCard = memo(function MemberHoverCard({
|
||||||
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
|
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
|
||||||
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
||||||
spawnHardFailure: spawnEntry?.hardFailure,
|
spawnHardFailure: spawnEntry?.hardFailure,
|
||||||
|
spawnHardFailureReason: spawnEntry?.hardFailureReason,
|
||||||
|
spawnError: spawnEntry?.error,
|
||||||
|
spawnRuntimeDiagnostic: spawnEntry?.runtimeDiagnostic,
|
||||||
spawnLivenessKind: spawnEntry?.livenessKind,
|
spawnLivenessKind: spawnEntry?.livenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity,
|
||||||
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt: spawnEntry?.updatedAt,
|
spawnUpdatedAt: spawnEntry?.updatedAt,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
|
|
@ -226,6 +231,7 @@ export const MemberHoverCard = memo(function MemberHoverCard({
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: spawnEntry?.status,
|
||||||
spawnLaunchState: spawnEntry?.launchState,
|
spawnLaunchState: spawnEntry?.launchState,
|
||||||
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
})
|
})
|
||||||
? reviewTaskCandidate
|
? reviewTaskCandidate
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ import { buildMemberColorMap, shouldDisplayMemberCurrentTask } from '@renderer/u
|
||||||
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
|
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
|
||||||
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
|
import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState';
|
||||||
|
|
||||||
import { MemberCard, type RuntimeTelemetryScale } from './MemberCard';
|
import { MemberCard, type RuntimeTelemetryScale } from './MemberCard';
|
||||||
|
|
@ -785,6 +789,7 @@ export const MemberList = memo(function MemberList({
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: spawnEntry?.status,
|
||||||
spawnLaunchState: spawnEntry?.launchState,
|
spawnLaunchState: spawnEntry?.launchState,
|
||||||
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -837,6 +842,7 @@ export const MemberList = memo(function MemberList({
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: spawnEntry?.status,
|
||||||
spawnLaunchState: spawnEntry?.launchState,
|
spawnLaunchState: spawnEntry?.launchState,
|
||||||
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
});
|
});
|
||||||
syncMemberActivityTimer({
|
syncMemberActivityTimer({
|
||||||
|
|
@ -924,6 +930,32 @@ export const MemberList = memo(function MemberList({
|
||||||
{activeMembers.map((member) => {
|
{activeMembers.map((member) => {
|
||||||
const spawnEntry = memberSpawnStatuses?.get(member.name);
|
const spawnEntry = memberSpawnStatuses?.get(member.name);
|
||||||
const runtimeEntry = memberRuntimeEntries?.get(member.name);
|
const runtimeEntry = memberRuntimeEntries?.get(member.name);
|
||||||
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
||||||
|
const hasUnsafeProvisionedButNotAliveEvidence =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(
|
||||||
|
spawnEntry,
|
||||||
|
runtimeEntry
|
||||||
|
);
|
||||||
|
const canPromoteBootstrapConfirmedVisualState =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
spawnEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
||||||
|
runtimeEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveEvidence;
|
||||||
|
const effectiveSpawnStatus = canPromoteBootstrapConfirmedVisualState
|
||||||
|
? 'online'
|
||||||
|
: spawnEntry?.status;
|
||||||
|
const effectiveSpawnLaunchState = canPromoteBootstrapConfirmedVisualState
|
||||||
|
? 'confirmed_alive'
|
||||||
|
: spawnEntry?.launchState;
|
||||||
|
const useBootstrapConfirmedRuntimeAlive =
|
||||||
|
canPromoteBootstrapConfirmedVisualState &&
|
||||||
|
runtimeEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
||||||
|
spawnEntry?.runtimeDiagnosticSeverity !== 'error';
|
||||||
|
const effectiveSpawnRuntimeAlive = useBootstrapConfirmedRuntimeAlive
|
||||||
|
? true
|
||||||
|
: spawnEntry?.runtimeAlive;
|
||||||
const currentTaskCandidate =
|
const currentTaskCandidate =
|
||||||
member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null;
|
member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null;
|
||||||
const currentTask =
|
const currentTask =
|
||||||
|
|
@ -931,9 +963,10 @@ export const MemberList = memo(function MemberList({
|
||||||
shouldDisplayMemberCurrentTask({
|
shouldDisplayMemberCurrentTask({
|
||||||
member,
|
member,
|
||||||
isTeamAlive,
|
isTeamAlive,
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: effectiveSpawnStatus,
|
||||||
spawnLaunchState: spawnEntry?.launchState,
|
spawnLaunchState: effectiveSpawnLaunchState,
|
||||||
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
spawnRuntimeAlive: effectiveSpawnRuntimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
})
|
})
|
||||||
? currentTaskCandidate
|
? currentTaskCandidate
|
||||||
|
|
@ -945,9 +978,10 @@ export const MemberList = memo(function MemberList({
|
||||||
shouldDisplayMemberCurrentTask({
|
shouldDisplayMemberCurrentTask({
|
||||||
member,
|
member,
|
||||||
isTeamAlive,
|
isTeamAlive,
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: effectiveSpawnStatus,
|
||||||
spawnLaunchState: spawnEntry?.launchState,
|
spawnLaunchState: effectiveSpawnLaunchState,
|
||||||
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
|
spawnRuntimeAlive: effectiveSpawnRuntimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
})
|
})
|
||||||
? reviewCandidate
|
? reviewCandidate
|
||||||
|
|
@ -995,12 +1029,16 @@ export const MemberList = memo(function MemberList({
|
||||||
runtimeSummary={buildRuntimeSummary(member, spawnEntry, runtimeEntry)}
|
runtimeSummary={buildRuntimeSummary(member, spawnEntry, runtimeEntry)}
|
||||||
runtimeEntry={runtimeEntry}
|
runtimeEntry={runtimeEntry}
|
||||||
runtimeRunId={runtimeRunId}
|
runtimeRunId={runtimeRunId}
|
||||||
spawnStatus={spawnEntry?.status}
|
spawnStatus={effectiveSpawnStatus}
|
||||||
spawnEntry={spawnEntry}
|
spawnEntry={spawnEntry}
|
||||||
spawnError={spawnEntry?.error ?? spawnEntry?.hardFailureReason}
|
spawnError={
|
||||||
|
canPromoteBootstrapConfirmedVisualState
|
||||||
|
? undefined
|
||||||
|
: (spawnEntry?.error ?? spawnEntry?.hardFailureReason)
|
||||||
|
}
|
||||||
spawnLivenessSource={spawnEntry?.livenessSource}
|
spawnLivenessSource={spawnEntry?.livenessSource}
|
||||||
spawnLaunchState={spawnEntry?.launchState}
|
spawnLaunchState={effectiveSpawnLaunchState}
|
||||||
spawnRuntimeAlive={spawnEntry?.runtimeAlive}
|
spawnRuntimeAlive={effectiveSpawnRuntimeAlive}
|
||||||
isTeamAlive={isTeamAlive}
|
isTeamAlive={isTeamAlive}
|
||||||
isTeamProvisioning={isTeamProvisioning}
|
isTeamProvisioning={isTeamProvisioning}
|
||||||
leadActivity={leadActivity}
|
leadActivity={leadActivity}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
mentionsProcessTableUnavailable,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
|
|
@ -80,6 +86,9 @@ function parseStatusUpdatedAtMs(value: string | undefined): number | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) {
|
||||||
|
return hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry);
|
||||||
|
}
|
||||||
return entry?.launchState === 'failed_to_start' || entry?.status === 'error';
|
return entry?.launchState === 'failed_to_start' || entry?.status === 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,13 +101,62 @@ function isStrongRuntimeProcessSpawnEntry(entry: MemberSpawnStatusEntry): boolea
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConfirmedSpawnEntry(entry: MemberSpawnStatusEntry): boolean {
|
function isConfirmedSpawnEntry(entry: MemberSpawnStatusEntry): boolean {
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) {
|
||||||
|
return !isFailedSpawnEntry(entry);
|
||||||
|
}
|
||||||
return entry.launchState === 'confirmed_alive' || entry.bootstrapConfirmed === true;
|
return entry.launchState === 'confirmed_alive' || entry.bootstrapConfirmed === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function spawnEntryContradictsConfirmedJoin(entry: MemberSpawnStatusEntry): boolean {
|
||||||
|
if (!isConfirmedSpawnEntry(entry) || entry.runtimeAlive !== false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (entry.runtimeDiagnosticSeverity === 'error') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
entry.livenessKind === 'not_found' ||
|
||||||
|
entry.livenessKind === 'shell_only' ||
|
||||||
|
entry.livenessKind === 'permission_blocked' ||
|
||||||
|
entry.livenessKind === 'runtime_process_candidate'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasProcessTableUnavailableMarker =
|
||||||
|
mentionsProcessTableUnavailable(entry.runtimeDiagnostic) ||
|
||||||
|
mentionsProcessTableUnavailable(entry.hardFailureReason) ||
|
||||||
|
mentionsProcessTableUnavailable(entry.error);
|
||||||
|
if (!entry.livenessKind) {
|
||||||
|
return !hasProcessTableUnavailableMarker;
|
||||||
|
}
|
||||||
|
if (entry.livenessKind !== 'registered_only' && entry.livenessKind !== 'stale_metadata') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !hasProcessTableUnavailableMarker;
|
||||||
|
}
|
||||||
|
|
||||||
function runtimeEntryContradictsConfirmedJoin(
|
function runtimeEntryContradictsConfirmedJoin(
|
||||||
|
entry: MemberSpawnStatusEntry,
|
||||||
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
||||||
): boolean {
|
): boolean {
|
||||||
return runtimeEntry?.alive === false;
|
if (runtimeEntry?.alive !== false || runtimeEntry.livenessKind === 'confirmed_bootstrap') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(entry) &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry) &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(entry, runtimeEntry) &&
|
||||||
|
(runtimeEntry.livenessKind == null ||
|
||||||
|
runtimeEntry.livenessKind === 'registered_only' ||
|
||||||
|
runtimeEntry.livenessKind === 'stale_metadata') &&
|
||||||
|
(mentionsProcessTableUnavailable(runtimeEntry.runtimeDiagnostic) ||
|
||||||
|
mentionsProcessTableUnavailable(entry.runtimeDiagnostic) ||
|
||||||
|
mentionsProcessTableUnavailable(entry.hardFailureReason) ||
|
||||||
|
mentionsProcessTableUnavailable(entry.error))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldPreferSnapshotEntryOverLive(
|
function shouldPreferSnapshotEntryOverLive(
|
||||||
|
|
@ -159,7 +217,7 @@ function summarizeLiveLaunchJoinMilestones(params: {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
observedTeammateCount += 1;
|
observedTeammateCount += 1;
|
||||||
if (entry.launchState === 'failed_to_start') {
|
if (isFailedSpawnEntry(entry)) {
|
||||||
failedSpawnCount += 1;
|
failedSpawnCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -167,14 +225,21 @@ function summarizeLiveLaunchJoinMilestones(params: {
|
||||||
skippedSpawnCount += 1;
|
skippedSpawnCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (spawnEntryContradictsConfirmedJoin(entry)) {
|
||||||
|
pendingSpawnCount += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
isConfirmedSpawnEntry(entry) &&
|
isConfirmedSpawnEntry(entry) &&
|
||||||
runtimeEntryContradictsConfirmedJoin(getRuntimeEntry(params.memberRuntimeEntries, memberName))
|
runtimeEntryContradictsConfirmedJoin(
|
||||||
|
entry,
|
||||||
|
getRuntimeEntry(params.memberRuntimeEntries, memberName)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
pendingSpawnCount += 1;
|
pendingSpawnCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (entry.launchState === 'confirmed_alive') {
|
if (isConfirmedSpawnEntry(entry)) {
|
||||||
heartbeatConfirmedCount += 1;
|
heartbeatConfirmedCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
TeamAgentRuntimeDiagnosticSeverity,
|
TeamAgentRuntimeDiagnosticSeverity,
|
||||||
|
|
@ -139,15 +145,29 @@ function buildRuntimeBackedDisplayRow(
|
||||||
spawn?: MemberSpawnStatusEntry
|
spawn?: MemberSpawnStatusEntry
|
||||||
): TeamRuntimeDisplayRow {
|
): TeamRuntimeDisplayRow {
|
||||||
const hasErrorDiagnostic = runtime.runtimeDiagnosticSeverity === 'error';
|
const hasErrorDiagnostic = runtime.runtimeDiagnosticSeverity === 'error';
|
||||||
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawn);
|
||||||
const spawnDegradation = getSpawnDegradation(spawn);
|
const spawnDegradation = getSpawnDegradation(spawn);
|
||||||
|
const unsafeRuntimeEvidence = hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(
|
||||||
|
spawn,
|
||||||
|
runtime
|
||||||
|
);
|
||||||
|
const useBootstrapConfirmedState =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
!hasErrorDiagnostic &&
|
||||||
|
!unsafeRuntimeEvidence &&
|
||||||
|
spawnDegradation == null;
|
||||||
const spawnStoppedEvidence = spawnDegradation ? null : getSpawnStoppedEvidence(runtime, spawn);
|
const spawnStoppedEvidence = spawnDegradation ? null : getSpawnStoppedEvidence(runtime, spawn);
|
||||||
const state = spawnStoppedEvidence
|
const state = useBootstrapConfirmedState
|
||||||
? 'stopped'
|
? 'running'
|
||||||
: getRuntimeBackedState(runtime, hasErrorDiagnostic, spawnDegradation != null);
|
: 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 =
|
||||||
|
(useBootstrapConfirmedState ? 'Bootstrap confirmed' : undefined) ??
|
||||||
degradedReason ??
|
degradedReason ??
|
||||||
spawnStoppedEvidence?.reason ??
|
spawnStoppedEvidence?.reason ??
|
||||||
runtime.runtimeDiagnostic ??
|
runtime.runtimeDiagnostic ??
|
||||||
|
|
@ -181,6 +201,17 @@ function buildRuntimeBackedDisplayRow(
|
||||||
|
|
||||||
function getSpawnDegradation(spawn?: MemberSpawnStatusEntry): SpawnDegradation | null {
|
function getSpawnDegradation(spawn?: MemberSpawnStatusEntry): SpawnDegradation | null {
|
||||||
if (!spawn) return null;
|
if (!spawn) return null;
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawn)) {
|
||||||
|
if (!hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawn)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const reason = spawn.runtimeDiagnostic ?? 'Runtime launch status needs attention';
|
||||||
|
return {
|
||||||
|
reason,
|
||||||
|
diagnostic: spawn.runtimeDiagnostic ?? reason,
|
||||||
|
diagnosticSeverity: spawn.runtimeDiagnosticSeverity === 'error' ? 'error' : 'warning',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (spawn.status === 'error' || spawn.hardFailure === true) {
|
if (spawn.status === 'error' || spawn.hardFailure === true) {
|
||||||
const reason =
|
const reason =
|
||||||
|
|
@ -226,7 +257,10 @@ function getSpawnStoppedEvidence(
|
||||||
runtime: TeamAgentRuntimeEntry,
|
runtime: TeamAgentRuntimeEntry,
|
||||||
spawn?: MemberSpawnStatusEntry
|
spawn?: MemberSpawnStatusEntry
|
||||||
): SpawnStoppedEvidence | null {
|
): SpawnStoppedEvidence | null {
|
||||||
if (!spawn || spawn.runtimeAlive !== false || runtime.livenessKind !== 'confirmed_bootstrap') {
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawn)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (spawn?.runtimeAlive !== false || runtime.livenessKind !== 'confirmed_bootstrap') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') {
|
if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') {
|
||||||
|
|
@ -267,6 +301,23 @@ function buildSpawnBackedDisplayRow(
|
||||||
memberName: string,
|
memberName: string,
|
||||||
spawn: MemberSpawnStatusEntry
|
spawn: MemberSpawnStatusEntry
|
||||||
): TeamRuntimeDisplayRow {
|
): TeamRuntimeDisplayRow {
|
||||||
|
if (
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawn) &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawn)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
memberName,
|
||||||
|
state: 'running',
|
||||||
|
stateReason: 'Bootstrap confirmed',
|
||||||
|
source: 'spawn-status',
|
||||||
|
updatedAt: spawn.livenessLastCheckedAt ?? spawn.lastHeartbeatAt ?? spawn.updatedAt,
|
||||||
|
runtimeModel: spawn.runtimeModel,
|
||||||
|
diagnostic: spawn.runtimeDiagnostic,
|
||||||
|
diagnosticSeverity: spawn.runtimeDiagnosticSeverity,
|
||||||
|
actionsAllowed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const spawnDegradation = getSpawnDegradation(spawn);
|
const spawnDegradation = getSpawnDegradation(spawn);
|
||||||
if (spawnDegradation) {
|
if (spawnDegradation) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -359,6 +410,7 @@ function buildSpawnBackedDisplayRow(
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSpawnOnlyStoppedEvidence(spawn: MemberSpawnStatusEntry): SpawnStoppedEvidence | null {
|
function getSpawnOnlyStoppedEvidence(spawn: MemberSpawnStatusEntry): SpawnStoppedEvidence | null {
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawn)) return null;
|
||||||
if (spawn.runtimeAlive !== false) return null;
|
if (spawn.runtimeAlive !== false) return null;
|
||||||
if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') return null;
|
if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') return null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4117,6 +4117,27 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
operation: 'fetchTeams',
|
operation: 'fetchTeams',
|
||||||
});
|
});
|
||||||
void get().fetchTeams();
|
void get().fetchTeams();
|
||||||
|
const terminalRefreshState = get();
|
||||||
|
if (isVisibleInActiveTeamSurface(terminalRefreshState, progress.teamName)) {
|
||||||
|
noteTeamRefreshFanout({
|
||||||
|
teamName: progress.teamName,
|
||||||
|
surface: 'provisioning-progress',
|
||||||
|
phase: 'scheduled',
|
||||||
|
reason: terminalReason,
|
||||||
|
operation: 'fetchMemberSpawnStatuses',
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
void terminalRefreshState.fetchMemberSpawnStatuses(progress.teamName);
|
||||||
|
noteTeamRefreshFanout({
|
||||||
|
teamName: progress.teamName,
|
||||||
|
surface: 'provisioning-progress',
|
||||||
|
phase: 'scheduled',
|
||||||
|
reason: terminalReason,
|
||||||
|
operation: 'fetchTeamAgentRuntime',
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
void terminalRefreshState.fetchTeamAgentRuntime(progress.teamName);
|
||||||
|
}
|
||||||
if (hydratedVisibleTeam) {
|
if (hydratedVisibleTeam) {
|
||||||
noteTeamRefreshFanout({
|
noteTeamRefreshFanout({
|
||||||
teamName: progress.teamName,
|
teamName: progress.teamName,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -17,6 +21,7 @@ import type {
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
MemberStatus,
|
MemberStatus,
|
||||||
ResolvedTeamMember,
|
ResolvedTeamMember,
|
||||||
|
TeamAgentRuntimeDiagnosticSeverity,
|
||||||
TeamAgentRuntimeEntry,
|
TeamAgentRuntimeEntry,
|
||||||
TeamProviderId,
|
TeamProviderId,
|
||||||
TeamReviewState,
|
TeamReviewState,
|
||||||
|
|
@ -394,7 +399,7 @@ const OPENCODE_SESSION_REFRESH_REASON_PATTERN =
|
||||||
/\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/i;
|
/\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/i;
|
||||||
const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN =
|
const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN =
|
||||||
// eslint-disable-next-line sonarjs/regex-complexity -- Keyword taxonomy is kept literal to preserve diagnostic behavior.
|
// eslint-disable-next-line sonarjs/regex-complexity -- Keyword taxonomy is kept literal to preserve diagnostic behavior.
|
||||||
/(?:^|[_\s:;.\/()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;.\/(),-])/i;
|
/(?:^|[_\s:;./()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;./(),-])/i;
|
||||||
const OPENCODE_SESSION_REFRESH_ANY_REASON_PATTERN =
|
const OPENCODE_SESSION_REFRESH_ANY_REASON_PATTERN =
|
||||||
/\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/gi;
|
/\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/gi;
|
||||||
const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN =
|
const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN =
|
||||||
|
|
@ -983,6 +988,17 @@ function getCurrentRuntimeOfflineVisualState(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasStoppedRuntimeLivenessKind(
|
||||||
|
livenessKind: TeamAgentRuntimeEntry['livenessKind'] | undefined
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
livenessKind === 'not_found' ||
|
||||||
|
livenessKind === 'registered_only' ||
|
||||||
|
livenessKind === 'shell_only' ||
|
||||||
|
livenessKind === 'stale_metadata'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isCodexNativeProcessTeammate(member: ResolvedTeamMember): boolean {
|
function isCodexNativeProcessTeammate(member: ResolvedTeamMember): boolean {
|
||||||
if (isLeadMember(member)) {
|
if (isLeadMember(member)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1076,6 +1092,7 @@ export function shouldDisplayMemberCurrentTask({
|
||||||
spawnStatus,
|
spawnStatus,
|
||||||
spawnLaunchState,
|
spawnLaunchState,
|
||||||
spawnRuntimeAlive,
|
spawnRuntimeAlive,
|
||||||
|
spawnEntry,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
}: {
|
}: {
|
||||||
member: ResolvedTeamMember;
|
member: ResolvedTeamMember;
|
||||||
|
|
@ -1083,36 +1100,64 @@ export function shouldDisplayMemberCurrentTask({
|
||||||
spawnStatus?: MemberSpawnStatus;
|
spawnStatus?: MemberSpawnStatus;
|
||||||
spawnLaunchState?: MemberLaunchState;
|
spawnLaunchState?: MemberLaunchState;
|
||||||
spawnRuntimeAlive?: boolean;
|
spawnRuntimeAlive?: boolean;
|
||||||
|
spawnEntry?: MemberSpawnStatusEntry;
|
||||||
runtimeEntry?: TeamAgentRuntimeEntry;
|
runtimeEntry?: TeamAgentRuntimeEntry;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
||||||
|
const unsafeProvisionedButNotAliveEvidence =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry);
|
||||||
|
const useBootstrapConfirmedVisualState =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive && !unsafeProvisionedButNotAliveEvidence;
|
||||||
|
const effectiveSpawnStatus = useBootstrapConfirmedVisualState ? 'online' : spawnStatus;
|
||||||
|
const effectiveSpawnLaunchState = useBootstrapConfirmedVisualState
|
||||||
|
? 'confirmed_alive'
|
||||||
|
: spawnLaunchState;
|
||||||
|
const effectiveSpawnRuntimeAlive = useBootstrapConfirmedVisualState ? true : spawnRuntimeAlive;
|
||||||
if (member.removedAt || member.status === 'terminated') {
|
if (member.removedAt || member.status === 'terminated') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isTeamAlive === false) {
|
if (isTeamAlive === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (spawnStatus === 'offline' || spawnStatus === 'error' || spawnStatus === 'skipped') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
spawnLaunchState === 'failed_to_start' ||
|
effectiveSpawnStatus === 'offline' ||
|
||||||
spawnLaunchState === 'skipped_for_launch' ||
|
effectiveSpawnStatus === 'error' ||
|
||||||
spawnLaunchState === 'runtime_pending_permission'
|
effectiveSpawnStatus === 'skipped'
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
runtimeEntry?.livenessKind === 'shell_only' ||
|
effectiveSpawnLaunchState === 'failed_to_start' ||
|
||||||
runtimeEntry?.livenessKind === 'registered_only' ||
|
effectiveSpawnLaunchState === 'skipped_for_launch' ||
|
||||||
runtimeEntry?.livenessKind === 'stale_metadata' ||
|
effectiveSpawnLaunchState === 'runtime_pending_permission'
|
||||||
runtimeEntry?.livenessKind === 'not_found'
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (runtimeEntry?.alive === false) {
|
if (
|
||||||
|
!useBootstrapConfirmedVisualState &&
|
||||||
|
(runtimeEntry?.livenessKind === 'shell_only' ||
|
||||||
|
spawnEntry?.livenessKind === 'shell_only' ||
|
||||||
|
runtimeEntry?.livenessKind === 'registered_only' ||
|
||||||
|
spawnEntry?.livenessKind === 'registered_only' ||
|
||||||
|
runtimeEntry?.livenessKind === 'stale_metadata' ||
|
||||||
|
spawnEntry?.livenessKind === 'stale_metadata' ||
|
||||||
|
runtimeEntry?.livenessKind === 'not_found' ||
|
||||||
|
spawnEntry?.livenessKind === 'not_found')
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (spawnRuntimeAlive === false) {
|
if (runtimeEntry?.runtimeDiagnosticSeverity === 'error') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (spawnEntry?.runtimeDiagnosticSeverity === 'error') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (runtimeEntry?.alive === false && !useBootstrapConfirmedVisualState) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (effectiveSpawnRuntimeAlive === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isCodexNativeProcessTeammate(member) && !hasLiveRuntimeProcessEvidence(runtimeEntry)) {
|
if (isCodexNativeProcessTeammate(member) && !hasLiveRuntimeProcessEvidence(runtimeEntry)) {
|
||||||
|
|
@ -1228,6 +1273,9 @@ export function isOpenCodeRelaunchActionable({
|
||||||
runtimeEntry?.livenessKind === 'stale_metadata'
|
runtimeEntry?.livenessKind === 'stale_metadata'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry)) {
|
||||||
|
return hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
spawnEntry?.launchState === 'failed_to_start' ||
|
spawnEntry?.launchState === 'failed_to_start' ||
|
||||||
spawnEntry?.launchState === 'skipped_for_launch' ||
|
spawnEntry?.launchState === 'skipped_for_launch' ||
|
||||||
|
|
@ -1280,7 +1328,11 @@ export function buildMemberLaunchPresentation({
|
||||||
spawnBootstrapStalled,
|
spawnBootstrapStalled,
|
||||||
spawnAgentToolAccepted,
|
spawnAgentToolAccepted,
|
||||||
spawnHardFailure,
|
spawnHardFailure,
|
||||||
|
spawnHardFailureReason,
|
||||||
|
spawnError,
|
||||||
|
spawnRuntimeDiagnostic,
|
||||||
spawnLivenessKind,
|
spawnLivenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity,
|
||||||
spawnFirstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt,
|
spawnUpdatedAt,
|
||||||
runtimeAdvisory,
|
runtimeAdvisory,
|
||||||
|
|
@ -1300,7 +1352,11 @@ export function buildMemberLaunchPresentation({
|
||||||
spawnBootstrapStalled?: boolean;
|
spawnBootstrapStalled?: boolean;
|
||||||
spawnAgentToolAccepted?: boolean;
|
spawnAgentToolAccepted?: boolean;
|
||||||
spawnHardFailure?: boolean;
|
spawnHardFailure?: boolean;
|
||||||
|
spawnHardFailureReason?: string;
|
||||||
|
spawnError?: string;
|
||||||
|
spawnRuntimeDiagnostic?: string;
|
||||||
spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind'];
|
spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind'];
|
||||||
|
spawnRuntimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
spawnFirstSpawnAcceptedAt?: string;
|
spawnFirstSpawnAcceptedAt?: string;
|
||||||
spawnUpdatedAt?: string;
|
spawnUpdatedAt?: string;
|
||||||
runtimeAdvisory: MemberRuntimeAdvisory | undefined;
|
runtimeAdvisory: MemberRuntimeAdvisory | undefined;
|
||||||
|
|
@ -1311,46 +1367,105 @@ export function buildMemberLaunchPresentation({
|
||||||
leadActivity?: LeadActivityState;
|
leadActivity?: LeadActivityState;
|
||||||
nowMs?: number;
|
nowMs?: number;
|
||||||
}): MemberLaunchPresentation {
|
}): MemberLaunchPresentation {
|
||||||
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure({
|
||||||
|
status: spawnStatus,
|
||||||
|
launchState: spawnLaunchState,
|
||||||
|
hardFailure: spawnHardFailure,
|
||||||
|
hardFailureReason: spawnHardFailureReason,
|
||||||
|
error: spawnError,
|
||||||
|
runtimeDiagnostic: spawnRuntimeDiagnostic,
|
||||||
|
runtimeDiagnosticSeverity: spawnRuntimeDiagnosticSeverity,
|
||||||
|
bootstrapConfirmed: spawnBootstrapConfirmed,
|
||||||
|
livenessKind: spawnLivenessKind ?? runtimeEntry?.livenessKind,
|
||||||
|
});
|
||||||
|
const hasSpawnRuntimeErrorDiagnostic = spawnRuntimeDiagnosticSeverity === 'error';
|
||||||
|
const hasRuntimeErrorDiagnostic = runtimeEntry?.runtimeDiagnosticSeverity === 'error';
|
||||||
|
const hasUnsafeProvisionedButNotAliveEvidence =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(
|
||||||
|
{
|
||||||
|
status: spawnStatus,
|
||||||
|
launchState: spawnLaunchState,
|
||||||
|
hardFailure: spawnHardFailure,
|
||||||
|
hardFailureReason: spawnHardFailureReason,
|
||||||
|
error: spawnError,
|
||||||
|
runtimeDiagnostic: spawnRuntimeDiagnostic,
|
||||||
|
runtimeDiagnosticSeverity: spawnRuntimeDiagnosticSeverity,
|
||||||
|
bootstrapConfirmed: spawnBootstrapConfirmed,
|
||||||
|
livenessKind: spawnLivenessKind,
|
||||||
|
},
|
||||||
|
runtimeEntry
|
||||||
|
);
|
||||||
|
const allowBootstrapConfirmedVisualPromotion =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
!hasSpawnRuntimeErrorDiagnostic &&
|
||||||
|
!hasRuntimeErrorDiagnostic &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveEvidence;
|
||||||
|
const useBootstrapConfirmedRuntimeAlive =
|
||||||
|
allowBootstrapConfirmedVisualPromotion && !hasRuntimeErrorDiagnostic;
|
||||||
|
const suppressConfirmedLaunchRuntimeAlivePromotion =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive && !useBootstrapConfirmedRuntimeAlive;
|
||||||
|
const visualSpawnStatus = allowBootstrapConfirmedVisualPromotion ? 'online' : spawnStatus;
|
||||||
|
const visualSpawnLaunchState = allowBootstrapConfirmedVisualPromotion
|
||||||
|
? 'confirmed_alive'
|
||||||
|
: spawnLaunchState;
|
||||||
|
const visualSpawnRuntimeAlive = useBootstrapConfirmedRuntimeAlive ? true : spawnRuntimeAlive;
|
||||||
|
const visualSpawnBootstrapConfirmed = allowBootstrapConfirmedVisualPromotion
|
||||||
|
? true
|
||||||
|
: spawnBootstrapConfirmed;
|
||||||
|
const visualSpawnHardFailure = allowBootstrapConfirmedVisualPromotion ? false : spawnHardFailure;
|
||||||
|
const visualSpawnLivenessKind = allowBootstrapConfirmedVisualPromotion
|
||||||
|
? 'confirmed_bootstrap'
|
||||||
|
: spawnLivenessKind;
|
||||||
|
const visualRuntimeEntry =
|
||||||
|
useBootstrapConfirmedRuntimeAlive && runtimeEntry
|
||||||
|
? ({
|
||||||
|
...runtimeEntry,
|
||||||
|
alive: true,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
} satisfies TeamAgentRuntimeEntry)
|
||||||
|
: runtimeEntry;
|
||||||
const currentRuntimeOfflineVisualState = getCurrentRuntimeOfflineVisualState(
|
const currentRuntimeOfflineVisualState = getCurrentRuntimeOfflineVisualState(
|
||||||
member,
|
member,
|
||||||
runtimeEntry,
|
visualRuntimeEntry,
|
||||||
spawnStatus,
|
visualSpawnStatus,
|
||||||
spawnLaunchState,
|
visualSpawnLaunchState,
|
||||||
spawnRuntimeAlive,
|
visualSpawnRuntimeAlive,
|
||||||
spawnBootstrapConfirmed,
|
visualSpawnBootstrapConfirmed,
|
||||||
isTeamProvisioning
|
isTeamProvisioning
|
||||||
);
|
);
|
||||||
const hasConfirmedSpawnLaunch =
|
const hasConfirmedSpawnLaunch =
|
||||||
spawnLaunchState === 'confirmed_alive' && spawnBootstrapConfirmed === true;
|
visualSpawnLaunchState === 'confirmed_alive' && visualSpawnBootstrapConfirmed === true;
|
||||||
const suppressOpenCodeAppMcpAdvisory = isHealthyOpenCodeAppMcpConnectivityAdvisory({
|
const suppressOpenCodeAppMcpAdvisory = isHealthyOpenCodeAppMcpConnectivityAdvisory({
|
||||||
providerId: member.providerId,
|
providerId: member.providerId,
|
||||||
runtimeAdvisory,
|
runtimeAdvisory,
|
||||||
spawnStatus,
|
spawnStatus: visualSpawnStatus,
|
||||||
launchState: spawnLaunchState,
|
launchState: visualSpawnLaunchState,
|
||||||
runtimeAlive: spawnRuntimeAlive,
|
runtimeAlive: visualSpawnRuntimeAlive,
|
||||||
bootstrapConfirmed: spawnBootstrapConfirmed,
|
bootstrapConfirmed: visualSpawnBootstrapConfirmed,
|
||||||
agentToolAccepted: spawnAgentToolAccepted,
|
agentToolAccepted: spawnAgentToolAccepted,
|
||||||
hardFailure: spawnHardFailure,
|
hardFailure: visualSpawnHardFailure,
|
||||||
livenessKind: spawnLivenessKind ?? runtimeEntry?.livenessKind,
|
livenessKind: visualSpawnLivenessKind ?? visualRuntimeEntry?.livenessKind,
|
||||||
runtimeEntry,
|
runtimeEntry: visualRuntimeEntry,
|
||||||
});
|
});
|
||||||
const displayRuntimeAdvisory = suppressOpenCodeAppMcpAdvisory ? undefined : runtimeAdvisory;
|
const displayRuntimeAdvisory = suppressOpenCodeAppMcpAdvisory ? undefined : runtimeAdvisory;
|
||||||
const effectiveSpawnStatus =
|
const effectiveSpawnStatus =
|
||||||
hasConfirmedSpawnLaunch &&
|
hasConfirmedSpawnLaunch &&
|
||||||
currentRuntimeOfflineVisualState == null &&
|
currentRuntimeOfflineVisualState == null &&
|
||||||
(spawnStatus === 'waiting' || spawnStatus === 'spawning')
|
(visualSpawnStatus === 'waiting' || visualSpawnStatus === 'spawning')
|
||||||
? 'online'
|
? 'online'
|
||||||
: spawnStatus;
|
: visualSpawnStatus;
|
||||||
const effectiveSpawnRuntimeAlive =
|
const effectiveSpawnRuntimeAlive =
|
||||||
currentRuntimeOfflineVisualState != null
|
currentRuntimeOfflineVisualState != null
|
||||||
? false
|
? false
|
||||||
: hasConfirmedSpawnLaunch
|
: hasConfirmedSpawnLaunch && !suppressConfirmedLaunchRuntimeAlivePromotion
|
||||||
? true
|
? true
|
||||||
: spawnRuntimeAlive;
|
: visualSpawnRuntimeAlive;
|
||||||
const presenceLabel = getLaunchAwarePresenceLabel(
|
const presenceLabel = getLaunchAwarePresenceLabel(
|
||||||
member,
|
member,
|
||||||
effectiveSpawnStatus,
|
effectiveSpawnStatus,
|
||||||
spawnLaunchState,
|
visualSpawnLaunchState,
|
||||||
spawnLivenessSource,
|
spawnLivenessSource,
|
||||||
effectiveSpawnRuntimeAlive,
|
effectiveSpawnRuntimeAlive,
|
||||||
displayRuntimeAdvisory,
|
displayRuntimeAdvisory,
|
||||||
|
|
@ -1362,7 +1477,7 @@ export function buildMemberLaunchPresentation({
|
||||||
const baseDotClass = getSpawnAwareDotClass(
|
const baseDotClass = getSpawnAwareDotClass(
|
||||||
member,
|
member,
|
||||||
effectiveSpawnStatus,
|
effectiveSpawnStatus,
|
||||||
spawnLaunchState,
|
visualSpawnLaunchState,
|
||||||
effectiveSpawnRuntimeAlive,
|
effectiveSpawnRuntimeAlive,
|
||||||
isLaunchSettling,
|
isLaunchSettling,
|
||||||
isTeamAlive,
|
isTeamAlive,
|
||||||
|
|
@ -1371,7 +1486,7 @@ export function buildMemberLaunchPresentation({
|
||||||
);
|
);
|
||||||
const cardClass = getSpawnCardClass(
|
const cardClass = getSpawnCardClass(
|
||||||
effectiveSpawnStatus,
|
effectiveSpawnStatus,
|
||||||
spawnLaunchState,
|
visualSpawnLaunchState,
|
||||||
effectiveSpawnRuntimeAlive,
|
effectiveSpawnRuntimeAlive,
|
||||||
isLaunchSettling,
|
isLaunchSettling,
|
||||||
isTeamAlive,
|
isTeamAlive,
|
||||||
|
|
@ -1393,8 +1508,8 @@ export function buildMemberLaunchPresentation({
|
||||||
const startingIsStale =
|
const startingIsStale =
|
||||||
!hasConfirmedSpawnLaunch &&
|
!hasConfirmedSpawnLaunch &&
|
||||||
isMemberStartingStale({
|
isMemberStartingStale({
|
||||||
spawnStatus,
|
spawnStatus: visualSpawnStatus,
|
||||||
spawnLaunchState,
|
spawnLaunchState: visualSpawnLaunchState,
|
||||||
spawnFirstSpawnAcceptedAt,
|
spawnFirstSpawnAcceptedAt,
|
||||||
spawnUpdatedAt,
|
spawnUpdatedAt,
|
||||||
nowMs,
|
nowMs,
|
||||||
|
|
@ -1402,19 +1517,19 @@ export function buildMemberLaunchPresentation({
|
||||||
|
|
||||||
let launchVisualState: MemberLaunchVisualState = null;
|
let launchVisualState: MemberLaunchVisualState = null;
|
||||||
if (isTeamAlive !== false || isTeamProvisioning) {
|
if (isTeamAlive !== false || isTeamProvisioning) {
|
||||||
if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') {
|
if (visualSpawnLaunchState === 'failed_to_start' || visualSpawnStatus === 'error') {
|
||||||
launchVisualState = 'error';
|
launchVisualState = 'error';
|
||||||
} else if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') {
|
} else if (visualSpawnLaunchState === 'skipped_for_launch' || visualSpawnStatus === 'skipped') {
|
||||||
launchVisualState = 'skipped';
|
launchVisualState = 'skipped';
|
||||||
} else if (spawnLaunchState === 'runtime_pending_permission') {
|
} else if (visualSpawnLaunchState === 'runtime_pending_permission') {
|
||||||
launchVisualState = 'permission_pending';
|
launchVisualState = 'permission_pending';
|
||||||
} else if (spawnBootstrapStalled === true) {
|
} else if (spawnBootstrapStalled === true) {
|
||||||
launchVisualState = 'bootstrap_stalled';
|
launchVisualState = 'bootstrap_stalled';
|
||||||
} else if (currentRuntimeOfflineVisualState != null) {
|
} else if (currentRuntimeOfflineVisualState != null) {
|
||||||
launchVisualState = currentRuntimeOfflineVisualState;
|
launchVisualState = currentRuntimeOfflineVisualState;
|
||||||
} else if (runtimeEntry?.livenessKind === 'shell_only') {
|
} else if (visualRuntimeEntry?.livenessKind === 'shell_only') {
|
||||||
launchVisualState = 'shell_only';
|
launchVisualState = 'shell_only';
|
||||||
} else if (runtimeEntry?.livenessKind === 'runtime_process_candidate') {
|
} else if (visualRuntimeEntry?.livenessKind === 'runtime_process_candidate') {
|
||||||
launchVisualState = 'runtime_candidate';
|
launchVisualState = 'runtime_candidate';
|
||||||
} else if (!hasConfirmedSpawnLaunch && startingIsStale) {
|
} else if (!hasConfirmedSpawnLaunch && startingIsStale) {
|
||||||
launchVisualState = 'starting_stale';
|
launchVisualState = 'starting_stale';
|
||||||
|
|
@ -1422,9 +1537,9 @@ export function buildMemberLaunchPresentation({
|
||||||
!hasConfirmedSpawnLaunch &&
|
!hasConfirmedSpawnLaunch &&
|
||||||
isQueuedOpenCodeLaunch(
|
isQueuedOpenCodeLaunch(
|
||||||
member,
|
member,
|
||||||
spawnStatus,
|
visualSpawnStatus,
|
||||||
spawnLaunchState,
|
visualSpawnLaunchState,
|
||||||
runtimeEntry,
|
visualRuntimeEntry,
|
||||||
isLaunchSettling,
|
isLaunchSettling,
|
||||||
isTeamProvisioning
|
isTeamProvisioning
|
||||||
)
|
)
|
||||||
|
|
@ -1433,21 +1548,21 @@ export function buildMemberLaunchPresentation({
|
||||||
} else if (
|
} else if (
|
||||||
!hasConfirmedSpawnLaunch &&
|
!hasConfirmedSpawnLaunch &&
|
||||||
isLaunchStillStarting(
|
isLaunchStillStarting(
|
||||||
spawnStatus,
|
visualSpawnStatus,
|
||||||
spawnLaunchState,
|
visualSpawnLaunchState,
|
||||||
spawnRuntimeAlive,
|
visualSpawnRuntimeAlive,
|
||||||
keepLaunchSettlingVisuals
|
keepLaunchSettlingVisuals
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
launchVisualState = spawnStatus === 'spawning' ? 'spawning' : 'waiting';
|
launchVisualState = visualSpawnStatus === 'spawning' ? 'spawning' : 'waiting';
|
||||||
} else if (
|
} else if (
|
||||||
!hasConfirmedSpawnLaunch &&
|
!hasConfirmedSpawnLaunch &&
|
||||||
spawnLaunchState === 'runtime_pending_bootstrap' &&
|
visualSpawnLaunchState === 'runtime_pending_bootstrap' &&
|
||||||
(runtimeEntry?.livenessKind === 'runtime_process' ||
|
(visualRuntimeEntry?.livenessKind === 'runtime_process' ||
|
||||||
(spawnStatus === 'online' && spawnRuntimeAlive === true))
|
(visualSpawnStatus === 'online' && visualSpawnRuntimeAlive === true))
|
||||||
) {
|
) {
|
||||||
launchVisualState = 'runtime_pending';
|
launchVisualState = 'runtime_pending';
|
||||||
} else if (isLaunchSettling && spawnLaunchState === 'confirmed_alive') {
|
} else if (isLaunchSettling && visualSpawnLaunchState === 'confirmed_alive') {
|
||||||
launchVisualState = 'settling';
|
launchVisualState = 'settling';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1471,12 +1586,12 @@ export function buildMemberLaunchPresentation({
|
||||||
? (launchStatusLabel ?? presenceLabel)
|
? (launchStatusLabel ?? presenceLabel)
|
||||||
: presenceLabel;
|
: presenceLabel;
|
||||||
const spawnBadgeLabel =
|
const spawnBadgeLabel =
|
||||||
spawnStatus && spawnStatus !== 'online'
|
effectiveSpawnStatus && effectiveSpawnStatus !== 'online'
|
||||||
? spawnStatus === 'waiting' || spawnStatus === 'spawning'
|
? effectiveSpawnStatus === 'waiting' || effectiveSpawnStatus === 'spawning'
|
||||||
? startingIsStale
|
? startingIsStale
|
||||||
? 'starting stale'
|
? 'starting stale'
|
||||||
: 'starting'
|
: 'starting'
|
||||||
: spawnStatus
|
: effectiveSpawnStatus
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
import { isHealthyOpenCodeAppMcpConnectivityAdvisory } from './openCodeAdvisoryHealth';
|
import { isHealthyOpenCodeAppMcpConnectivityAdvisory } from './openCodeAdvisoryHealth';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -87,6 +93,15 @@ const SECRET_ENV_KEY_PARTS = [
|
||||||
'PASSWORD',
|
'PASSWORD',
|
||||||
'AUTHORIZATION',
|
'AUTHORIZATION',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function hasStoppedRuntimeLivenessKind(livenessKind: TeamAgentRuntimeLivenessKind | undefined) {
|
||||||
|
return (
|
||||||
|
livenessKind === 'not_found' ||
|
||||||
|
livenessKind === 'registered_only' ||
|
||||||
|
livenessKind === 'shell_only' ||
|
||||||
|
livenessKind === 'stale_metadata'
|
||||||
|
);
|
||||||
|
}
|
||||||
const OPENCODE_SESSION_REFRESH_REASON_MARKERS = [
|
const OPENCODE_SESSION_REFRESH_REASON_MARKERS = [
|
||||||
'resolved_behavior_changed',
|
'resolved_behavior_changed',
|
||||||
'opencode_app_mcp_transport_changed',
|
'opencode_app_mcp_transport_changed',
|
||||||
|
|
@ -94,7 +109,7 @@ const OPENCODE_SESSION_REFRESH_REASON_MARKERS = [
|
||||||
const OPENCODE_SESSION_REFRESH_REASON_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789._~/=->';
|
const OPENCODE_SESSION_REFRESH_REASON_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789._~/=->';
|
||||||
const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN =
|
const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN =
|
||||||
// eslint-disable-next-line sonarjs/regex-complexity -- Keyword taxonomy is kept literal to preserve diagnostic behavior.
|
// eslint-disable-next-line sonarjs/regex-complexity -- Keyword taxonomy is kept literal to preserve diagnostic behavior.
|
||||||
/(?:^|[_\s:;.\/()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;.\/(),-])/i;
|
/(?:^|[_\s:;./()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;./(),-])/i;
|
||||||
const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN =
|
const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN =
|
||||||
/\b(?:not_observed|pending|prompt_not_indexed|responded_tool_call|responded_visible_message|responded_non_visible_tool|responded_plain_text|permission_blocked|tool_error|empty_assistant_turn|prompt_delivered_no_assistant_message|session_stale|session_error|reconcile_failed)\b/g;
|
/\b(?:not_observed|pending|prompt_not_indexed|responded_tool_call|responded_visible_message|responded_non_visible_tool|responded_plain_text|permission_blocked|tool_error|empty_assistant_turn|prompt_delivered_no_assistant_message|session_stale|session_error|reconcile_failed)\b/g;
|
||||||
|
|
||||||
|
|
@ -527,9 +542,48 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
|
||||||
const providerBackendId = runtimeEntry?.providerBackendId ?? params.member?.providerBackendId;
|
const providerBackendId = runtimeEntry?.providerBackendId ?? params.member?.providerBackendId;
|
||||||
const laneId = runtimeEntry?.laneId ?? params.member?.laneId;
|
const laneId = runtimeEntry?.laneId ?? params.member?.laneId;
|
||||||
const laneKind = runtimeEntry?.laneKind ?? params.member?.laneKind;
|
const laneKind = runtimeEntry?.laneKind ?? params.member?.laneKind;
|
||||||
const livenessKind = spawnEntry?.livenessKind ?? runtimeEntry?.livenessKind;
|
const livenessKind = hasStoppedRuntimeLivenessKind(runtimeEntry?.livenessKind)
|
||||||
const launchState = spawnEntry?.launchState ?? params.launchState;
|
? runtimeEntry?.livenessKind
|
||||||
const spawnStatus = spawnEntry?.status ?? params.spawnStatus;
|
: (spawnEntry?.livenessKind ?? runtimeEntry?.livenessKind);
|
||||||
|
const bootstrapConfirmedProvisionedButNotAlive =
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
||||||
|
const hasUnsafeSpawnProvisionedButNotAliveEvidence =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawnEntry);
|
||||||
|
const hasUnsafeRuntimeProvisionedButNotAliveEvidence =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
!hasUnsafeSpawnProvisionedButNotAliveEvidence &&
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry);
|
||||||
|
const hasUnsafeProvisionedButNotAliveEvidence =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
(hasUnsafeSpawnProvisionedButNotAliveEvidence ||
|
||||||
|
hasUnsafeRuntimeProvisionedButNotAliveEvidence);
|
||||||
|
const useBootstrapConfirmedVisualState =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive &&
|
||||||
|
spawnEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
||||||
|
runtimeEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
||||||
|
!hasUnsafeProvisionedButNotAliveEvidence;
|
||||||
|
const useBootstrapConfirmedRuntimeAlive =
|
||||||
|
useBootstrapConfirmedVisualState &&
|
||||||
|
runtimeEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
||||||
|
spawnEntry?.runtimeDiagnosticSeverity !== 'error';
|
||||||
|
const runtimeEntryDiagnostic = boundedString(runtimeEntry?.runtimeDiagnostic);
|
||||||
|
const hasRuntimeDiagnosticEvidence =
|
||||||
|
runtimeEntryDiagnostic != null || runtimeEntry?.runtimeDiagnosticSeverity != null;
|
||||||
|
const useSpawnDiagnosticsForHealedEntry =
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive && !hasRuntimeDiagnosticEvidence;
|
||||||
|
const keepSpawnFailureDiagnostics =
|
||||||
|
useSpawnDiagnosticsForHealedEntry ||
|
||||||
|
hasUnsafeSpawnProvisionedButNotAliveEvidence ||
|
||||||
|
spawnEntry?.runtimeDiagnosticSeverity === 'error';
|
||||||
|
const launchState = useBootstrapConfirmedVisualState
|
||||||
|
? 'confirmed_alive'
|
||||||
|
: (spawnEntry?.launchState ?? params.launchState);
|
||||||
|
const spawnStatus = useBootstrapConfirmedVisualState
|
||||||
|
? 'online'
|
||||||
|
: (spawnEntry?.status ?? params.spawnStatus);
|
||||||
|
const spawnRuntimeAlive = useBootstrapConfirmedRuntimeAlive ? true : spawnEntry?.runtimeAlive;
|
||||||
|
const spawnHardFailure = useBootstrapConfirmedVisualState ? false : spawnEntry?.hardFailure;
|
||||||
const runtimeAdvisoryTitle = boundedString(params.runtimeAdvisoryTitle);
|
const runtimeAdvisoryTitle = boundedString(params.runtimeAdvisoryTitle);
|
||||||
const runtimeAdvisoryLabel = boundedString(params.runtimeAdvisoryLabel ?? undefined);
|
const runtimeAdvisoryLabel = boundedString(params.runtimeAdvisoryLabel ?? undefined);
|
||||||
const runtimeAdvisoryMessage = boundedString(runtimeAdvisory?.message);
|
const runtimeAdvisoryMessage = boundedString(runtimeAdvisory?.message);
|
||||||
|
|
@ -541,10 +595,10 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
|
||||||
runtimeAdvisoryMessage,
|
runtimeAdvisoryMessage,
|
||||||
spawnStatus,
|
spawnStatus,
|
||||||
launchState,
|
launchState,
|
||||||
runtimeAlive: spawnEntry?.runtimeAlive,
|
runtimeAlive: spawnRuntimeAlive,
|
||||||
bootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
|
bootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
|
||||||
agentToolAccepted: spawnEntry?.agentToolAccepted,
|
agentToolAccepted: spawnEntry?.agentToolAccepted,
|
||||||
hardFailure: spawnEntry?.hardFailure,
|
hardFailure: spawnHardFailure,
|
||||||
livenessKind,
|
livenessKind,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
});
|
});
|
||||||
|
|
@ -553,9 +607,17 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
|
||||||
? (runtimeAdvisoryTitle ?? runtimeAdvisoryLabel ?? runtimeAdvisoryMessage)
|
? (runtimeAdvisoryTitle ?? runtimeAdvisoryLabel ?? runtimeAdvisoryMessage)
|
||||||
: undefined;
|
: undefined;
|
||||||
const runtimeDiagnosticSeverity =
|
const runtimeDiagnosticSeverity =
|
||||||
spawnEntry?.runtimeDiagnosticSeverity ?? runtimeEntry?.runtimeDiagnosticSeverity;
|
spawnEntry?.runtimeDiagnosticSeverity === 'error'
|
||||||
|
? spawnEntry.runtimeDiagnosticSeverity
|
||||||
|
: bootstrapConfirmedProvisionedButNotAlive
|
||||||
|
? (runtimeEntry?.runtimeDiagnosticSeverity ??
|
||||||
|
(useSpawnDiagnosticsForHealedEntry ? spawnEntry?.runtimeDiagnosticSeverity : undefined))
|
||||||
|
: (spawnEntry?.runtimeDiagnosticSeverity ?? runtimeEntry?.runtimeDiagnosticSeverity);
|
||||||
const spawnRuntimeDiagnosticCardError = isRuntimeDiagnosticCardError({
|
const spawnRuntimeDiagnosticCardError = isRuntimeDiagnosticCardError({
|
||||||
runtimeDiagnostic: spawnEntry?.runtimeDiagnostic,
|
runtimeDiagnostic:
|
||||||
|
bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics
|
||||||
|
? undefined
|
||||||
|
: spawnEntry?.runtimeDiagnostic,
|
||||||
runtimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity,
|
runtimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity,
|
||||||
launchState: spawnEntry?.launchState,
|
launchState: spawnEntry?.launchState,
|
||||||
spawnStatus: spawnEntry?.status,
|
spawnStatus: spawnEntry?.status,
|
||||||
|
|
@ -564,6 +626,10 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
|
||||||
})
|
})
|
||||||
? spawnEntry?.runtimeDiagnostic
|
? spawnEntry?.runtimeDiagnostic
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const healedSpawnFailureCardError =
|
||||||
|
keepSpawnFailureDiagnostics && spawnEntry?.runtimeDiagnosticSeverity === 'error'
|
||||||
|
? (spawnRuntimeDiagnosticCardError ?? spawnEntry?.error ?? spawnEntry?.hardFailureReason)
|
||||||
|
: undefined;
|
||||||
const runtimeEntryDiagnosticCardError = isRuntimeDiagnosticCardError({
|
const runtimeEntryDiagnosticCardError = isRuntimeDiagnosticCardError({
|
||||||
runtimeDiagnostic: runtimeEntry?.runtimeDiagnostic,
|
runtimeDiagnostic: runtimeEntry?.runtimeDiagnostic,
|
||||||
runtimeDiagnosticSeverity: runtimeEntry?.runtimeDiagnosticSeverity,
|
runtimeDiagnosticSeverity: runtimeEntry?.runtimeDiagnosticSeverity,
|
||||||
|
|
@ -572,19 +638,24 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
|
||||||
? runtimeEntry?.runtimeDiagnostic
|
? runtimeEntry?.runtimeDiagnostic
|
||||||
: undefined;
|
: undefined;
|
||||||
const runtimeDiagnostic =
|
const runtimeDiagnostic =
|
||||||
boundedString(spawnEntry?.runtimeDiagnostic) ??
|
(bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics
|
||||||
boundedString(runtimeEntry?.runtimeDiagnostic) ??
|
? undefined
|
||||||
boundedString(spawnEntry?.hardFailureReason) ??
|
: boundedString(spawnEntry?.runtimeDiagnostic)) ??
|
||||||
boundedString(spawnEntry?.error) ??
|
runtimeEntryDiagnostic ??
|
||||||
|
(bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics
|
||||||
|
? undefined
|
||||||
|
: (boundedString(spawnEntry?.hardFailureReason) ?? boundedString(spawnEntry?.error))) ??
|
||||||
runtimeAdvisoryMessage;
|
runtimeAdvisoryMessage;
|
||||||
const memberCardError = firstMemberCardFailureReason({
|
const memberCardError = firstMemberCardFailureReason({
|
||||||
candidates: [
|
candidates: bootstrapConfirmedProvisionedButNotAlive
|
||||||
spawnEntry?.error,
|
? [healedSpawnFailureCardError, runtimeEntryDiagnosticCardError, runtimeAdvisoryCardError]
|
||||||
spawnEntry?.hardFailureReason,
|
: [
|
||||||
spawnRuntimeDiagnosticCardError,
|
spawnEntry?.error,
|
||||||
runtimeEntryDiagnosticCardError,
|
spawnEntry?.hardFailureReason,
|
||||||
runtimeAdvisoryCardError,
|
spawnRuntimeDiagnosticCardError,
|
||||||
],
|
runtimeEntryDiagnosticCardError,
|
||||||
|
runtimeAdvisoryCardError,
|
||||||
|
],
|
||||||
evidence: [
|
evidence: [
|
||||||
spawnEntry?.runtimeDiagnostic,
|
spawnEntry?.runtimeDiagnostic,
|
||||||
runtimeEntry?.runtimeDiagnostic,
|
runtimeEntry?.runtimeDiagnostic,
|
||||||
|
|
@ -601,8 +672,13 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
|
||||||
runtimeAdvisoryTitle ? [runtimeAdvisoryTitle] : undefined,
|
runtimeAdvisoryTitle ? [runtimeAdvisoryTitle] : undefined,
|
||||||
runtimeAdvisoryLabel ? [runtimeAdvisoryLabel] : undefined,
|
runtimeAdvisoryLabel ? [runtimeAdvisoryLabel] : undefined,
|
||||||
runtimeAdvisoryMessage ? [runtimeAdvisoryMessage] : undefined,
|
runtimeAdvisoryMessage ? [runtimeAdvisoryMessage] : undefined,
|
||||||
spawnEntry?.hardFailureReason ? [spawnEntry.hardFailureReason] : undefined,
|
(!bootstrapConfirmedProvisionedButNotAlive || keepSpawnFailureDiagnostics) &&
|
||||||
spawnEntry?.error ? [spawnEntry.error] : undefined,
|
spawnEntry?.hardFailureReason
|
||||||
|
? [spawnEntry.hardFailureReason]
|
||||||
|
: undefined,
|
||||||
|
(!bootstrapConfirmedProvisionedButNotAlive || keepSpawnFailureDiagnostics) && spawnEntry?.error
|
||||||
|
? [spawnEntry.error]
|
||||||
|
: undefined,
|
||||||
runtimeEntry?.diagnostics
|
runtimeEntry?.diagnostics
|
||||||
);
|
);
|
||||||
const runId = boundedString(params.runId ?? undefined);
|
const runId = boundedString(params.runId ?? undefined);
|
||||||
|
|
@ -648,18 +724,14 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
|
||||||
...(typeof runtimeEntry?.restartable === 'boolean'
|
...(typeof runtimeEntry?.restartable === 'boolean'
|
||||||
? { restartable: runtimeEntry.restartable }
|
? { restartable: runtimeEntry.restartable }
|
||||||
: {}),
|
: {}),
|
||||||
...(typeof spawnEntry?.runtimeAlive === 'boolean'
|
...(typeof spawnRuntimeAlive === 'boolean' ? { runtimeAlive: spawnRuntimeAlive } : {}),
|
||||||
? { runtimeAlive: spawnEntry.runtimeAlive }
|
|
||||||
: {}),
|
|
||||||
...(typeof spawnEntry?.bootstrapConfirmed === 'boolean'
|
...(typeof spawnEntry?.bootstrapConfirmed === 'boolean'
|
||||||
? { bootstrapConfirmed: spawnEntry.bootstrapConfirmed }
|
? { bootstrapConfirmed: spawnEntry.bootstrapConfirmed }
|
||||||
: {}),
|
: {}),
|
||||||
...(typeof spawnEntry?.agentToolAccepted === 'boolean'
|
...(typeof spawnEntry?.agentToolAccepted === 'boolean'
|
||||||
? { agentToolAccepted: spawnEntry.agentToolAccepted }
|
? { agentToolAccepted: spawnEntry.agentToolAccepted }
|
||||||
: {}),
|
: {}),
|
||||||
...(typeof spawnEntry?.hardFailure === 'boolean'
|
...(typeof spawnHardFailure === 'boolean' ? { hardFailure: spawnHardFailure } : {}),
|
||||||
? { hardFailure: spawnEntry.hardFailure }
|
|
||||||
: {}),
|
|
||||||
...(livenessKind ? { livenessKind } : {}),
|
...(livenessKind ? { livenessKind } : {}),
|
||||||
...((spawnEntry?.livenessSource ?? params.livenessSource)
|
...((spawnEntry?.livenessSource ?? params.livenessSource)
|
||||||
? { livenessSource: spawnEntry?.livenessSource ?? params.livenessSource }
|
? { livenessSource: spawnEntry?.livenessSource ?? params.livenessSource }
|
||||||
|
|
@ -751,6 +823,9 @@ function parseStatusUpdatedAtMs(value: string | undefined): number | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) {
|
||||||
|
return hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry);
|
||||||
|
}
|
||||||
return entry?.launchState === 'failed_to_start' || entry?.status === 'error';
|
return entry?.launchState === 'failed_to_start' || entry?.status === 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ import {
|
||||||
getLaunchJoinState,
|
getLaunchJoinState,
|
||||||
} from '@renderer/components/team/provisioningSteps';
|
} from '@renderer/components/team/provisioningSteps';
|
||||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
|
|
@ -85,9 +89,19 @@ function parseStatusUpdatedAtMs(value: string | undefined): number | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) {
|
||||||
|
return hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry);
|
||||||
|
}
|
||||||
return entry?.launchState === 'failed_to_start' || entry?.status === 'error';
|
return entry?.launchState === 'failed_to_start' || entry?.status === 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isConfirmedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
||||||
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) {
|
||||||
|
return !isFailedSpawnEntry(entry);
|
||||||
|
}
|
||||||
|
return entry?.launchState === 'confirmed_alive' || entry?.bootstrapConfirmed === true;
|
||||||
|
}
|
||||||
|
|
||||||
function isSkippedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
function isSkippedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
||||||
return entry?.launchState === 'skipped_for_launch' || entry?.skippedForLaunch === true;
|
return entry?.launchState === 'skipped_for_launch' || entry?.skippedForLaunch === true;
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +139,7 @@ function isOpenCodeSecondaryRetryCandidate(params: {
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return entry.launchState === 'failed_to_start' || entry.status === 'error';
|
return isFailedSpawnEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldPreferSnapshotEntryOverLive(params: {
|
function shouldPreferSnapshotEntryOverLive(params: {
|
||||||
|
|
@ -275,7 +289,7 @@ function getPendingDiagnosticNameGroups(params: {
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
!entry ||
|
!entry ||
|
||||||
entry.launchState === 'confirmed_alive' ||
|
isConfirmedSpawnEntry(entry) ||
|
||||||
isFailedSpawnEntry(entry) ||
|
isFailedSpawnEntry(entry) ||
|
||||||
isSkippedSpawnEntry(entry)
|
isSkippedSpawnEntry(entry)
|
||||||
) {
|
) {
|
||||||
|
|
@ -328,7 +342,7 @@ function getPendingSpawnNames(params: {
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
entry != null &&
|
entry != null &&
|
||||||
entry.launchState !== 'confirmed_alive' &&
|
!isConfirmedSpawnEntry(entry) &&
|
||||||
!isFailedSpawnEntry(entry) &&
|
!isFailedSpawnEntry(entry) &&
|
||||||
!isSkippedSpawnEntry(entry)
|
!isSkippedSpawnEntry(entry)
|
||||||
);
|
);
|
||||||
|
|
@ -611,9 +625,7 @@ function getFailedSpawnDetails(params: {
|
||||||
}),
|
}),
|
||||||
] as const;
|
] as const;
|
||||||
})
|
})
|
||||||
.filter(
|
.filter(([, entry]) => isFailedSpawnEntry(entry))
|
||||||
([, entry]) => entry && (entry.launchState === 'failed_to_start' || entry.status === 'error')
|
|
||||||
)
|
|
||||||
.map(([name, entry]) => ({
|
.map(([name, entry]) => ({
|
||||||
name,
|
name,
|
||||||
reason:
|
reason:
|
||||||
|
|
|
||||||
142
src/shared/utils/teamLaunchFailureReason.ts
Normal file
142
src/shared/utils/teamLaunchFailureReason.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import type {
|
||||||
|
MemberLaunchState,
|
||||||
|
MemberSpawnStatus,
|
||||||
|
TeamAgentRuntimeDiagnosticSeverity,
|
||||||
|
TeamAgentRuntimeLivenessKind,
|
||||||
|
} from '@shared/types';
|
||||||
|
|
||||||
|
export interface ProvisionedButNotAliveLaunchEntry {
|
||||||
|
launchState?: MemberLaunchState;
|
||||||
|
status?: MemberSpawnStatus;
|
||||||
|
hardFailure?: boolean;
|
||||||
|
hardFailureReason?: string;
|
||||||
|
error?: string;
|
||||||
|
runtimeDiagnostic?: string;
|
||||||
|
runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
||||||
|
bootstrapConfirmed?: boolean;
|
||||||
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripProcessTableUnavailableDiagnosticSuffix(reason: string): string | null {
|
||||||
|
const match = /^(.*?);\s*process table (?:is )?unavailable$/i.exec(reason.trim());
|
||||||
|
const baseReason = match?.[1]?.trim();
|
||||||
|
return baseReason && baseReason.length > 0 ? baseReason : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProvisionedButNotAliveFailureReason(reason?: string): boolean {
|
||||||
|
return isCliProvisionedButNotAliveFailureReason(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCliProvisionedButNotAliveFailureReason(reason?: string): boolean {
|
||||||
|
const text = reason?.trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalizedText = stripProcessTableUnavailableDiagnosticSuffix(text) ?? text;
|
||||||
|
return /^CLI process exited \(code (?:unknown|-?\d+|\?)\)\s+[-\u2013\u2014]\s+team provisioned but not alive$/i.test(
|
||||||
|
normalizedText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mentionsProcessTableUnavailable(value: string | undefined): boolean {
|
||||||
|
return /\bprocess table\b.*\bunavailable\b/i.test(value ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasBootstrapConfirmationProofForLaunchFailure(
|
||||||
|
entry: ProvisionedButNotAliveLaunchEntry | undefined
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
entry?.bootstrapConfirmed === true ||
|
||||||
|
entry?.launchState === 'confirmed_alive' ||
|
||||||
|
entry?.livenessKind === 'confirmed_bootstrap'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProvisionedButNotAliveLaunchFailure(
|
||||||
|
entry: ProvisionedButNotAliveLaunchEntry | undefined
|
||||||
|
): boolean {
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hardFailureReason = entry.hardFailureReason?.trim();
|
||||||
|
const failureReasonMatches = hardFailureReason
|
||||||
|
? isProvisionedButNotAliveFailureReason(hardFailureReason)
|
||||||
|
: isProvisionedButNotAliveFailureReason(entry.error ?? entry.runtimeDiagnostic);
|
||||||
|
if (!failureReasonMatches) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
entry.launchState === 'failed_to_start' ||
|
||||||
|
entry.status === 'error' ||
|
||||||
|
entry.hardFailure === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBootstrapConfirmedProvisionedButNotAliveFailure(
|
||||||
|
entry: ProvisionedButNotAliveLaunchEntry | undefined
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
isProvisionedButNotAliveLaunchFailure(entry) &&
|
||||||
|
hasBootstrapConfirmationProofForLaunchFailure(entry)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasUnsafeProvisionedButNotAliveRuntimeEvidence(
|
||||||
|
entry: ProvisionedButNotAliveLaunchEntry | undefined
|
||||||
|
): boolean {
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (entry.runtimeDiagnosticSeverity === 'error') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
entry.livenessKind === 'not_found' ||
|
||||||
|
entry.livenessKind === 'shell_only' ||
|
||||||
|
entry.livenessKind === 'permission_blocked' ||
|
||||||
|
entry.livenessKind === 'runtime_process_candidate'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasProcessTableUnavailableMarker =
|
||||||
|
mentionsProcessTableUnavailable(entry.runtimeDiagnostic) ||
|
||||||
|
mentionsProcessTableUnavailable(entry.hardFailureReason) ||
|
||||||
|
mentionsProcessTableUnavailable(entry.error);
|
||||||
|
if (!entry.livenessKind) {
|
||||||
|
return !hasProcessTableUnavailableMarker;
|
||||||
|
}
|
||||||
|
if (entry.livenessKind !== 'registered_only' && entry.livenessKind !== 'stale_metadata') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !hasProcessTableUnavailableMarker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(
|
||||||
|
spawnEntry: ProvisionedButNotAliveLaunchEntry | undefined,
|
||||||
|
runtimeEntry: ProvisionedButNotAliveLaunchEntry | undefined
|
||||||
|
): boolean {
|
||||||
|
if (hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawnEntry)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!runtimeEntry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeDiagnostic = runtimeEntry.runtimeDiagnostic?.trim();
|
||||||
|
if (
|
||||||
|
!runtimeDiagnostic &&
|
||||||
|
(runtimeEntry.livenessKind == null ||
|
||||||
|
runtimeEntry.livenessKind === 'registered_only' ||
|
||||||
|
runtimeEntry.livenessKind === 'stale_metadata')
|
||||||
|
) {
|
||||||
|
return hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
runtimeDiagnostic: spawnEntry?.runtimeDiagnostic,
|
||||||
|
hardFailureReason: spawnEntry?.hardFailureReason,
|
||||||
|
error: spawnEntry?.error,
|
||||||
|
runtimeDiagnosticSeverity: runtimeEntry.runtimeDiagnosticSeverity,
|
||||||
|
livenessKind: runtimeEntry.livenessKind,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUnsafeProvisionedButNotAliveRuntimeEvidence(runtimeEntry);
|
||||||
|
}
|
||||||
|
|
@ -151,6 +151,74 @@ describe('TeamConfigReader', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('projects bootstrap-confirmed provisioned-but-not-alive launch state as settled', async () => {
|
||||||
|
const teamName = 'signal-ops';
|
||||||
|
const teamDir = path.join(tempDir, teamName);
|
||||||
|
await fs.mkdir(teamDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(teamDir, 'config.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'Signal Ops',
|
||||||
|
members: [{ name: 'team-lead', agentType: 'team-lead' }],
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(teamDir, 'launch-state.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 2,
|
||||||
|
teamName,
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const reader = new TeamConfigReader();
|
||||||
|
const teams = await reader.listTeams();
|
||||||
|
|
||||||
|
expect(teams).toHaveLength(1);
|
||||||
|
expect(teams[0]).toMatchObject({
|
||||||
|
teamName,
|
||||||
|
displayName: 'Signal Ops',
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
confirmedMemberCount: 1,
|
||||||
|
confirmedCount: 1,
|
||||||
|
failedCount: 0,
|
||||||
|
});
|
||||||
|
expect(teams[0]).not.toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not invent a partial-failure summary from artifact counts for mixed-aware teams when canonical launch truth is unavailable', async () => {
|
it('does not invent a partial-failure summary from artifact counts for mixed-aware teams when canonical launch truth is unavailable', async () => {
|
||||||
const teamName = 'mixed-aware-team';
|
const teamName = 'mixed-aware-team';
|
||||||
const teamDir = path.join(tempDir, teamName);
|
const teamDir = path.join(tempDir, teamName);
|
||||||
|
|
@ -578,16 +646,19 @@ describe('TeamConfigReader', () => {
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
let ctimeMs = 1000;
|
let ctimeMs = 1000;
|
||||||
vi.spyOn(nodeFs.promises, 'stat').mockImplementation(async () => ({
|
vi.spyOn(nodeFs.promises, 'stat').mockImplementation(
|
||||||
size: BigInt(4096),
|
async () =>
|
||||||
mode: BigInt(33188),
|
({
|
||||||
dev: BigInt(1),
|
size: BigInt(4096),
|
||||||
ino: BigInt(2),
|
mode: BigInt(33188),
|
||||||
mtimeMs: 1000,
|
dev: BigInt(1),
|
||||||
ctimeMs,
|
ino: BigInt(2),
|
||||||
birthtimeMs: 1000,
|
mtimeMs: 1000,
|
||||||
isFile: () => true,
|
ctimeMs,
|
||||||
}) as never);
|
birthtimeMs: 1000,
|
||||||
|
isFile: () => true,
|
||||||
|
}) as never
|
||||||
|
);
|
||||||
const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');
|
const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile');
|
||||||
|
|
||||||
const reader = new TeamConfigReader();
|
const reader = new TeamConfigReader();
|
||||||
|
|
@ -682,15 +753,16 @@ describe('TeamConfigReader', () => {
|
||||||
const readDeferred = createDeferred<string>();
|
const readDeferred = createDeferred<string>();
|
||||||
const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
|
const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
|
||||||
let intercepted = false;
|
let intercepted = false;
|
||||||
vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(
|
vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(((
|
||||||
((file: unknown, ...args: unknown[]) => {
|
file: unknown,
|
||||||
if (!intercepted && String(file) === configPath) {
|
...args: unknown[]
|
||||||
intercepted = true;
|
) => {
|
||||||
return readDeferred.promise as never;
|
if (!intercepted && String(file) === configPath) {
|
||||||
}
|
intercepted = true;
|
||||||
return realReadFile(file as never, ...(args as never[])) as never;
|
return readDeferred.promise as never;
|
||||||
}) as never
|
}
|
||||||
);
|
return realReadFile(file as never, ...(args as never[])) as never;
|
||||||
|
}) as never);
|
||||||
|
|
||||||
const reader = new TeamConfigReader();
|
const reader = new TeamConfigReader();
|
||||||
const staleSnapshot = reader.getConfigSnapshot(teamName);
|
const staleSnapshot = reader.getConfigSnapshot(teamName);
|
||||||
|
|
@ -730,15 +802,16 @@ describe('TeamConfigReader', () => {
|
||||||
const readDeferred = createDeferred<string>();
|
const readDeferred = createDeferred<string>();
|
||||||
const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
|
const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
|
||||||
let intercepted = false;
|
let intercepted = false;
|
||||||
vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(
|
vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(((
|
||||||
((file: unknown, ...args: unknown[]) => {
|
file: unknown,
|
||||||
if (!intercepted && String(file) === configPath) {
|
...args: unknown[]
|
||||||
intercepted = true;
|
) => {
|
||||||
return readDeferred.promise as never;
|
if (!intercepted && String(file) === configPath) {
|
||||||
}
|
intercepted = true;
|
||||||
return realReadFile(file as never, ...(args as never[])) as never;
|
return readDeferred.promise as never;
|
||||||
}) as never
|
}
|
||||||
);
|
return realReadFile(file as never, ...(args as never[])) as never;
|
||||||
|
}) as never);
|
||||||
|
|
||||||
const reader = new TeamConfigReader();
|
const reader = new TeamConfigReader();
|
||||||
const staleVerified = reader.getConfig(teamName);
|
const staleVerified = reader.getConfig(teamName);
|
||||||
|
|
@ -781,15 +854,16 @@ describe('TeamConfigReader', () => {
|
||||||
const readDeferred = createDeferred<string>();
|
const readDeferred = createDeferred<string>();
|
||||||
const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
|
const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises);
|
||||||
let intercepted = false;
|
let intercepted = false;
|
||||||
vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(
|
vi.spyOn(nodeFs.promises, 'readFile').mockImplementation(((
|
||||||
((file: unknown, ...args: unknown[]) => {
|
file: unknown,
|
||||||
if (!intercepted && String(file) === configPath) {
|
...args: unknown[]
|
||||||
intercepted = true;
|
) => {
|
||||||
return readDeferred.promise as never;
|
if (!intercepted && String(file) === configPath) {
|
||||||
}
|
intercepted = true;
|
||||||
return realReadFile(file as never, ...(args as never[])) as never;
|
return readDeferred.promise as never;
|
||||||
}) as never
|
}
|
||||||
);
|
return realReadFile(file as never, ...(args as never[])) as never;
|
||||||
|
}) as never);
|
||||||
|
|
||||||
const reader = new TeamConfigReader();
|
const reader = new TeamConfigReader();
|
||||||
const staleSnapshot = reader.getConfigSnapshot(teamName);
|
const staleSnapshot = reader.getConfigSnapshot(teamName);
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,428 @@ describe('TeamLaunchSummaryProjection', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('projects provisioned-but-not-alive failures with bootstrap proof as confirmed', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
launchSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
confirmedMemberCount: 1,
|
||||||
|
confirmedCount: 1,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
});
|
||||||
|
expect(summary).not.toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('projects Windows process-table-unavailable provisioned-but-not-alive metadata as confirmed', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
launchSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
confirmedMemberCount: 1,
|
||||||
|
confirmedCount: 1,
|
||||||
|
failedCount: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps provisioned-but-not-alive failures with runtime error evidence as failed', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
launchSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
confirmedCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reconciles unhealed launch-summary projections with bootstrap proof', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
bootstrapSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 1,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
} as never,
|
||||||
|
launchSummaryProjection: {
|
||||||
|
version: 1,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchUpdatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
expectedMemberCount: 1,
|
||||||
|
confirmedMemberCount: 0,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
confirmedMemberCount: 1,
|
||||||
|
confirmedCount: 1,
|
||||||
|
failedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
});
|
||||||
|
expect(summary).not.toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reconcile launch-summary projections from stale bootstrap proof', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
bootstrapSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:10:10.000Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:10:00.000Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:10:05.000Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:10:10.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 1,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
} as never,
|
||||||
|
launchSummaryProjection: {
|
||||||
|
version: 1,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchUpdatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
expectedMemberCount: 1,
|
||||||
|
confirmedMemberCount: 0,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
confirmedCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reconcile launch-summary projections from stopped bootstrap proof', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
bootstrapSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 1,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
} as never,
|
||||||
|
launchSummaryProjection: {
|
||||||
|
version: 1,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchUpdatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
expectedMemberCount: 1,
|
||||||
|
confirmedMemberCount: 0,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
confirmedCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps provisioned-but-not-alive failures without bootstrap proof as failed', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
launchSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
confirmedCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not project provisioned-but-not-alive from stale bootstrap proof before spawn acceptance', () => {
|
||||||
|
const summary = choosePreferredLaunchStateSummary({
|
||||||
|
bootstrapSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:10:10.000Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:10:00.000Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:10:05.000Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:10:10.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 1,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'clean_success',
|
||||||
|
} as never,
|
||||||
|
launchSnapshot: {
|
||||||
|
version: 2,
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastEvaluatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 0,
|
||||||
|
pendingCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
runtimeAlivePendingCount: 0,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
partialLaunchFailure: true,
|
||||||
|
missingMembers: ['tom'],
|
||||||
|
teamLaunchState: 'partial_failure',
|
||||||
|
confirmedCount: 0,
|
||||||
|
failedCount: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('prefers a mixed-aware persisted summary projection over a newer but poorer bootstrap snapshot', () => {
|
it('prefers a mixed-aware persisted summary projection over a newer but poorer bootstrap snapshot', () => {
|
||||||
const bootstrapSnapshot = {
|
const bootstrapSnapshot = {
|
||||||
version: 2,
|
version: 2,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function spawnEntry(overrides: Partial<MemberSpawnStatusEntry>): MemberSpawnStat
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRun(entries: Array<[string, Partial<MemberSpawnStatusEntry>]>, isLaunch = true) {
|
function buildRun(entries: [string, Partial<MemberSpawnStatusEntry>][], isLaunch = true) {
|
||||||
return {
|
return {
|
||||||
isLaunch,
|
isLaunch,
|
||||||
memberSpawnStatuses: new Map(
|
memberSpawnStatuses: new Map(
|
||||||
|
|
@ -215,6 +215,139 @@ describe('TeamProvisioningLaunchDiagnostics', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('classifies bootstrap-confirmed provisioned-but-not-alive entries as confirmed', () => {
|
||||||
|
const diagnostics = buildLaunchDiagnosticsFromRun(
|
||||||
|
buildRun([
|
||||||
|
[
|
||||||
|
'tom',
|
||||||
|
{
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
{ nowIso }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(diagnostics).toEqual([
|
||||||
|
{
|
||||||
|
id: 'tom:bootstrap_confirmed',
|
||||||
|
memberName: 'tom',
|
||||||
|
severity: 'info',
|
||||||
|
code: 'bootstrap_confirmed',
|
||||||
|
label: 'tom - bootstrap confirmed',
|
||||||
|
observedAt: NOW,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('classifies process-table-unavailable registered metadata as confirmed', () => {
|
||||||
|
const diagnostics = buildLaunchDiagnosticsFromRun(
|
||||||
|
buildRun([
|
||||||
|
[
|
||||||
|
'tom',
|
||||||
|
{
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
{ nowIso }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(diagnostics).toEqual([
|
||||||
|
{
|
||||||
|
id: 'tom:bootstrap_confirmed',
|
||||||
|
memberName: 'tom',
|
||||||
|
severity: 'info',
|
||||||
|
code: 'bootstrap_confirmed',
|
||||||
|
label: 'tom - bootstrap confirmed',
|
||||||
|
observedAt: NOW,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps error diagnostics for bootstrap-confirmed provisioned-but-not-alive entries', () => {
|
||||||
|
const diagnostics = buildLaunchDiagnosticsFromRun(
|
||||||
|
buildRun([
|
||||||
|
[
|
||||||
|
'tom',
|
||||||
|
{
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
{ nowIso }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(diagnostics).toEqual([
|
||||||
|
{
|
||||||
|
id: 'tom:bootstrap_stalled',
|
||||||
|
memberName: 'tom',
|
||||||
|
severity: 'error',
|
||||||
|
code: 'bootstrap_stalled',
|
||||||
|
label: 'tom - launch diagnostic error',
|
||||||
|
detail: 'Runtime process crashed',
|
||||||
|
observedAt: NOW,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps stopped liveness diagnostics for bootstrap-confirmed provisioned-but-not-alive entries', () => {
|
||||||
|
const diagnostics = buildLaunchDiagnosticsFromRun(
|
||||||
|
buildRun([
|
||||||
|
[
|
||||||
|
'tom',
|
||||||
|
{
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
{ nowIso }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(diagnostics).toEqual([
|
||||||
|
{
|
||||||
|
id: 'tom:bootstrap_stalled',
|
||||||
|
memberName: 'tom',
|
||||||
|
severity: 'error',
|
||||||
|
code: 'bootstrap_stalled',
|
||||||
|
label: 'tom - launch diagnostic error',
|
||||||
|
detail: 'Runtime is no longer registered',
|
||||||
|
observedAt: NOW,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('uses failed launch error when hard failure reason is absent', () => {
|
it('uses failed launch error when hard failure reason is absent', () => {
|
||||||
expect(
|
expect(
|
||||||
buildLaunchDiagnosticsFromRun(
|
buildLaunchDiagnosticsFromRun(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
isAutoClearableLaunchFailureReason,
|
isAutoClearableLaunchFailureReason,
|
||||||
isBootstrapCheckInTimeoutFailureReason,
|
isBootstrapCheckInTimeoutFailureReason,
|
||||||
isBootstrapInstructionPromptFailureReason,
|
isBootstrapInstructionPromptFailureReason,
|
||||||
|
isCliProvisionedButNotAliveFailureReason,
|
||||||
isBootstrapMcpResourceReadFailureReason,
|
isBootstrapMcpResourceReadFailureReason,
|
||||||
isConfigRegistrationFailureReason,
|
isConfigRegistrationFailureReason,
|
||||||
isLaunchCleanupBootstrapIncompleteFailureReason,
|
isLaunchCleanupBootstrapIncompleteFailureReason,
|
||||||
|
|
@ -10,9 +11,11 @@ import {
|
||||||
isNeverSpawnedDuringLaunchReason,
|
isNeverSpawnedDuringLaunchReason,
|
||||||
isOpenCodeBridgeLaunchFailureReason,
|
isOpenCodeBridgeLaunchFailureReason,
|
||||||
isProcessTableUnavailableFailureReason,
|
isProcessTableUnavailableFailureReason,
|
||||||
|
isProvisionedButNotAliveFailureReason,
|
||||||
isRegisteredRuntimeMetadataFailureReason,
|
isRegisteredRuntimeMetadataFailureReason,
|
||||||
stripProcessTableUnavailableDiagnosticSuffix,
|
stripProcessTableUnavailableDiagnosticSuffix,
|
||||||
} from '@main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy';
|
} from '@main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy';
|
||||||
|
import { isBootstrapConfirmedProvisionedButNotAliveFailure } from '@shared/utils/teamLaunchFailureReason';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('TeamProvisioningLaunchFailurePolicy', () => {
|
describe('TeamProvisioningLaunchFailurePolicy', () => {
|
||||||
|
|
@ -28,12 +31,27 @@ describe('TeamProvisioningLaunchFailurePolicy', () => {
|
||||||
'Teammate was not registered in config.json during launch. Persistent spawn failed.'
|
'Teammate was not registered in config.json during launch. Persistent spawn failed.'
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(isOpenCodeBridgeLaunchFailureReason('OpenCode bridge reported member launch failure')).toBe(
|
expect(
|
||||||
true
|
isOpenCodeBridgeLaunchFailureReason('OpenCode bridge reported member launch failure')
|
||||||
);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
isRegisteredRuntimeMetadataFailureReason('registered runtime metadata without live process')
|
isRegisteredRuntimeMetadataFailureReason('registered runtime metadata without live process')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isProvisionedButNotAliveFailureReason(
|
||||||
|
'CLI process exited (code 1) \u2014 team provisioned but not alive'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isProvisionedButNotAliveFailureReason(
|
||||||
|
'CLI process exited (code unknown) - team provisioned but not alive; process table unavailable'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isCliProvisionedButNotAliveFailureReason(
|
||||||
|
'CLI process exited (code ?) - team provisioned but not alive'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recognizes bootstrap-specific failure reasons without accepting unrelated text', () => {
|
it('recognizes bootstrap-specific failure reasons without accepting unrelated text', () => {
|
||||||
|
|
@ -42,9 +60,9 @@ describe('TeamProvisioningLaunchFailurePolicy', () => {
|
||||||
'resources/read failed for member_briefing: MCP error method not found'
|
'resources/read failed for member_briefing: MCP error method not found'
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(isBootstrapMcpResourceReadFailureReason('resources/read failed for other resource')).toBe(
|
expect(
|
||||||
false
|
isBootstrapMcpResourceReadFailureReason('resources/read failed for other resource')
|
||||||
);
|
).toBe(false);
|
||||||
expect(
|
expect(
|
||||||
isBootstrapCheckInTimeoutFailureReason(
|
isBootstrapCheckInTimeoutFailureReason(
|
||||||
'Teammate was registered but did not bootstrap-confirm before timeout.'
|
'Teammate was registered but did not bootstrap-confirm before timeout.'
|
||||||
|
|
@ -69,9 +87,9 @@ describe('TeamProvisioningLaunchFailurePolicy', () => {
|
||||||
'runtime pid could not be verified because process table is unavailable'
|
'runtime pid could not be verified because process table is unavailable'
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(isProcessTableUnavailableFailureReason('runtime failed; process table unavailable')).toBe(
|
expect(
|
||||||
false
|
isProcessTableUnavailableFailureReason('runtime failed; process table unavailable')
|
||||||
);
|
).toBe(false);
|
||||||
expect(
|
expect(
|
||||||
stripProcessTableUnavailableDiagnosticSuffix(
|
stripProcessTableUnavailableDiagnosticSuffix(
|
||||||
'Teammate did not join within the launch grace window.; process table unavailable'
|
'Teammate did not join within the launch grace window.; process table unavailable'
|
||||||
|
|
@ -80,9 +98,9 @@ describe('TeamProvisioningLaunchFailurePolicy', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps auto-clear policy narrow but accepts known recoverable suffixes', () => {
|
it('keeps auto-clear policy narrow but accepts known recoverable suffixes', () => {
|
||||||
expect(
|
expect(isAutoClearableLaunchFailureReason('Teammate was never spawned during launch.')).toBe(
|
||||||
isAutoClearableLaunchFailureReason('Teammate was never spawned during launch.')
|
true
|
||||||
).toBe(true);
|
);
|
||||||
expect(isAutoClearableLaunchFailureReason('process table is unavailable')).toBe(true);
|
expect(isAutoClearableLaunchFailureReason('process table is unavailable')).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
isAutoClearableLaunchFailureReason(
|
isAutoClearableLaunchFailureReason(
|
||||||
|
|
@ -91,11 +109,57 @@ describe('TeamProvisioningLaunchFailurePolicy', () => {
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
isAutoClearableLaunchFailureReason(
|
isAutoClearableLaunchFailureReason(
|
||||||
'CLI process exited (code 1) — team provisioned but not alive'
|
'CLI process exited (code 1) \u2014 team provisioned but not alive'
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(false);
|
||||||
expect(isAutoClearableLaunchFailureReason('model not found')).toBe(false);
|
expect(isAutoClearableLaunchFailureReason('model not found')).toBe(false);
|
||||||
expect(isAutoClearableLaunchFailureReason(undefined)).toBe(false);
|
expect(isAutoClearableLaunchFailureReason()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires bootstrap proof before treating provisioned-but-not-alive as healed', () => {
|
||||||
|
const reason = 'CLI process exited (code 1) \u2014 team provisioned but not alive';
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
hardFailure: true,
|
||||||
|
error: reason,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'model not found',
|
||||||
|
error: reason,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: reason,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
hardFailure: true,
|
||||||
|
runtimeDiagnostic: reason,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('derives member launch state by the existing precedence order', () => {
|
it('derives member launch state by the existing precedence order', () => {
|
||||||
|
|
@ -110,9 +174,7 @@ describe('TeamProvisioningLaunchFailurePolicy', () => {
|
||||||
'runtime_pending_permission'
|
'runtime_pending_permission'
|
||||||
);
|
);
|
||||||
expect(deriveMemberLaunchState({ runtimeAlive: true })).toBe('runtime_pending_bootstrap');
|
expect(deriveMemberLaunchState({ runtimeAlive: true })).toBe('runtime_pending_bootstrap');
|
||||||
expect(deriveMemberLaunchState({ agentToolAccepted: true })).toBe(
|
expect(deriveMemberLaunchState({ agentToolAccepted: true })).toBe('runtime_pending_bootstrap');
|
||||||
'runtime_pending_bootstrap'
|
|
||||||
);
|
|
||||||
expect(deriveMemberLaunchState({})).toBe('starting');
|
expect(deriveMemberLaunchState({})).toBe('starting');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { buildGeminiPostLaunchHydrationPrompt } from '@main/services/team/provisioning/TeamProvisioningPromptBuilders';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import type { MemberSpawnStatusEntry, TeamCreateRequest } from '@shared/types';
|
||||||
|
|
||||||
|
function buildPromptWithStatus(status: MemberSpawnStatusEntry): string {
|
||||||
|
return buildGeminiPostLaunchHydrationPrompt(
|
||||||
|
{
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
request: { prompt: 'Check readiness.' },
|
||||||
|
memberSpawnStatuses: new Map([['tom', status]]),
|
||||||
|
},
|
||||||
|
'lead',
|
||||||
|
[{ name: 'tom', providerId: 'anthropic', model: 'sonnet' }] as TeamCreateRequest['members'],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TeamProvisioningPromptBuilders', () => {
|
||||||
|
it('keeps errored provisioned-but-not-alive members failed in Gemini hydration prompts', () => {
|
||||||
|
const prompt = buildPromptWithStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prompt).toContain(
|
||||||
|
'- @tom: failed to start - CLI process exited (code 1) - team provisioned but not alive'
|
||||||
|
);
|
||||||
|
expect(prompt).not.toContain('- @tom: bootstrap confirmed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps benign provisioned-but-not-alive members confirmed in Gemini hydration prompts', () => {
|
||||||
|
const prompt = buildPromptWithStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prompt).toContain('- @tom: bootstrap confirmed');
|
||||||
|
expect(prompt).not.toContain('- @tom: failed to start');
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1217,6 +1217,55 @@ describe('MemberCard starting-state visuals', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps stopped provisioned-but-not-alive launches failed and retryable', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const reason = 'CLI process exited (code 1) - team provisioned but not alive';
|
||||||
|
const spawnEntry: MemberSpawnStatusEntry = {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: reason,
|
||||||
|
agentToolAccepted: true,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberCard, {
|
||||||
|
member,
|
||||||
|
memberColor: 'blue',
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
spawnStatus: 'error',
|
||||||
|
spawnLaunchState: 'failed_to_start',
|
||||||
|
spawnRuntimeAlive: false,
|
||||||
|
spawnError: reason,
|
||||||
|
spawnEntry,
|
||||||
|
onRestartMember: vi.fn(),
|
||||||
|
onSkipMemberForLaunch: vi.fn(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.querySelector('[data-testid="member-launch-failure-reason"]')).not.toBeNull();
|
||||||
|
expect(host.querySelector('[aria-label="Retry teammate"]')).not.toBeNull();
|
||||||
|
expect(host.querySelector('[aria-label="Skip for this launch"]')).not.toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('shows a compact failed launch reason on the member row with clickable links', async () => {
|
it('shows a compact failed launch reason on the member row with clickable links', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
|
|
|
||||||
|
|
@ -531,6 +531,67 @@ describe('MemberDetailDialog activity count', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows Relaunch OpenCode copy for unsafe provisioned-but-not-alive OpenCode teammates without runtime evidence', async () => {
|
||||||
|
const member: ResolvedTeamMember = {
|
||||||
|
name: 'jack',
|
||||||
|
status: 'active',
|
||||||
|
currentTaskId: null,
|
||||||
|
taskCount: 0,
|
||||||
|
lastActiveAt: null,
|
||||||
|
messageCount: 0,
|
||||||
|
providerId: 'opencode',
|
||||||
|
};
|
||||||
|
const onRestartMember = vi.fn(() => Promise.resolve(undefined));
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberDetailDialog, {
|
||||||
|
open: true,
|
||||||
|
member,
|
||||||
|
teamName: 'demo-team',
|
||||||
|
members: [member],
|
||||||
|
tasks: [],
|
||||||
|
isTeamAlive: true,
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
onClose: () => undefined,
|
||||||
|
onSendMessage: () => undefined,
|
||||||
|
onAssignTask: () => undefined,
|
||||||
|
onTaskClick: () => undefined,
|
||||||
|
onRestartMember,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain(
|
||||||
|
'No OpenCode runtime session was recorded. Relaunch this teammate to start a fresh OpenCode session.'
|
||||||
|
);
|
||||||
|
const relaunchButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
button.textContent?.includes('Relaunch OpenCode')
|
||||||
|
);
|
||||||
|
expect(relaunchButton).not.toBeUndefined();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('shows Relaunch OpenCode copy for stalled OpenCode bootstrap', async () => {
|
it('shows Relaunch OpenCode copy for stalled OpenCode bootstrap', async () => {
|
||||||
const member: ResolvedTeamMember = {
|
const member: ResolvedTeamMember = {
|
||||||
name: 'tom',
|
name: 'tom',
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,12 @@ import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { MemberSpawnStatusEntry, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
|
import type {
|
||||||
|
MemberSpawnStatusEntry,
|
||||||
|
ResolvedTeamMember,
|
||||||
|
TeamAgentRuntimeEntry,
|
||||||
|
TeamTaskWithKanban,
|
||||||
|
} from '@shared/types';
|
||||||
|
|
||||||
vi.mock('@renderer/components/team/members/MemberCard', () => ({
|
vi.mock('@renderer/components/team/members/MemberCard', () => ({
|
||||||
MemberCard: ({
|
MemberCard: ({
|
||||||
|
|
@ -115,6 +120,19 @@ function offlineSpawnStatus(): MemberSpawnStatusEntry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function provisionedButNotAliveSpawnStatus(): MemberSpawnStatusEntry {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function activeTask(id = 'task-active'): TeamTaskWithKanban {
|
function activeTask(id = 'task-active'): TeamTaskWithKanban {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|
@ -543,6 +561,142 @@ describe('MemberList spawn-status memoization', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps tasks visible and suppresses launch actions for healed provisioned-but-not-alive status', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const task = activeTask();
|
||||||
|
const members: ResolvedTeamMember[] = [{ ...member, currentTaskId: task.id }];
|
||||||
|
const restart = vi.fn();
|
||||||
|
const skip = vi.fn();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberList, {
|
||||||
|
members,
|
||||||
|
isTeamAlive: true,
|
||||||
|
taskMap: new Map([[task.id, task]]),
|
||||||
|
memberSpawnStatuses: new Map([['bob', provisionedButNotAliveSpawnStatus()]]),
|
||||||
|
memberRuntimeEntries: new Map<string, TeamAgentRuntimeEntry>([
|
||||||
|
[
|
||||||
|
'bob',
|
||||||
|
{
|
||||||
|
memberName: 'bob',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:05.411Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
onRestartMember: restart,
|
||||||
|
onSkipMemberForLaunch: skip,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.querySelector('[data-testid="current-bob"]')?.textContent).toBe(task.id);
|
||||||
|
expect(host.querySelector('[data-testid="retry-bob"]')).toBeNull();
|
||||||
|
expect(host.querySelector('[data-testid="skip-bob"]')).toBeNull();
|
||||||
|
expect(host.textContent).not.toContain('team provisioned but not alive');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps stopped provisioned-but-not-alive status failed and actionable', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const task = activeTask();
|
||||||
|
const members: ResolvedTeamMember[] = [{ ...member, currentTaskId: task.id }];
|
||||||
|
const restart = vi.fn();
|
||||||
|
const skip = vi.fn();
|
||||||
|
const spawnEntry = {
|
||||||
|
...provisionedButNotAliveSpawnStatus(),
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
} satisfies MemberSpawnStatusEntry;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberList, {
|
||||||
|
members,
|
||||||
|
isTeamAlive: true,
|
||||||
|
taskMap: new Map([[task.id, task]]),
|
||||||
|
memberSpawnStatuses: new Map([['bob', spawnEntry]]),
|
||||||
|
onRestartMember: restart,
|
||||||
|
onSkipMemberForLaunch: skip,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.querySelector('[data-testid="current-bob"]')).toBeNull();
|
||||||
|
expect(host.querySelector('[data-testid="retry-bob"]')).not.toBeNull();
|
||||||
|
expect(host.querySelector('[data-testid="skip-bob"]')).not.toBeNull();
|
||||||
|
expect(host.textContent).toContain('team provisioned but not alive');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides tasks for healed provisioned-but-not-alive status when runtime has an error', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const task = activeTask();
|
||||||
|
const members: ResolvedTeamMember[] = [{ ...member, currentTaskId: task.id }];
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MemberList, {
|
||||||
|
members,
|
||||||
|
isTeamAlive: true,
|
||||||
|
taskMap: new Map([[task.id, task]]),
|
||||||
|
memberSpawnStatuses: new Map([['bob', provisionedButNotAliveSpawnStatus()]]),
|
||||||
|
memberRuntimeEntries: new Map<string, TeamAgentRuntimeEntry>([
|
||||||
|
[
|
||||||
|
'bob',
|
||||||
|
{
|
||||||
|
memberName: 'bob',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-05-25T20:14:05.411Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.querySelector('[data-testid="current-bob"]')).toBeNull();
|
||||||
|
expect(host.querySelector('[data-testid="retry-bob"]')).toBeNull();
|
||||||
|
expect(host.querySelector('[data-testid="skip-bob"]')).toBeNull();
|
||||||
|
expect(host.textContent).toContain('team provisioned but not alive');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('passes skip callbacks to failed member cards and rerenders when the callback changes', async () => {
|
it('passes skip callbacks to failed member cards and rerenders when the callback changes', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,208 @@ describe('getLaunchJoinMilestonesFromMembers', () => {
|
||||||
expect(milestones.pendingSpawnCount).toBe(3);
|
expect(milestones.pendingSpawnCount).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('counts bootstrap-confirmed provisioned-but-not-alive entries as joined', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members: [{ name: 'tom' }],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
tom: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(1);
|
||||||
|
expect(milestones.failedSpawnCount).toBe(0);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses spawn process-table proof when runtime registered metadata has no diagnostic text', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members: [{ name: 'tom' }],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
tom: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(1);
|
||||||
|
expect(milestones.failedSpawnCount).toBe(0);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses spawn process-table proof when runtime metadata has no liveness or diagnostic text', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members: [{ name: 'tom' }],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
tom: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(1);
|
||||||
|
expect(milestones.failedSpawnCount).toBe(0);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts unsafe bootstrap-confirmed provisioned-but-not-alive entries as failed', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members: [{ name: 'tom' }],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(0);
|
||||||
|
expect(milestones.failedSpawnCount).toBe(1);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps ambiguous runtime-offline entries pending even when provisioned-but-not-alive spawn evidence is safe', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members: [{ name: 'tom' }],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
tom: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
runtimeDiagnostic: 'Runtime heartbeat is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(0);
|
||||||
|
expect(milestones.failedSpawnCount).toBe(0);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not count safe provisioned-but-not-alive spawn evidence as joined when live runtime evidence is an error', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members: [{ name: 'tom' }],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
tom: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(0);
|
||||||
|
expect(milestones.failedSpawnCount).toBe(0);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not let a stale clean snapshot hide live registered-only members', () => {
|
it('does not let a stale clean snapshot hide live registered-only members', () => {
|
||||||
const milestones = getLaunchJoinMilestonesFromMembers({
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
members,
|
members,
|
||||||
|
|
@ -243,4 +445,131 @@ describe('getLaunchJoinMilestonesFromMembers', () => {
|
||||||
expect(milestones.pendingSpawnCount).toBe(1);
|
expect(milestones.pendingSpawnCount).toBe(1);
|
||||||
expect(milestones.expectedTeammateCount).toBe(4);
|
expect(milestones.expectedTeammateCount).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not count confirmed spawn as joined when spawn metadata carries runtime error evidence', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members,
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
alice: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
bob: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-04-24T12:00:01.000Z',
|
||||||
|
},
|
||||||
|
tom: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
jane: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(3);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(1);
|
||||||
|
expect(milestones.expectedTeammateCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not count confirmed spawn as joined when stopped spawn metadata has no liveness kind', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members,
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
alice: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
bob: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:01.000Z',
|
||||||
|
},
|
||||||
|
tom: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
jane: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(3);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(1);
|
||||||
|
expect(milestones.expectedTeammateCount).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts process-table-unavailable provisioned-but-not-alive spawn without liveness kind as joined', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members,
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
alice: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
bob: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
updatedAt: '2026-04-24T12:00:01.000Z',
|
||||||
|
},
|
||||||
|
tom: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
jane: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(4);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(0);
|
||||||
|
expect(milestones.failedSpawnCount).toBe(0);
|
||||||
|
expect(milestones.expectedTeammateCount).toBe(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ function createRuntimeSnapshot(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSpawnStatus(overrides: Partial<MemberSpawnStatusEntry> = {}): MemberSpawnStatusEntry {
|
function createSpawnStatus(
|
||||||
|
overrides: Partial<MemberSpawnStatusEntry> = {}
|
||||||
|
): MemberSpawnStatusEntry {
|
||||||
return {
|
return {
|
||||||
status: 'spawning',
|
status: 'spawning',
|
||||||
launchState: 'starting',
|
launchState: 'starting',
|
||||||
|
|
@ -251,6 +253,274 @@ describe('buildTeamRuntimeDisplayRows', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not degrade bootstrap-confirmed provisioned-but-not-alive rows', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
runtimeSnapshot: createRuntimeSnapshot({
|
||||||
|
alice: createRuntimeEntry({
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'running',
|
||||||
|
source: 'mixed',
|
||||||
|
stateReason: 'Bootstrap confirmed',
|
||||||
|
diagnosticSeverity: 'warning',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not degrade Windows process-table-unavailable registered metadata rows', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
runtimeSnapshot: createRuntimeSnapshot({
|
||||||
|
alice: createRuntimeEntry({
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'running',
|
||||||
|
source: 'mixed',
|
||||||
|
stateReason: 'Bootstrap confirmed',
|
||||||
|
diagnosticSeverity: 'warning',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses spawn process-table proof when runtime registered metadata has no diagnostic text', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
runtimeSnapshot: createRuntimeSnapshot({
|
||||||
|
alice: createRuntimeEntry({
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'running',
|
||||||
|
source: 'mixed',
|
||||||
|
stateReason: 'Bootstrap confirmed',
|
||||||
|
diagnosticSeverity: 'warning',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not let stale provisioned-but-not-alive spawn evidence hide runtime errors', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
runtimeSnapshot: createRuntimeSnapshot({
|
||||||
|
alice: createRuntimeEntry({
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'degraded',
|
||||||
|
source: 'mixed',
|
||||||
|
stateReason: 'Runtime process crashed',
|
||||||
|
diagnosticSeverity: 'error',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not let provisioned-but-not-alive spawn evidence hide stopped runtime evidence', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
runtimeSnapshot: createRuntimeSnapshot({
|
||||||
|
alice: createRuntimeEntry({
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime metadata was not found',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'stopped',
|
||||||
|
source: 'mixed',
|
||||||
|
stateReason: 'Runtime metadata was not found',
|
||||||
|
diagnosticSeverity: 'warning',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not let stopped provisioned-but-not-alive spawn evidence hide live runtime context', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
runtimeSnapshot: createRuntimeSnapshot({
|
||||||
|
alice: createRuntimeEntry({
|
||||||
|
alive: true,
|
||||||
|
livenessKind: 'runtime_process',
|
||||||
|
runtimeDiagnostic: 'Runtime process is alive',
|
||||||
|
runtimeDiagnosticSeverity: 'info',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'degraded',
|
||||||
|
source: 'mixed',
|
||||||
|
stateReason: 'Runtime is no longer registered. Process is still alive.',
|
||||||
|
diagnosticSeverity: 'warning',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps spawn-only runtime errors visible for provisioned-but-not-alive entries', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'degraded',
|
||||||
|
source: 'spawn-status',
|
||||||
|
stateReason: 'Runtime process crashed',
|
||||||
|
diagnosticSeverity: 'error',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps spawn-only stopped liveness visible for provisioned-but-not-alive entries', () => {
|
||||||
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
spawnStatuses: {
|
||||||
|
alice: createSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
memberName: 'alice',
|
||||||
|
state: 'degraded',
|
||||||
|
source: 'spawn-status',
|
||||||
|
stateReason: 'Runtime is no longer registered',
|
||||||
|
diagnosticSeverity: 'warning',
|
||||||
|
actionsAllowed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('degrades spawn-only rows when online process evidence has stalled bootstrap', () => {
|
it('degrades spawn-only rows when online process evidence has stalled bootstrap', () => {
|
||||||
const rows = buildTeamRuntimeDisplayRows({
|
const rows = buildTeamRuntimeDisplayRows({
|
||||||
members: [{ name: 'alice' }],
|
members: [{ name: 'alice' }],
|
||||||
|
|
|
||||||
|
|
@ -1777,6 +1777,154 @@ describe('TeamGraphAdapter particles', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps bootstrap-confirmed spawn diagnostic errors in graph error state', () => {
|
||||||
|
const adapter = TeamGraphAdapter.create();
|
||||||
|
const graph = adapter.adapt(createBaseTeamData(), 'my-team', {
|
||||||
|
alice: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findNode(graph, 'member:my-team:alice')).toMatchObject({
|
||||||
|
state: 'error',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
launchVisualState: 'error',
|
||||||
|
launchStatusLabel: 'failed',
|
||||||
|
exceptionTone: 'error',
|
||||||
|
exceptionLabel: 'spawn failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps bootstrap-confirmed stopped runtime evidence in graph error state', () => {
|
||||||
|
const adapter = TeamGraphAdapter.create();
|
||||||
|
const graph = adapter.adapt(createBaseTeamData(), 'my-team', {
|
||||||
|
alice: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findNode(graph, 'member:my-team:alice')).toMatchObject({
|
||||||
|
state: 'error',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
launchVisualState: 'error',
|
||||||
|
launchStatusLabel: 'failed',
|
||||||
|
exceptionTone: 'error',
|
||||||
|
exceptionLabel: 'spawn failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses spawn process-table proof when graph runtime metadata has no diagnostic text', () => {
|
||||||
|
const adapter = TeamGraphAdapter.create();
|
||||||
|
const graph = adapter.adapt(
|
||||||
|
createBaseTeamData({
|
||||||
|
runtimeEntriesByMember: {
|
||||||
|
alice: createLiveRuntimeEntry('alice', {
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'my-team',
|
||||||
|
{
|
||||||
|
alice: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(findNode(graph, 'member:my-team:alice')).toMatchObject({
|
||||||
|
state: 'active',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
launchVisualState: undefined,
|
||||||
|
launchStatusLabel: undefined,
|
||||||
|
exceptionTone: undefined,
|
||||||
|
exceptionLabel: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: 'runtime diagnostic error',
|
||||||
|
runtime: {
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'runtime_process',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stopped runtime liveness',
|
||||||
|
runtime: {
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const)(
|
||||||
|
'keeps graph errors when live runtime has unsafe evidence for a safe bootstrap-confirmed spawn: $name',
|
||||||
|
({ runtime }) => {
|
||||||
|
const adapter = TeamGraphAdapter.create();
|
||||||
|
const graph = adapter.adapt(
|
||||||
|
createBaseTeamData({
|
||||||
|
runtimeEntriesByMember: {
|
||||||
|
alice: createLiveRuntimeEntry('alice', runtime),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'my-team',
|
||||||
|
{
|
||||||
|
alice: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(findNode(graph, 'member:my-team:alice')).toMatchObject({
|
||||||
|
state: 'error',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
exceptionTone: 'error',
|
||||||
|
exceptionLabel: 'spawn failed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('treats permission-blocked spawn state as awaiting approval even without pending approval feed', () => {
|
it('treats permission-blocked spawn state as awaiting approval even without pending approval feed', () => {
|
||||||
const adapter = TeamGraphAdapter.create();
|
const adapter = TeamGraphAdapter.create();
|
||||||
const teamData = createBaseTeamData();
|
const teamData = createBaseTeamData();
|
||||||
|
|
|
||||||
|
|
@ -419,6 +419,8 @@ describe('teamSlice actions', () => {
|
||||||
expect(fetchTeams).toHaveBeenCalledTimes(1);
|
expect(fetchTeams).toHaveBeenCalledTimes(1);
|
||||||
expect(refreshTeamData).toHaveBeenCalledTimes(1);
|
expect(refreshTeamData).toHaveBeenCalledTimes(1);
|
||||||
expect(refreshTeamData).toHaveBeenCalledWith('my-team', { withDedup: true });
|
expect(refreshTeamData).toHaveBeenCalledWith('my-team', { withDedup: true });
|
||||||
|
expect(hoisted.getMemberSpawnStatuses).toHaveBeenCalledWith('my-team');
|
||||||
|
expect(hoisted.getTeamAgentRuntime).toHaveBeenCalledWith('my-team');
|
||||||
|
|
||||||
const snapshot = getTeamRefreshFanoutSnapshotForTests(
|
const snapshot = getTeamRefreshFanoutSnapshotForTests(
|
||||||
'my-team'
|
'my-team'
|
||||||
|
|
@ -431,6 +433,16 @@ describe('teamSlice actions', () => {
|
||||||
'provisioning-progress:provisioning:terminal-ready:refreshTeamData:scheduled'
|
'provisioning-progress:provisioning:terminal-ready:refreshTeamData:scheduled'
|
||||||
]
|
]
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
|
expect(
|
||||||
|
snapshot?.counts[
|
||||||
|
'provisioning-progress:provisioning:terminal-ready:fetchMemberSpawnStatuses:scheduled'
|
||||||
|
]
|
||||||
|
).toBe(1);
|
||||||
|
expect(
|
||||||
|
snapshot?.counts[
|
||||||
|
'provisioning-progress:provisioning:terminal-ready:fetchTeamAgentRuntime:scheduled'
|
||||||
|
]
|
||||||
|
).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps inbox verify failure to user-friendly text', async () => {
|
it('maps inbox verify failure to user-friendly text', async () => {
|
||||||
|
|
@ -6396,6 +6408,84 @@ describe('teamSlice actions', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('refreshes retained terminal spawn errors after disconnected progress', async () => {
|
||||||
|
const store = createSliceStore();
|
||||||
|
const startedAt = '2026-03-12T10:00:00.000Z';
|
||||||
|
const staleReason = 'CLI process exited (code 1) \u2014 team provisioned but not alive';
|
||||||
|
store.setState({
|
||||||
|
selectedTeamName: 'my-team',
|
||||||
|
selectedTeamData: createTeamSnapshot(),
|
||||||
|
paneLayout: {
|
||||||
|
focusedPaneId: 'pane-default',
|
||||||
|
panes: [
|
||||||
|
{
|
||||||
|
id: 'pane-default',
|
||||||
|
widthFraction: 1,
|
||||||
|
tabs: [{ id: 'team-my-team', type: 'team', teamName: 'my-team', label: 'My Team' }],
|
||||||
|
activeTabId: 'team-my-team',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
currentProvisioningRunIdByTeam: {
|
||||||
|
'my-team': 'run-current',
|
||||||
|
},
|
||||||
|
currentRuntimeRunIdByTeam: {
|
||||||
|
'my-team': 'run-current',
|
||||||
|
},
|
||||||
|
memberSpawnStatusesByTeam: {
|
||||||
|
'my-team': {
|
||||||
|
tom: createMemberSpawnStatus({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
error: staleReason,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: staleReason,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
hoisted.getMemberSpawnStatuses.mockResolvedValue(
|
||||||
|
createMemberSpawnSnapshot({
|
||||||
|
runId: 'run-current',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
statuses: {
|
||||||
|
tom: createMemberSpawnStatus({
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: false,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
hardFailure: false,
|
||||||
|
hardFailureReason: undefined,
|
||||||
|
error: undefined,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
store.getState().onProvisioningProgress({
|
||||||
|
runId: 'run-current',
|
||||||
|
teamName: 'my-team',
|
||||||
|
state: 'disconnected',
|
||||||
|
message: 'Disconnected',
|
||||||
|
startedAt,
|
||||||
|
updatedAt: '2026-03-12T10:00:01.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(store.getState().memberSpawnStatusesByTeam['my-team']?.tom).toMatchObject({
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
store.getState().memberSpawnStatusesByTeam['my-team']?.tom?.hardFailureReason
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not fall back to a team-wide latest run when no current run is pinned', () => {
|
it('does not fall back to a team-wide latest run when no current run is pinned', () => {
|
||||||
expect(
|
expect(
|
||||||
getCurrentProvisioningProgressForTeam(
|
getCurrentProvisioningProgressForTeam(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ import {
|
||||||
shouldDisplayMemberCurrentTask,
|
shouldDisplayMemberCurrentTask,
|
||||||
} from '@renderer/utils/memberHelpers';
|
} from '@renderer/utils/memberHelpers';
|
||||||
|
|
||||||
import type { ResolvedTeamMember } from '@shared/types';
|
import type {
|
||||||
|
MemberSpawnStatusEntry,
|
||||||
|
ResolvedTeamMember,
|
||||||
|
TeamAgentRuntimeEntry,
|
||||||
|
} from '@shared/types';
|
||||||
|
|
||||||
const member: ResolvedTeamMember = {
|
const member: ResolvedTeamMember = {
|
||||||
name: 'alice',
|
name: 'alice',
|
||||||
|
|
@ -27,6 +31,28 @@ const member: ResolvedTeamMember = {
|
||||||
removedAt: undefined,
|
removedAt: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const provisionedButNotAliveSpawn: MemberSpawnStatusEntry = {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
};
|
||||||
|
|
||||||
|
const processTableUnavailableRuntime: TeamAgentRuntimeEntry = {
|
||||||
|
memberName: 'alice',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
providerId: 'anthropic',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:05.411Z',
|
||||||
|
};
|
||||||
|
|
||||||
describe('memberHelpers spawn-aware presence', () => {
|
describe('memberHelpers spawn-aware presence', () => {
|
||||||
it('does not display current task labels for offline or terminal launch states', () => {
|
it('does not display current task labels for offline or terminal launch states', () => {
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -121,6 +147,92 @@ describe('memberHelpers spawn-aware presence', () => {
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats bootstrap-confirmed provisioned-but-not-alive entries as active for task display', () => {
|
||||||
|
expect(
|
||||||
|
shouldDisplayMemberCurrentTask({
|
||||||
|
member: { ...member, currentTaskId: 'task-1' },
|
||||||
|
isTeamAlive: true,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnEntry: provisionedButNotAliveSpawn,
|
||||||
|
runtimeEntry: processTableUnavailableRuntime,
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats spawn-only bootstrap-confirmed provisioned-but-not-alive entries as active for task display', () => {
|
||||||
|
expect(
|
||||||
|
shouldDisplayMemberCurrentTask({
|
||||||
|
member: { ...member, currentTaskId: 'task-1' },
|
||||||
|
isTeamAlive: true,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnEntry: provisionedButNotAliveSpawn,
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show task activity for provisioned-but-not-alive entries with runtime errors', () => {
|
||||||
|
expect(
|
||||||
|
shouldDisplayMemberCurrentTask({
|
||||||
|
member: { ...member, currentTaskId: 'task-1' },
|
||||||
|
isTeamAlive: true,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnEntry: provisionedButNotAliveSpawn,
|
||||||
|
runtimeEntry: {
|
||||||
|
...processTableUnavailableRuntime,
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shouldDisplayMemberCurrentTask({
|
||||||
|
member: { ...member, currentTaskId: 'task-1' },
|
||||||
|
isTeamAlive: true,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnEntry: {
|
||||||
|
...provisionedButNotAliveSpawn,
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
},
|
||||||
|
runtimeEntry: processTableUnavailableRuntime,
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show task activity for unsafe provisioned-but-not-alive runtime candidates', () => {
|
||||||
|
expect(
|
||||||
|
shouldDisplayMemberCurrentTask({
|
||||||
|
member: { ...member, currentTaskId: 'task-1' },
|
||||||
|
isTeamAlive: true,
|
||||||
|
spawnStatus: 'online',
|
||||||
|
spawnLaunchState: 'confirmed_alive',
|
||||||
|
spawnRuntimeAlive: true,
|
||||||
|
spawnEntry: {
|
||||||
|
...provisionedButNotAliveSpawn,
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
},
|
||||||
|
runtimeEntry: {
|
||||||
|
...processTableUnavailableRuntime,
|
||||||
|
alive: false,
|
||||||
|
livenessKind: 'runtime_process_candidate',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'OpenCode runtime process detected, but teammate bootstrap is not confirmed',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows process-online teammates as online with a green dot', () => {
|
it('shows process-online teammates as online with a green dot', () => {
|
||||||
expect(
|
expect(
|
||||||
getSpawnAwarePresenceLabel(
|
getSpawnAwarePresenceLabel(
|
||||||
|
|
@ -657,6 +769,44 @@ describe('memberHelpers spawn-aware presence', () => {
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks unsafe provisioned-but-not-alive OpenCode entries as relaunchable', () => {
|
||||||
|
expect(
|
||||||
|
isOpenCodeRelaunchActionable({
|
||||||
|
member: { ...member, providerId: 'opencode' },
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isOpenCodeRelaunchActionable({
|
||||||
|
member: { ...member, providerId: 'opencode' },
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not mark fresh OpenCode runtime candidates as relaunchable', () => {
|
it('does not mark fresh OpenCode runtime candidates as relaunchable', () => {
|
||||||
expect(
|
expect(
|
||||||
isOpenCodeRelaunchActionable({
|
isOpenCodeRelaunchActionable({
|
||||||
|
|
@ -780,6 +930,214 @@ describe('memberHelpers spawn-aware presence', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not render bootstrap-confirmed provisioned-but-not-alive entries as failed or stale', () => {
|
||||||
|
expect(
|
||||||
|
buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed,
|
||||||
|
spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled,
|
||||||
|
spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted,
|
||||||
|
spawnHardFailure: provisionedButNotAliveSpawn.hardFailure,
|
||||||
|
spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason,
|
||||||
|
spawnError: provisionedButNotAliveSpawn.error,
|
||||||
|
spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind,
|
||||||
|
runtimeEntry: processTableUnavailableRuntime,
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
presenceLabel: 'idle',
|
||||||
|
launchVisualState: null,
|
||||||
|
launchStatusLabel: null,
|
||||||
|
spawnBadgeLabel: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render spawn-only bootstrap-confirmed provisioned-but-not-alive entries as failed or stale', () => {
|
||||||
|
expect(
|
||||||
|
buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed,
|
||||||
|
spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled,
|
||||||
|
spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted,
|
||||||
|
spawnHardFailure: provisionedButNotAliveSpawn.hardFailure,
|
||||||
|
spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason,
|
||||||
|
spawnError: provisionedButNotAliveSpawn.error,
|
||||||
|
spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind,
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
presenceLabel: 'idle',
|
||||||
|
launchVisualState: null,
|
||||||
|
launchStatusLabel: null,
|
||||||
|
spawnBadgeLabel: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not leak safe process-table liveness into healed member visuals', () => {
|
||||||
|
expect(
|
||||||
|
buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed,
|
||||||
|
spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled,
|
||||||
|
spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted,
|
||||||
|
spawnHardFailure: provisionedButNotAliveSpawn.hardFailure,
|
||||||
|
spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason,
|
||||||
|
spawnError: provisionedButNotAliveSpawn.error,
|
||||||
|
spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind,
|
||||||
|
runtimeEntry: {
|
||||||
|
...processTableUnavailableRuntime,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
},
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
presenceLabel: 'idle',
|
||||||
|
launchVisualState: null,
|
||||||
|
launchStatusLabel: null,
|
||||||
|
spawnBadgeLabel: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recognizes provisioned-but-not-alive when the reason is only in runtime diagnostics', () => {
|
||||||
|
expect(
|
||||||
|
buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed,
|
||||||
|
spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled,
|
||||||
|
spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted,
|
||||||
|
spawnHardFailure: provisionedButNotAliveSpawn.hardFailure,
|
||||||
|
spawnRuntimeDiagnostic: provisionedButNotAliveSpawn.hardFailureReason,
|
||||||
|
spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind,
|
||||||
|
runtimeEntry: processTableUnavailableRuntime,
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
presenceLabel: 'idle',
|
||||||
|
launchVisualState: null,
|
||||||
|
launchStatusLabel: null,
|
||||||
|
spawnBadgeLabel: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps runtime errors visible for bootstrap-confirmed provisioned-but-not-alive entries', () => {
|
||||||
|
expect(
|
||||||
|
buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed,
|
||||||
|
spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled,
|
||||||
|
spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted,
|
||||||
|
spawnHardFailure: provisionedButNotAliveSpawn.hardFailure,
|
||||||
|
spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason,
|
||||||
|
spawnError: provisionedButNotAliveSpawn.error,
|
||||||
|
spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity: provisionedButNotAliveSpawn.runtimeDiagnosticSeverity,
|
||||||
|
runtimeEntry: {
|
||||||
|
...processTableUnavailableRuntime,
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
},
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
presenceLabel: 'spawn failed',
|
||||||
|
launchVisualState: 'error',
|
||||||
|
launchStatusLabel: 'failed',
|
||||||
|
spawnBadgeLabel: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps spawn diagnostic errors visible for bootstrap-confirmed provisioned-but-not-alive entries', () => {
|
||||||
|
expect(
|
||||||
|
buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed,
|
||||||
|
spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled,
|
||||||
|
spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted,
|
||||||
|
spawnHardFailure: provisionedButNotAliveSpawn.hardFailure,
|
||||||
|
spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason,
|
||||||
|
spawnError: provisionedButNotAliveSpawn.error,
|
||||||
|
spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind,
|
||||||
|
spawnRuntimeDiagnosticSeverity: 'error',
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
presenceLabel: 'spawn failed',
|
||||||
|
launchVisualState: 'error',
|
||||||
|
launchStatusLabel: 'failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps stopped runtime evidence failed for bootstrap-confirmed provisioned-but-not-alive entries', () => {
|
||||||
|
expect(
|
||||||
|
buildMemberLaunchPresentation({
|
||||||
|
member,
|
||||||
|
spawnStatus: provisionedButNotAliveSpawn.status,
|
||||||
|
spawnLaunchState: provisionedButNotAliveSpawn.launchState,
|
||||||
|
spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource,
|
||||||
|
spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive,
|
||||||
|
spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed,
|
||||||
|
spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled,
|
||||||
|
spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted,
|
||||||
|
spawnHardFailure: provisionedButNotAliveSpawn.hardFailure,
|
||||||
|
spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason,
|
||||||
|
spawnError: provisionedButNotAliveSpawn.error,
|
||||||
|
spawnLivenessKind: 'not_found',
|
||||||
|
spawnRuntimeDiagnosticSeverity: 'warning',
|
||||||
|
runtimeAdvisory: undefined,
|
||||||
|
isLaunchSettling: false,
|
||||||
|
isTeamAlive: true,
|
||||||
|
isTeamProvisioning: false,
|
||||||
|
})
|
||||||
|
).toMatchObject({
|
||||||
|
presenceLabel: 'spawn failed',
|
||||||
|
launchVisualState: 'error',
|
||||||
|
launchStatusLabel: 'failed',
|
||||||
|
spawnBadgeLabel: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('renders unified retry advisory labels for provider retries', () => {
|
it('renders unified retry advisory labels for provider retries', () => {
|
||||||
expect(
|
expect(
|
||||||
getMemberRuntimeAdvisoryLabel(
|
getMemberRuntimeAdvisoryLabel(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
buildMemberLaunchDiagnosticsPayload,
|
buildMemberLaunchDiagnosticsPayload,
|
||||||
|
buildTeamMemberLaunchDiagnosticsPayloads,
|
||||||
formatMemberLaunchDiagnosticsPayload,
|
formatMemberLaunchDiagnosticsPayload,
|
||||||
getMemberLaunchDiagnosticsErrorMessage,
|
getMemberLaunchDiagnosticsErrorMessage,
|
||||||
hasMemberLaunchDiagnosticsDetails,
|
hasMemberLaunchDiagnosticsDetails,
|
||||||
|
|
@ -123,6 +124,360 @@ describe('member launch diagnostics', () => {
|
||||||
expect(payload.runtimeDiagnostic).toBe('persisted runtime pid is not alive');
|
expect(payload.runtimeDiagnostic).toBe('persisted runtime pid is not alive');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not surface bootstrap-confirmed provisioned-but-not-alive entries as card errors', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
runtimeEntry: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
spawnStatus: 'online',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
});
|
||||||
|
expect(payload.memberCardError).toBeUndefined();
|
||||||
|
expect(payload.probableCause).toBeUndefined();
|
||||||
|
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(false);
|
||||||
|
expect(getMemberLaunchDiagnosticsErrorMessage(payload)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not surface spawn-only safe bootstrap-confirmed provisioned-but-not-alive entries as card errors', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
spawnStatus: 'online',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
});
|
||||||
|
expect(payload.memberCardError).toBeUndefined();
|
||||||
|
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(false);
|
||||||
|
expect(getMemberLaunchDiagnosticsErrorMessage(payload)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps runtime errors visible for bootstrap-confirmed provisioned-but-not-alive entries', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
runtimeEntry: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
memberCardError: 'Runtime process crashed',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
});
|
||||||
|
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
|
||||||
|
expect(getMemberLaunchDiagnosticsErrorMessage(payload)).toBe('Runtime process crashed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps spawn errors visible when runtime evidence is only warning severity', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
runtimeEntry: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
runtimeAlive: false,
|
||||||
|
hardFailure: true,
|
||||||
|
memberCardError: 'Runtime process crashed',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
});
|
||||||
|
expect(payload.diagnostics).toContain('Runtime process crashed');
|
||||||
|
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps spawn diagnostics for bootstrap-confirmed provisioned-but-not-alive entries without runtime evidence', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
memberCardError: 'Runtime process crashed',
|
||||||
|
runtimeDiagnostic: 'Runtime process crashed',
|
||||||
|
runtimeDiagnosticSeverity: 'error',
|
||||||
|
});
|
||||||
|
expect(payload.diagnostics).toContain('Runtime process crashed');
|
||||||
|
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not heal stopped liveness evidence for bootstrap-confirmed provisioned-but-not-alive entries', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z',
|
||||||
|
lastHeartbeatAt: '2026-05-25T20:13:56.110Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
runtimeAlive: false,
|
||||||
|
hardFailure: true,
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps unsafe spawn diagnostics over benign runtime warnings for provisioned-but-not-alive entries', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
runtimeEntry: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
spawnStatus: 'error',
|
||||||
|
runtimeAlive: false,
|
||||||
|
hardFailure: true,
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
});
|
||||||
|
expect(payload.diagnostics).toContain('Runtime is no longer registered');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers stopped runtime liveness over stale spawn liveness in copy diagnostics', () => {
|
||||||
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5',
|
||||||
|
memberName: 'tom',
|
||||||
|
spawnEntry: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
runtimeEntry: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeAlive: false,
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers newer healed snapshots over unsafe live provisioned-but-not-alive diagnostics', () => {
|
||||||
|
const [payload] = buildTeamMemberLaunchDiagnosticsPayloads({
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
runId: 'run-42',
|
||||||
|
members: [{ name: 'tom', providerId: 'anthropic' }],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberSpawnSnapshot: {
|
||||||
|
updatedAt: '2026-05-25T20:14:10.000Z',
|
||||||
|
statuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
updatedAt: '2026-05-25T20:14:10.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toMatchObject({
|
||||||
|
memberName: 'tom',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
spawnStatus: 'online',
|
||||||
|
runtimeAlive: true,
|
||||||
|
hardFailure: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('includes runtime advisory evidence in copy diagnostics', () => {
|
it('includes runtime advisory evidence in copy diagnostics', () => {
|
||||||
const payload = buildMemberLaunchDiagnosticsPayload({
|
const payload = buildMemberLaunchDiagnosticsPayload({
|
||||||
memberName: 'alice',
|
memberName: 'alice',
|
||||||
|
|
|
||||||
|
|
@ -924,6 +924,127 @@ describe('buildTeamProvisioningPresentation', () => {
|
||||||
expect(presentation?.currentStepIndex).toBe(2);
|
expect(presentation?.currentStepIndex).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not present bootstrap-confirmed provisioned-but-not-alive entries as failed', () => {
|
||||||
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
|
progress: {
|
||||||
|
runId: 'run-signal-ops',
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
state: 'ready',
|
||||||
|
startedAt: '2026-05-25T20:13:40.000Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:05.411Z',
|
||||||
|
message: 'Team provisioned',
|
||||||
|
messageSeverity: undefined,
|
||||||
|
pid: 27036,
|
||||||
|
cliLogsTail: '',
|
||||||
|
assistantOutput: '',
|
||||||
|
},
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: 'team-lead',
|
||||||
|
agentType: 'team-lead',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
status: 'active',
|
||||||
|
currentTaskId: null,
|
||||||
|
taskCount: 0,
|
||||||
|
lastActiveAt: null,
|
||||||
|
messageCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
laneKind: 'primary',
|
||||||
|
status: 'active',
|
||||||
|
currentTaskId: null,
|
||||||
|
taskCount: 0,
|
||||||
|
lastActiveAt: null,
|
||||||
|
messageCount: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive',
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
tom: {
|
||||||
|
memberName: 'tom',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:03.317Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(presentation?.isFailed).toBe(false);
|
||||||
|
expect(presentation?.failedSpawnCount).toBe(0);
|
||||||
|
expect(presentation?.heartbeatConfirmedCount).toBe(1);
|
||||||
|
expect(presentation?.panelTone).not.toBe('error');
|
||||||
|
expect(presentation?.compactTone).not.toBe('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('presents unsafe bootstrap-confirmed provisioned-but-not-alive entries as failed', () => {
|
||||||
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
|
progress: {
|
||||||
|
runId: 'run-signal-ops',
|
||||||
|
teamName: 'signal-ops',
|
||||||
|
state: 'ready',
|
||||||
|
startedAt: '2026-05-25T20:13:40.000Z',
|
||||||
|
updatedAt: '2026-05-25T20:14:05.411Z',
|
||||||
|
message: 'Team provisioned',
|
||||||
|
messageSeverity: undefined,
|
||||||
|
pid: 27036,
|
||||||
|
cliLogsTail: '',
|
||||||
|
assistantOutput: '',
|
||||||
|
},
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: 'tom',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
laneKind: 'primary',
|
||||||
|
status: 'active',
|
||||||
|
currentTaskId: null,
|
||||||
|
taskCount: 0,
|
||||||
|
lastActiveAt: null,
|
||||||
|
messageCount: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
tom: {
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
livenessKind: 'not_found',
|
||||||
|
runtimeDiagnostic: 'Runtime is no longer registered',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
updatedAt: '2026-05-25T20:14:02.147Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(presentation?.isFailed).toBe(false);
|
||||||
|
expect(presentation?.failedSpawnCount).toBe(1);
|
||||||
|
expect(presentation?.heartbeatConfirmedCount).toBe(0);
|
||||||
|
expect(presentation?.successMessageSeverity).toBe('warning');
|
||||||
|
expect(presentation?.compactTone).toBe('warning');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not show core team ready while a primary member is still joining', () => {
|
it('does not show core team ready while a primary member is still joining', () => {
|
||||||
const presentation = buildTeamProvisioningPresentation({
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
progress: {
|
progress: {
|
||||||
|
|
|
||||||
149
test/shared/utils/teamLaunchFailureReason.test.ts
Normal file
149
test/shared/utils/teamLaunchFailureReason.test.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
||||||
|
} from '@shared/utils/teamLaunchFailureReason';
|
||||||
|
|
||||||
|
describe('teamLaunchFailureReason', () => {
|
||||||
|
it('treats runtime process candidates as unsafe provisioned-but-not-alive evidence', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
livenessKind: 'runtime_process_candidate',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'OpenCode runtime process detected, but teammate bootstrap is not confirmed',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats permission-blocked runtime liveness as unsafe provisioned-but-not-alive evidence', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
livenessKind: 'permission_blocked',
|
||||||
|
runtimeDiagnostic: 'runtime is waiting for permission approval',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps process-table-unavailable registered metadata safe for bootstrap healing', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats missing liveness without process-table evidence as unsafe', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps missing liveness safe when process-table evidence is explicit', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence({
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses spawn process-table evidence for registered runtime metadata without diagnostics', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(
|
||||||
|
{
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
status: 'error',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses spawn process-table evidence for runtime metadata without liveness or diagnostics', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(
|
||||||
|
{
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
status: 'error',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps registered runtime metadata unsafe when runtime diagnostics contradict spawn proof', () => {
|
||||||
|
expect(
|
||||||
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(
|
||||||
|
{
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason:
|
||||||
|
'CLI process exited (code 1) - team provisioned but not alive; process table unavailable',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
status: 'error',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic: 'Runtime heartbeat is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recognizes runtime-diagnostic-only provisioned-but-not-alive failures', () => {
|
||||||
|
expect(
|
||||||
|
isBootstrapConfirmedProvisionedButNotAliveFailure({
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: true,
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
runtimeDiagnostic: 'CLI process exited (code 1) - team provisioned but not alive',
|
||||||
|
status: 'error',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue