fix(team): reconcile provisioned-but-not-alive bootstrap state

This commit is contained in:
infiniti 2026-05-27 12:16:41 +03:00 committed by GitHub
parent 9d5f176597
commit ebcc0e717f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 5114 additions and 418 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
} }

View file

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

View file

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

View file

@ -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)
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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';
} }

View file

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

View 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);
}

View file

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

View file

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

View file

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

View file

@ -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');
}); });
}); });

View file

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

View file

@ -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');

View file

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

View file

@ -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');

View file

@ -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);
});
}); });

View file

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

View file

@ -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();

View file

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

View file

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

View file

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

View file

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

View 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);
});
});