fix(team): reconcile cleanup bootstrap evidence
This commit is contained in:
parent
511dd5d7a6
commit
1b836530e7
2 changed files with 410 additions and 12 deletions
|
|
@ -3597,7 +3597,8 @@ function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean {
|
||||||
isOpenCodeBridgeLaunchFailureReason(reason) ||
|
isOpenCodeBridgeLaunchFailureReason(reason) ||
|
||||||
isBootstrapMcpResourceReadFailureReason(reason) ||
|
isBootstrapMcpResourceReadFailureReason(reason) ||
|
||||||
isBootstrapCheckInTimeoutFailureReason(reason) ||
|
isBootstrapCheckInTimeoutFailureReason(reason) ||
|
||||||
isBootstrapInstructionPromptFailureReason(reason)
|
isBootstrapInstructionPromptFailureReason(reason) ||
|
||||||
|
isLaunchCleanupBootstrapIncompleteFailureReason(reason)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3618,6 +3619,58 @@ function isBootstrapInstructionPromptFailureReason(reason?: string): boolean {
|
||||||
return typeof reason === 'string' && isBootstrapInstructionPrompt(reason);
|
return typeof reason === 'string' && isBootstrapInstructionPrompt(reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLaunchCleanupBootstrapIncompleteFailureReason(reason?: string): boolean {
|
||||||
|
const text = reason?.trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
text === 'Launch ended before teammate bootstrap completed.' ||
|
||||||
|
text === 'Deterministic bootstrap failed before teammate check-in.'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
text.startsWith('Launch ended before teammate bootstrap completed. ') &&
|
||||||
|
text.includes('Runtime process was alive after bootstrap failure')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBootstrapMemberEvidenceCurrentForMember(
|
||||||
|
current: { firstSpawnAcceptedAt?: string; lastEvaluatedAt?: string },
|
||||||
|
bootstrapMember: Pick<
|
||||||
|
PersistedTeamLaunchMemberState,
|
||||||
|
'firstSpawnAcceptedAt' | 'lastHeartbeatAt' | 'lastRuntimeAliveAt' | 'lastEvaluatedAt'
|
||||||
|
>,
|
||||||
|
evidenceKind: 'acceptance' | 'confirmation'
|
||||||
|
): boolean {
|
||||||
|
const bootstrapFirstSpawnAcceptedMs = Date.parse(bootstrapMember.firstSpawnAcceptedAt ?? '');
|
||||||
|
const bootstrapLastEvaluatedMs = Date.parse(bootstrapMember.lastEvaluatedAt ?? '');
|
||||||
|
const hasDurableBootstrapSpawnAcceptedAt =
|
||||||
|
Number.isFinite(bootstrapFirstSpawnAcceptedMs) &&
|
||||||
|
(!Number.isFinite(bootstrapLastEvaluatedMs) ||
|
||||||
|
bootstrapFirstSpawnAcceptedMs <= bootstrapLastEvaluatedMs);
|
||||||
|
const evidenceAt =
|
||||||
|
evidenceKind === 'confirmation'
|
||||||
|
? (bootstrapMember.lastHeartbeatAt ??
|
||||||
|
bootstrapMember.lastRuntimeAliveAt ??
|
||||||
|
bootstrapMember.lastEvaluatedAt)
|
||||||
|
: hasDurableBootstrapSpawnAcceptedAt
|
||||||
|
? bootstrapMember.firstSpawnAcceptedAt
|
||||||
|
: bootstrapMember.lastEvaluatedAt;
|
||||||
|
const evidenceMs = Date.parse(evidenceAt ?? '');
|
||||||
|
if (!Number.isFinite(evidenceMs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstSpawnAcceptedMs = Date.parse(current.firstSpawnAcceptedAt ?? '');
|
||||||
|
const lastEvaluatedMs = Date.parse(current.lastEvaluatedAt ?? '');
|
||||||
|
const hasDurableSpawnBoundary =
|
||||||
|
Number.isFinite(firstSpawnAcceptedMs) &&
|
||||||
|
(!Number.isFinite(lastEvaluatedMs) || firstSpawnAcceptedMs <= lastEvaluatedMs);
|
||||||
|
const boundaryMs = hasDurableSpawnBoundary ? firstSpawnAcceptedMs : NaN;
|
||||||
|
return !Number.isFinite(boundaryMs) || evidenceMs >= boundaryMs;
|
||||||
|
}
|
||||||
|
|
||||||
function isTmuxNoServerRunningError(error: unknown): boolean {
|
function isTmuxNoServerRunningError(error: unknown): boolean {
|
||||||
const text = error instanceof Error ? error.message : String(error ?? '');
|
const text = error instanceof Error ? error.message : String(error ?? '');
|
||||||
return (
|
return (
|
||||||
|
|
@ -27655,11 +27708,7 @@ export class TeamProvisioningService {
|
||||||
private async overlayPrimaryBootstrapTruthIntoRunStatusesFromBootstrapState(
|
private async overlayPrimaryBootstrapTruthIntoRunStatusesFromBootstrapState(
|
||||||
run: ProvisioningRun
|
run: ProvisioningRun
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (!run.isLaunch) {
|
||||||
!run.isLaunch ||
|
|
||||||
!Array.isArray(run.mixedSecondaryLanes) ||
|
|
||||||
run.mixedSecondaryLanes.length === 0
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27684,7 +27733,7 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryMemberNames = new Set(
|
const primaryMemberNames = new Set(
|
||||||
(run.effectiveMembers ?? [])
|
[...(run.effectiveMembers ?? []), ...(run.expectedMembers ?? []).map((name) => ({ name }))]
|
||||||
.map((member) => member.name?.trim())
|
.map((member) => member.name?.trim())
|
||||||
.filter((name): name is string => Boolean(name))
|
.filter((name): name is string => Boolean(name))
|
||||||
);
|
);
|
||||||
|
|
@ -27703,6 +27752,9 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
const current =
|
const current =
|
||||||
run.memberSpawnStatuses.get(memberName) ?? createInitialMemberSpawnStatusEntry();
|
run.memberSpawnStatuses.get(memberName) ?? createInitialMemberSpawnStatusEntry();
|
||||||
|
if (!isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'confirmation')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (current.launchState === 'skipped_for_launch' || current.skippedForLaunch === true) {
|
if (current.launchState === 'skipped_for_launch' || current.skippedForLaunch === true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -27793,6 +27845,9 @@ export class TeamProvisioningService {
|
||||||
if (!current || bootstrapMember?.bootstrapConfirmed !== true) {
|
if (!current || bootstrapMember?.bootstrapConfirmed !== true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'confirmation')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
current.providerId === 'opencode' ||
|
current.providerId === 'opencode' ||
|
||||||
isPersistedOpenCodeSecondaryLaneMember(current) ||
|
isPersistedOpenCodeSecondaryLaneMember(current) ||
|
||||||
|
|
@ -30114,9 +30169,25 @@ export class TeamProvisioningService {
|
||||||
if (!current || !bootstrapMember) {
|
if (!current || !bootstrapMember) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
bootstrapMember.bootstrapConfirmed === true &&
|
||||||
|
!isPersistedOpenCodeSecondaryLaneMember(current) &&
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'confirmation')
|
||||||
|
) {
|
||||||
|
const currentConfirmed =
|
||||||
|
current.bootstrapConfirmed === true || current.launchState === 'confirmed_alive';
|
||||||
|
const failureReason = current.hardFailureReason ?? current.runtimeDiagnostic;
|
||||||
|
const hasAutoClearableFailure =
|
||||||
|
(current.launchState === 'failed_to_start' || current.hardFailure === true) &&
|
||||||
|
isAutoClearableLaunchFailureReason(failureReason);
|
||||||
|
if (!currentConfirmed || hasAutoClearableFailure) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
const bootstrapProvesSpawnAcceptance =
|
const bootstrapProvesSpawnAcceptance =
|
||||||
bootstrapMember.agentToolAccepted === true ||
|
(bootstrapMember.agentToolAccepted === true ||
|
||||||
typeof bootstrapMember.firstSpawnAcceptedAt === 'string';
|
typeof bootstrapMember.firstSpawnAcceptedAt === 'string') &&
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'acceptance');
|
||||||
if (!bootstrapProvesSpawnAcceptance) {
|
if (!bootstrapProvesSpawnAcceptance) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -30327,7 +30398,11 @@ export class TeamProvisioningService {
|
||||||
lastEvaluatedAt: now,
|
lastEvaluatedAt: now,
|
||||||
};
|
};
|
||||||
const isOpenCodeSecondaryLaneMember = isPersistedOpenCodeSecondaryLaneMember(current);
|
const isOpenCodeSecondaryLaneMember = isPersistedOpenCodeSecondaryLaneMember(current);
|
||||||
if (bootstrapMember?.agentToolAccepted && !current.agentToolAccepted) {
|
if (
|
||||||
|
bootstrapMember?.agentToolAccepted &&
|
||||||
|
!current.agentToolAccepted &&
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'acceptance')
|
||||||
|
) {
|
||||||
current.agentToolAccepted = true;
|
current.agentToolAccepted = true;
|
||||||
current.firstSpawnAcceptedAt =
|
current.firstSpawnAcceptedAt =
|
||||||
current.firstSpawnAcceptedAt ?? bootstrapMember.firstSpawnAcceptedAt;
|
current.firstSpawnAcceptedAt ?? bootstrapMember.firstSpawnAcceptedAt;
|
||||||
|
|
@ -30335,7 +30410,8 @@ export class TeamProvisioningService {
|
||||||
if (
|
if (
|
||||||
bootstrapMember?.bootstrapConfirmed &&
|
bootstrapMember?.bootstrapConfirmed &&
|
||||||
!current.bootstrapConfirmed &&
|
!current.bootstrapConfirmed &&
|
||||||
!isOpenCodeSecondaryLaneMember
|
!isOpenCodeSecondaryLaneMember &&
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'confirmation')
|
||||||
) {
|
) {
|
||||||
current.bootstrapConfirmed = true;
|
current.bootstrapConfirmed = true;
|
||||||
current.lastHeartbeatAt = current.lastHeartbeatAt ?? bootstrapMember.lastHeartbeatAt;
|
current.lastHeartbeatAt = current.lastHeartbeatAt ?? bootstrapMember.lastHeartbeatAt;
|
||||||
|
|
|
||||||
|
|
@ -19943,6 +19943,256 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('heals cleanup-finalized launch failures when bootstrap-state confirms an Anthropic primary member', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'zz-unit-cleanup-finalized-bootstrap-state-heals';
|
||||||
|
const leadSessionId = 'lead-session';
|
||||||
|
const projectPath = '/Users/test/proj';
|
||||||
|
const acceptedAt = new Date(Date.now() - 90_000).toISOString();
|
||||||
|
const bootstrapAt = new Date(Date.now() - 60_000).toISOString();
|
||||||
|
const cleanupAt = new Date(Date.now() - 30_000).toISOString();
|
||||||
|
const runtimePid = 45_724;
|
||||||
|
|
||||||
|
writeLaunchConfig(teamName, projectPath, leadSessionId, ['jack']);
|
||||||
|
writeLaunchState(
|
||||||
|
teamName,
|
||||||
|
leadSessionId,
|
||||||
|
{
|
||||||
|
jack: {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
laneId: 'primary',
|
||||||
|
laneKind: 'primary',
|
||||||
|
laneOwnerProviderId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
runtimePid,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'Launch ended before teammate bootstrap completed.',
|
||||||
|
livenessKind: 'stale_metadata',
|
||||||
|
runtimeDiagnostic: 'persisted runtime pid is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: acceptedAt,
|
||||||
|
lastEvaluatedAt: cleanupAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ launchPhase: 'finished', updatedAt: cleanupAt }
|
||||||
|
);
|
||||||
|
writeBootstrapState(
|
||||||
|
teamName,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'jack',
|
||||||
|
status: 'bootstrap_confirmed',
|
||||||
|
lastAttemptAt: Date.parse(acceptedAt),
|
||||||
|
lastObservedAt: Date.parse(bootstrapAt),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bootstrapAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
privateHarness(svc).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
'jack',
|
||||||
|
{
|
||||||
|
alive: false,
|
||||||
|
backendType: 'process',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
livenessKind: 'stale_metadata',
|
||||||
|
pidSource: 'persisted_metadata',
|
||||||
|
runtimeDiagnostic: 'persisted runtime pid is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
metricsPid: runtimePid,
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await svc.getMemberSpawnStatuses(teamName);
|
||||||
|
|
||||||
|
expect(result.teamLaunchState).toBe('clean_success');
|
||||||
|
expect(result.statuses.jack).toMatchObject({
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
expect(result.statuses.jack?.hardFailureReason).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not heal cleanup-finalized launch failures from stale bootstrap-state confirmation', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'zz-unit-cleanup-finalized-stale-bootstrap-ignored';
|
||||||
|
const leadSessionId = 'lead-session';
|
||||||
|
const projectPath = '/Users/test/proj';
|
||||||
|
const staleBootstrapAt = '2026-05-22T14:14:58.000Z';
|
||||||
|
const acceptedAt = '2026-05-22T14:15:44.000Z';
|
||||||
|
const cleanupAt = '2026-05-22T14:16:07.000Z';
|
||||||
|
const runtimePid = 45_724;
|
||||||
|
|
||||||
|
writeLaunchConfig(teamName, projectPath, leadSessionId, ['jack']);
|
||||||
|
writeLaunchState(
|
||||||
|
teamName,
|
||||||
|
leadSessionId,
|
||||||
|
{
|
||||||
|
jack: {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
laneId: 'primary',
|
||||||
|
laneKind: 'primary',
|
||||||
|
laneOwnerProviderId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
runtimePid,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'Launch ended before teammate bootstrap completed.',
|
||||||
|
livenessKind: 'stale_metadata',
|
||||||
|
runtimeDiagnostic: 'persisted runtime pid is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: acceptedAt,
|
||||||
|
lastEvaluatedAt: cleanupAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ launchPhase: 'finished', updatedAt: cleanupAt }
|
||||||
|
);
|
||||||
|
writeBootstrapState(
|
||||||
|
teamName,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'jack',
|
||||||
|
status: 'bootstrap_confirmed',
|
||||||
|
lastAttemptAt: Date.parse(staleBootstrapAt),
|
||||||
|
lastObservedAt: Date.parse(staleBootstrapAt),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
staleBootstrapAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
privateHarness(svc).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
'jack',
|
||||||
|
{
|
||||||
|
alive: false,
|
||||||
|
backendType: 'process',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
livenessKind: 'stale_metadata',
|
||||||
|
pidSource: 'persisted_metadata',
|
||||||
|
runtimeDiagnostic: 'persisted runtime pid is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
metricsPid: runtimePid,
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await svc.getMemberSpawnStatuses(teamName);
|
||||||
|
|
||||||
|
expect(result.teamLaunchState).toBe('partial_failure');
|
||||||
|
expect(result.statuses.jack).toMatchObject({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'Launch ended before teammate bootstrap completed.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not heal cleanup-finalized launch failures from stale member evidence in a rewritten bootstrap-state file', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'zz-unit-cleanup-finalized-stale-member-evidence-ignored';
|
||||||
|
const leadSessionId = 'lead-session';
|
||||||
|
const projectPath = '/Users/test/proj';
|
||||||
|
const staleObservedAt = '2026-05-22T14:14:58.000Z';
|
||||||
|
const acceptedAt = '2026-05-22T14:15:44.000Z';
|
||||||
|
const cleanupAt = '2026-05-22T14:16:07.000Z';
|
||||||
|
const bootstrapFileUpdatedAt = '2026-05-22T14:16:30.000Z';
|
||||||
|
const runtimePid = 45_724;
|
||||||
|
|
||||||
|
writeLaunchConfig(teamName, projectPath, leadSessionId, ['jack']);
|
||||||
|
writeLaunchState(
|
||||||
|
teamName,
|
||||||
|
leadSessionId,
|
||||||
|
{
|
||||||
|
jack: {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
laneId: 'primary',
|
||||||
|
laneKind: 'primary',
|
||||||
|
laneOwnerProviderId: 'anthropic',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
runtimePid,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'Launch ended before teammate bootstrap completed.',
|
||||||
|
livenessKind: 'stale_metadata',
|
||||||
|
runtimeDiagnostic: 'persisted runtime pid is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: acceptedAt,
|
||||||
|
lastEvaluatedAt: cleanupAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ launchPhase: 'finished', updatedAt: cleanupAt }
|
||||||
|
);
|
||||||
|
writeBootstrapState(
|
||||||
|
teamName,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'jack',
|
||||||
|
status: 'bootstrap_confirmed',
|
||||||
|
lastObservedAt: Date.parse(staleObservedAt),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bootstrapFileUpdatedAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
privateHarness(svc).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
||||||
|
async () =>
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
'jack',
|
||||||
|
{
|
||||||
|
alive: false,
|
||||||
|
backendType: 'process',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
livenessKind: 'stale_metadata',
|
||||||
|
pidSource: 'persisted_metadata',
|
||||||
|
runtimeDiagnostic: 'persisted runtime pid is not alive',
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
metricsPid: runtimePid,
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await svc.getMemberSpawnStatuses(teamName);
|
||||||
|
|
||||||
|
expect(result.teamLaunchState).toBe('partial_failure');
|
||||||
|
expect(result.statuses.jack).toMatchObject({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: 'Launch ended before teammate bootstrap completed.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not heal terminal bootstrap-state failures from native app-managed proof with mismatched hashes', async () => {
|
it('does not heal terminal bootstrap-state failures from native app-managed proof with mismatched hashes', async () => {
|
||||||
allowConsoleLogs();
|
allowConsoleLogs();
|
||||||
const teamName = 'zz-unit-bootstrap-state-native-runtime-proof-hash-mismatch';
|
const teamName = 'zz-unit-bootstrap-state-native-runtime-proof-hash-mismatch';
|
||||||
|
|
@ -25349,6 +25599,7 @@ describe('TeamProvisioningService', () => {
|
||||||
hardFailure: true,
|
hardFailure: true,
|
||||||
error: 'Teammate was never spawned during launch.',
|
error: 'Teammate was never spawned during launch.',
|
||||||
hardFailureReason: 'Teammate was never spawned during launch.',
|
hardFailureReason: 'Teammate was never spawned during launch.',
|
||||||
|
firstSpawnAcceptedAt: '2026-04-23T10:00:30.000Z',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|
@ -25362,6 +25613,7 @@ describe('TeamProvisioningService', () => {
|
||||||
hardFailure: true,
|
hardFailure: true,
|
||||||
error: 'Teammate was never spawned during launch.',
|
error: 'Teammate was never spawned during launch.',
|
||||||
hardFailureReason: 'Teammate was never spawned during launch.',
|
hardFailureReason: 'Teammate was never spawned during launch.',
|
||||||
|
firstSpawnAcceptedAt: '2026-04-23T10:00:30.000Z',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
|
|
@ -25730,7 +25982,7 @@ describe('TeamProvisioningService', () => {
|
||||||
lastObservedAt: Date.parse('2026-04-23T09:59:00.000Z'),
|
lastObservedAt: Date.parse('2026-04-23T09:59:00.000Z'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'2026-04-23T09:59:00.000Z'
|
'2026-04-23T10:01:00.000Z'
|
||||||
);
|
);
|
||||||
const run = createMemberSpawnRun({
|
const run = createMemberSpawnRun({
|
||||||
teamName,
|
teamName,
|
||||||
|
|
@ -25749,6 +26001,7 @@ describe('TeamProvisioningService', () => {
|
||||||
hardFailure: true,
|
hardFailure: true,
|
||||||
error: 'Teammate was never spawned during launch.',
|
error: 'Teammate was never spawned during launch.',
|
||||||
hardFailureReason: 'Teammate was never spawned during launch.',
|
hardFailureReason: 'Teammate was never spawned during launch.',
|
||||||
|
firstSpawnAcceptedAt: '2026-04-23T10:00:30.000Z',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
|
|
@ -25786,6 +26039,75 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies bootstrap-state truth before persisting pure launch cleanup snapshots', async () => {
|
||||||
|
const teamName = 'pure-anthropic-cleanup-bootstrap-truth';
|
||||||
|
const acceptedAt = '2026-05-22T14:15:44.000Z';
|
||||||
|
const bootstrapAt = '2026-05-22T14:15:58.000Z';
|
||||||
|
const run = createMemberSpawnRun({
|
||||||
|
teamName,
|
||||||
|
runId: 'run-pure-anthropic-cleanup-bootstrap-truth',
|
||||||
|
startedAt: '2026-05-22T14:15:40.000Z',
|
||||||
|
expectedMembers: ['jack'],
|
||||||
|
memberSpawnStatuses: new Map([
|
||||||
|
[
|
||||||
|
'jack',
|
||||||
|
createMemberSpawnStatusEntry({
|
||||||
|
status: 'error',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
error: 'Launch ended before teammate bootstrap completed.',
|
||||||
|
hardFailureReason: 'Launch ended before teammate bootstrap completed.',
|
||||||
|
firstSpawnAcceptedAt: acceptedAt,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
run.isLaunch = true;
|
||||||
|
run.request = {
|
||||||
|
teamName,
|
||||||
|
cwd: '/Users/test/proj',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
members: [],
|
||||||
|
};
|
||||||
|
run.effectiveMembers = [
|
||||||
|
{ name: 'jack', providerId: 'anthropic', model: 'claude-opus-4-7' },
|
||||||
|
];
|
||||||
|
fs.mkdirSync(path.join(tempTeamsBase, teamName), { recursive: true });
|
||||||
|
writeBootstrapState(
|
||||||
|
teamName,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'jack',
|
||||||
|
status: 'bootstrap_confirmed',
|
||||||
|
lastAttemptAt: Date.parse(acceptedAt),
|
||||||
|
lastObservedAt: Date.parse(bootstrapAt),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
bootstrapAt
|
||||||
|
);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const snapshot = await (
|
||||||
|
svc as unknown as {
|
||||||
|
persistLaunchStateSnapshot: (
|
||||||
|
run: typeof run,
|
||||||
|
phase: 'finished'
|
||||||
|
) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
).persistLaunchStateSnapshot(run, 'finished');
|
||||||
|
|
||||||
|
expect(snapshot).toBeNull();
|
||||||
|
await expect(fsPromises.readFile(getTeamLaunchStatePath(teamName), 'utf8')).rejects.toMatchObject(
|
||||||
|
{
|
||||||
|
code: 'ENOENT',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('includes queued OpenCode secondary lanes in live spawn statuses during createTeam runs', async () => {
|
it('includes queued OpenCode secondary lanes in live spawn statuses during createTeam runs', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
vi.spyOn(svc as any, 'refreshMemberSpawnStatusesFromLeadInbox').mockResolvedValue(undefined);
|
vi.spyOn(svc as any, 'refreshMemberSpawnStatusesFromLeadInbox').mockResolvedValue(undefined);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue