fix(team): allow same-run bootstrap clock skew for native proofs
resolveBootstrapRuntimeEvidenceBoundaryMs учитывает оба источника времени старта (firstSpawnAcceptedAt и bootstrapExpectedAfter) и принимает более раннее, если у member и runtime совпадает bootstrapProofToken + runId. Это лечит случай, когда proof подписан до того, как app зафиксировал firstSpawnAcceptedAt, но после bootstrap boundary самого ранкона. Та же логика применена в isBootstrapMemberEvidenceCurrentForMember для confirmation evidence.
This commit is contained in:
parent
1f6c9fe34b
commit
7e6c0da21e
4 changed files with 222 additions and 19 deletions
|
|
@ -5017,7 +5017,11 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private readPersistedTeamProcessRows(teamName: string): unknown[] | null {
|
private readPersistedTeamProcessRows(teamName: string): unknown[] | null {
|
||||||
const processesPath = this.resolveSafeTeamStoragePath(getTeamsBasePath(), teamName, 'processes.json');
|
const processesPath = this.resolveSafeTeamStoragePath(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
teamName,
|
||||||
|
'processes.json'
|
||||||
|
);
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(fs.readFileSync(processesPath, 'utf8')) as unknown;
|
parsed = JSON.parse(fs.readFileSync(processesPath, 'utf8')) as unknown;
|
||||||
|
|
@ -14768,7 +14772,11 @@ export class TeamProvisioningService {
|
||||||
bootstrapContextHash?: string;
|
bootstrapContextHash?: string;
|
||||||
bootstrapBriefingHash?: string;
|
bootstrapBriefingHash?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const configPath = this.resolveSafeTeamStoragePath(getTeamsBasePath(), input.teamName, 'config.json');
|
const configPath = this.resolveSafeTeamStoragePath(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
input.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
const raw = await tryReadRegularFileUtf8(configPath, {
|
const raw = await tryReadRegularFileUtf8(configPath, {
|
||||||
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
||||||
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
||||||
|
|
@ -19982,7 +19990,11 @@ export class TeamProvisioningService {
|
||||||
try {
|
try {
|
||||||
const teamsBasePathsToProbe = getTeamsBasePathsToProbe();
|
const teamsBasePathsToProbe = getTeamsBasePathsToProbe();
|
||||||
for (const probe of teamsBasePathsToProbe) {
|
for (const probe of teamsBasePathsToProbe) {
|
||||||
const configPath = this.resolveSafeTeamStoragePath(probe.basePath, request.teamName, 'config.json');
|
const configPath = this.resolveSafeTeamStoragePath(
|
||||||
|
probe.basePath,
|
||||||
|
request.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
if (await this.pathExists(configPath)) {
|
if (await this.pathExists(configPath)) {
|
||||||
const suffix = probe.location === 'configured' ? '' : ` (found under ${probe.basePath})`;
|
const suffix = probe.location === 'configured' ? '' : ` (found under ${probe.basePath})`;
|
||||||
throw new Error(`Team already exists${suffix}`);
|
throw new Error(`Team already exists${suffix}`);
|
||||||
|
|
@ -20602,7 +20614,11 @@ export class TeamProvisioningService {
|
||||||
): Promise<TeamCreateResponse> {
|
): Promise<TeamCreateResponse> {
|
||||||
const teamsBasePathsToProbe = getTeamsBasePathsToProbe();
|
const teamsBasePathsToProbe = getTeamsBasePathsToProbe();
|
||||||
for (const probe of teamsBasePathsToProbe) {
|
for (const probe of teamsBasePathsToProbe) {
|
||||||
const configPath = this.resolveSafeTeamStoragePath(probe.basePath, request.teamName, 'config.json');
|
const configPath = this.resolveSafeTeamStoragePath(
|
||||||
|
probe.basePath,
|
||||||
|
request.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
if (await this.pathExists(configPath)) {
|
if (await this.pathExists(configPath)) {
|
||||||
const suffix = probe.location === 'configured' ? '' : ` (found under ${probe.basePath})`;
|
const suffix = probe.location === 'configured' ? '' : ` (found under ${probe.basePath})`;
|
||||||
throw new Error(`Team already exists${suffix}`);
|
throw new Error(`Team already exists${suffix}`);
|
||||||
|
|
@ -20661,7 +20677,11 @@ export class TeamProvisioningService {
|
||||||
request: TeamLaunchRequest,
|
request: TeamLaunchRequest,
|
||||||
onProgress: (progress: TeamProvisioningProgress) => void
|
onProgress: (progress: TeamProvisioningProgress) => void
|
||||||
): Promise<TeamLaunchResponse> {
|
): Promise<TeamLaunchResponse> {
|
||||||
const configPath = this.resolveSafeTeamStoragePath(getTeamsBasePath(), request.teamName, 'config.json');
|
const configPath = this.resolveSafeTeamStoragePath(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
request.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
const configRaw = await tryReadRegularFileUtf8(configPath, {
|
const configRaw = await tryReadRegularFileUtf8(configPath, {
|
||||||
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
||||||
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
||||||
|
|
@ -20943,7 +20963,11 @@ export class TeamProvisioningService {
|
||||||
request: TeamCreateRequest,
|
request: TeamCreateRequest,
|
||||||
members: TeamCreateRequest['members']
|
members: TeamCreateRequest['members']
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const configPath = this.resolveSafeTeamStoragePath(getTeamsBasePath(), request.teamName, 'config.json');
|
const configPath = this.resolveSafeTeamStoragePath(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
request.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
const config: TeamConfig = {
|
const config: TeamConfig = {
|
||||||
name: request.displayName?.trim() || request.teamName,
|
name: request.displayName?.trim() || request.teamName,
|
||||||
description: request.description,
|
description: request.description,
|
||||||
|
|
@ -21182,7 +21206,11 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify config.json exists — team must already be provisioned
|
// Verify config.json exists — team must already be provisioned
|
||||||
const configPath = this.resolveSafeTeamStoragePath(getTeamsBasePath(), request.teamName, 'config.json');
|
const configPath = this.resolveSafeTeamStoragePath(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
request.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
const configRaw = await tryReadRegularFileUtf8(configPath, {
|
const configRaw = await tryReadRegularFileUtf8(configPath, {
|
||||||
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
||||||
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
||||||
|
|
@ -29399,17 +29427,41 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveBootstrapRuntimeEvidenceBoundaryMs(
|
||||||
|
member: Pick<PersistedTeamLaunchMemberState, 'firstSpawnAcceptedAt' | 'runtimeRunId'>,
|
||||||
|
runtimeMember: PersistedRuntimeMemberLike | undefined
|
||||||
|
): number {
|
||||||
|
const firstSpawnAcceptedMs = Date.parse(member.firstSpawnAcceptedAt ?? '');
|
||||||
|
const bootstrapExpectedAfterMs = Date.parse(runtimeMember?.bootstrapExpectedAfter ?? '');
|
||||||
|
if (!Number.isFinite(firstSpawnAcceptedMs)) {
|
||||||
|
return Number.isFinite(bootstrapExpectedAfterMs) ? bootstrapExpectedAfterMs : Number.NaN;
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(bootstrapExpectedAfterMs)) {
|
||||||
|
return firstSpawnAcceptedMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proofToken = runtimeMember?.bootstrapProofToken?.trim() ?? '';
|
||||||
|
const memberRunId = typeof member.runtimeRunId === 'string' ? member.runtimeRunId.trim() : '';
|
||||||
|
const runtimeRunId = runtimeMember?.bootstrapRunId?.trim() ?? '';
|
||||||
|
const runIdsCompatible =
|
||||||
|
memberRunId.length === 0 || runtimeRunId.length === 0 || memberRunId === runtimeRunId;
|
||||||
|
if (proofToken.length === 0 || !runIdsCompatible) {
|
||||||
|
return firstSpawnAcceptedMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(firstSpawnAcceptedMs, bootstrapExpectedAfterMs);
|
||||||
|
}
|
||||||
|
|
||||||
private async findBootstrapRuntimeProofObservedAt(
|
private async findBootstrapRuntimeProofObservedAt(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
memberName: string,
|
memberName: string,
|
||||||
member: Pick<
|
member: Pick<
|
||||||
PersistedTeamLaunchMemberState,
|
PersistedTeamLaunchMemberState,
|
||||||
'firstSpawnAcceptedAt' | 'launchState' | 'hardFailureReason'
|
'firstSpawnAcceptedAt' | 'launchState' | 'hardFailureReason' | 'runtimeRunId'
|
||||||
>
|
>
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const runtimeMember = this.resolveBootstrapRuntimeMember(teamName, memberName);
|
const runtimeMember = this.resolveBootstrapRuntimeMember(teamName, memberName);
|
||||||
const boundaryText = member.firstSpawnAcceptedAt ?? runtimeMember?.bootstrapExpectedAfter;
|
const boundaryMs = this.resolveBootstrapRuntimeEvidenceBoundaryMs(member, runtimeMember);
|
||||||
const boundaryMs = boundaryText ? Date.parse(boundaryText) : Number.NaN;
|
|
||||||
if (!runtimeMember?.bootstrapProofToken && !Number.isFinite(boundaryMs)) {
|
if (!runtimeMember?.bootstrapProofToken && !Number.isFinite(boundaryMs)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -29510,8 +29562,7 @@ export class TeamProvisioningService {
|
||||||
if (runtimeBackendType !== 'process' && !processPaneId?.startsWith('process:')) {
|
if (runtimeBackendType !== 'process' && !processPaneId?.startsWith('process:')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const boundaryText = member.firstSpawnAcceptedAt ?? runtimeMember?.bootstrapExpectedAfter;
|
const boundaryMs = this.resolveBootstrapRuntimeEvidenceBoundaryMs(member, runtimeMember);
|
||||||
const boundaryMs = boundaryText ? Date.parse(boundaryText) : Number.NaN;
|
|
||||||
const expectedPid =
|
const expectedPid =
|
||||||
typeof member.runtimePid === 'number' && member.runtimePid > 0
|
typeof member.runtimePid === 'number' && member.runtimePid > 0
|
||||||
? member.runtimePid
|
? member.runtimePid
|
||||||
|
|
@ -34451,7 +34502,11 @@ export class TeamProvisioningService {
|
||||||
// Best-effort: detect CLI-suffixed member names (alice-2, bob-2) that indicate
|
// Best-effort: detect CLI-suffixed member names (alice-2, bob-2) that indicate
|
||||||
// a stale config.json was present during launch (double-launch race).
|
// a stale config.json was present during launch (double-launch race).
|
||||||
try {
|
try {
|
||||||
const postLaunchConfigPath = this.resolveSafeTeamStoragePath(getTeamsBasePath(), run.teamName, 'config.json');
|
const postLaunchConfigPath = this.resolveSafeTeamStoragePath(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
run.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
const raw = await tryReadRegularFileUtf8(postLaunchConfigPath, {
|
const raw = await tryReadRegularFileUtf8(postLaunchConfigPath, {
|
||||||
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
timeoutMs: TEAM_JSON_READ_TIMEOUT_MS,
|
||||||
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
maxBytes: TEAM_CONFIG_MAX_BYTES,
|
||||||
|
|
@ -35787,9 +35842,17 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
const configuredConfigPath = this.resolveSafeTeamStoragePath(getTeamsBasePath(), run.teamName, 'config.json');
|
const configuredConfigPath = this.resolveSafeTeamStoragePath(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
run.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
const defaultTeamsBasePath = path.join(getAutoDetectedClaudeBasePath(), 'teams');
|
const defaultTeamsBasePath = path.join(getAutoDetectedClaudeBasePath(), 'teams');
|
||||||
const defaultConfigPath = this.resolveSafeTeamStoragePath(defaultTeamsBasePath, run.teamName, 'config.json');
|
const defaultConfigPath = this.resolveSafeTeamStoragePath(
|
||||||
|
defaultTeamsBasePath,
|
||||||
|
run.teamName,
|
||||||
|
'config.json'
|
||||||
|
);
|
||||||
const combinedLogs = buildCombinedLogs(run.stdoutBuffer, run.stderrBuffer);
|
const combinedLogs = buildCombinedLogs(run.stdoutBuffer, run.stderrBuffer);
|
||||||
const cleanupHint = logsSuggestShutdownOrCleanup(combinedLogs)
|
const cleanupHint = logsSuggestShutdownOrCleanup(combinedLogs)
|
||||||
? ' CLI output suggests the team was shut down / cleaned up, so no persisted config was left on disk.'
|
? ' CLI output suggests the team was shut down / cleaned up, so no persisted config was left on disk.'
|
||||||
|
|
|
||||||
|
|
@ -600,6 +600,10 @@ export function isBootstrapMemberEvidenceCurrentForMember(
|
||||||
typeof current.runtimeRunId === 'string' ? current.runtimeRunId.trim() : '';
|
typeof current.runtimeRunId === 'string' ? current.runtimeRunId.trim() : '';
|
||||||
const bootstrapRuntimeRunId =
|
const bootstrapRuntimeRunId =
|
||||||
typeof bootstrapMember.runtimeRunId === 'string' ? bootstrapMember.runtimeRunId.trim() : '';
|
typeof bootstrapMember.runtimeRunId === 'string' ? bootstrapMember.runtimeRunId.trim() : '';
|
||||||
|
const hasSameRuntimeRunId =
|
||||||
|
currentRuntimeRunId.length > 0 &&
|
||||||
|
bootstrapRuntimeRunId.length > 0 &&
|
||||||
|
currentRuntimeRunId === bootstrapRuntimeRunId;
|
||||||
if (
|
if (
|
||||||
currentRuntimeRunId.length > 0 &&
|
currentRuntimeRunId.length > 0 &&
|
||||||
bootstrapRuntimeRunId.length > 0 &&
|
bootstrapRuntimeRunId.length > 0 &&
|
||||||
|
|
@ -631,10 +635,18 @@ export function isBootstrapMemberEvidenceCurrentForMember(
|
||||||
const hasDurableSpawnBoundary =
|
const hasDurableSpawnBoundary =
|
||||||
Number.isFinite(firstSpawnAcceptedMs) &&
|
Number.isFinite(firstSpawnAcceptedMs) &&
|
||||||
(!Number.isFinite(lastEvaluatedMs) || firstSpawnAcceptedMs <= lastEvaluatedMs);
|
(!Number.isFinite(lastEvaluatedMs) || firstSpawnAcceptedMs <= lastEvaluatedMs);
|
||||||
const boundaryMs = hasDurableSpawnBoundary ? firstSpawnAcceptedMs : NaN;
|
const currentBoundaryMs = hasDurableSpawnBoundary ? firstSpawnAcceptedMs : NaN;
|
||||||
const hasCompatibleRuntimeRunIdForSkew =
|
const sameRunBootstrapBoundaryMs =
|
||||||
currentRuntimeRunId.length === 0 ||
|
evidenceKind === 'confirmation' && hasSameRuntimeRunId && hasDurableBootstrapSpawnAcceptedAt
|
||||||
(bootstrapRuntimeRunId.length > 0 && currentRuntimeRunId === bootstrapRuntimeRunId);
|
? bootstrapFirstSpawnAcceptedMs
|
||||||
|
: NaN;
|
||||||
|
const boundaryMs =
|
||||||
|
Number.isFinite(currentBoundaryMs) && Number.isFinite(sameRunBootstrapBoundaryMs)
|
||||||
|
? Math.min(currentBoundaryMs, sameRunBootstrapBoundaryMs)
|
||||||
|
: Number.isFinite(currentBoundaryMs)
|
||||||
|
? currentBoundaryMs
|
||||||
|
: sameRunBootstrapBoundaryMs;
|
||||||
|
const hasCompatibleRuntimeRunIdForSkew = currentRuntimeRunId.length === 0 || hasSameRuntimeRunId;
|
||||||
const withinBootstrapConfirmationClockSkew =
|
const withinBootstrapConfirmationClockSkew =
|
||||||
evidenceKind === 'confirmation' &&
|
evidenceKind === 'confirmation' &&
|
||||||
Number.isFinite(boundaryMs) &&
|
Number.isFinite(boundaryMs) &&
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,35 @@ describe('TeamProvisioningOpenCodeRuntimeEvidencePolicy', () => {
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts same-run bootstrap confirmation before delayed app acceptance past skew', () => {
|
||||||
|
const current = {
|
||||||
|
firstSpawnAcceptedAt: '2026-05-24T09:25:52.497Z',
|
||||||
|
lastEvaluatedAt: '2026-05-24T09:31:05.525Z',
|
||||||
|
runtimeRunId: 'run-process-table-unavailable-skew',
|
||||||
|
};
|
||||||
|
const bootstrapMember = {
|
||||||
|
firstSpawnAcceptedAt: '2026-05-24T09:25:33.388Z',
|
||||||
|
lastHeartbeatAt: '2026-05-24T09:25:42.494Z',
|
||||||
|
lastRuntimeAliveAt: '2026-05-24T09:25:42.494Z',
|
||||||
|
lastEvaluatedAt: '2026-05-24T09:25:42.494Z',
|
||||||
|
runtimeRunId: 'run-process-table-unavailable-skew',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'confirmation')
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isBootstrapMemberEvidenceCurrentForMember(
|
||||||
|
current,
|
||||||
|
{ ...bootstrapMember, runtimeRunId: 'previous-run' },
|
||||||
|
'confirmation'
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
expect(isBootstrapMemberEvidenceCurrentForMember(current, bootstrapMember, 'acceptance')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('classifies recoverable persisted OpenCode runtime candidates', () => {
|
it('classifies recoverable persisted OpenCode runtime candidates', () => {
|
||||||
expect(
|
expect(
|
||||||
isRecoverablePersistedOpenCodeRuntimeCandidate(makePersisted({ runtimeSessionId: 'rt-1' }))
|
isRecoverablePersistedOpenCodeRuntimeCandidate(makePersisted({ runtimeSessionId: 'rt-1' }))
|
||||||
|
|
|
||||||
|
|
@ -20579,6 +20579,105 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('heals native runtime proof that predates delayed app acceptance but follows bootstrap boundary', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'zz-unit-native-proof-delayed-app-acceptance-heals';
|
||||||
|
const leadSessionId = 'lead-session';
|
||||||
|
const projectPath = '/Users/test/proj';
|
||||||
|
const bootstrapExpectedAfter = '2026-05-28T14:32:47.928Z';
|
||||||
|
const proofAt = '2026-05-28T14:32:54.123Z';
|
||||||
|
const appAcceptedAt = '2026-05-28T14:33:04.126Z';
|
||||||
|
const cleanupAt = '2026-05-28T14:39:22.768Z';
|
||||||
|
const proofToken = 'proof-token-tom-native';
|
||||||
|
const bootstrapRunId = 'run-native-delayed-acceptance';
|
||||||
|
const contextHash = 'c'.repeat(64);
|
||||||
|
const briefingHash = 'd'.repeat(64);
|
||||||
|
const runtimePid = 48_518;
|
||||||
|
const runtimeEventsPath = path.join(tempTeamsBase, teamName, 'runtime', 'tom.runtime.jsonl');
|
||||||
|
const processTableReason =
|
||||||
|
'runtime pid could not be verified because process table is unavailable';
|
||||||
|
|
||||||
|
writeLaunchConfig(teamName, projectPath, leadSessionId, ['tom']);
|
||||||
|
const configPath = path.join(tempTeamsBase, teamName, 'config.json');
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
|
||||||
|
members: Array<Record<string, unknown>>;
|
||||||
|
};
|
||||||
|
config.members = config.members.map((member) =>
|
||||||
|
member.name === 'tom'
|
||||||
|
? {
|
||||||
|
...member,
|
||||||
|
agentId: `tom@${teamName}`,
|
||||||
|
backendType: 'process',
|
||||||
|
tmuxPaneId: `process:${runtimePid}`,
|
||||||
|
runtimePid,
|
||||||
|
bootstrapExpectedAfter,
|
||||||
|
bootstrapProofToken: proofToken,
|
||||||
|
bootstrapRunId,
|
||||||
|
bootstrapProofMode: 'native_app_managed_context',
|
||||||
|
bootstrapContextHash: contextHash,
|
||||||
|
bootstrapBriefingHash: briefingHash,
|
||||||
|
bootstrapRuntimeEventsPath: runtimeEventsPath,
|
||||||
|
}
|
||||||
|
: member
|
||||||
|
);
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
|
||||||
|
const snapshot = createPersistedLaunchSnapshot({
|
||||||
|
teamName,
|
||||||
|
leadSessionId,
|
||||||
|
launchPhase: 'finished',
|
||||||
|
expectedMembers: ['tom'],
|
||||||
|
members: {
|
||||||
|
tom: {
|
||||||
|
name: 'tom',
|
||||||
|
launchState: 'failed_to_start',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: false,
|
||||||
|
runtimePid,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: true,
|
||||||
|
hardFailureReason: processTableReason,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic: processTableReason,
|
||||||
|
runtimeDiagnosticSeverity: 'warning',
|
||||||
|
firstSpawnAcceptedAt: appAcceptedAt,
|
||||||
|
runtimeLastSeenAt: cleanupAt,
|
||||||
|
lastEvaluatedAt: cleanupAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fs.mkdirSync(path.dirname(runtimeEventsPath), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
runtimeEventsPath,
|
||||||
|
`${JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
type: 'bootstrap_confirmed',
|
||||||
|
timestamp: proofAt,
|
||||||
|
pid: runtimePid,
|
||||||
|
teamName,
|
||||||
|
agentName: 'tom',
|
||||||
|
agentId: `tom@${teamName}`,
|
||||||
|
runId: bootstrapRunId,
|
||||||
|
bootstrapRunId,
|
||||||
|
source: 'native_app_managed_bootstrap_private_turn',
|
||||||
|
bootstrapProofToken: proofToken,
|
||||||
|
contextHash,
|
||||||
|
briefingHash,
|
||||||
|
})}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const result = await privateHarness(svc).applyBootstrapTranscriptEvidenceOverlay(snapshot);
|
||||||
|
|
||||||
|
expect(result?.members.tom).toMatchObject({
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
hardFailure: false,
|
||||||
|
hardFailureReason: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('heals cleanup-finalized launch failures when bootstrap-state confirms an Anthropic primary member', async () => {
|
it('heals cleanup-finalized launch failures when bootstrap-state confirms an Anthropic primary member', async () => {
|
||||||
allowConsoleLogs();
|
allowConsoleLogs();
|
||||||
const teamName = 'zz-unit-cleanup-finalized-bootstrap-state-heals';
|
const teamName = 'zz-unit-cleanup-finalized-bootstrap-state-heals';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue