fix(team): gate launch readiness on first real turn
This commit is contained in:
parent
555e805f7b
commit
f882970b6c
2 changed files with 428 additions and 65 deletions
|
|
@ -1062,6 +1062,15 @@ function getRunRuntimeFailureLabel(run: ProvisioningRun): string {
|
||||||
return getCliFlavorUiOptions(getConfiguredCliFlavor()).displayName;
|
return getCliFlavorUiOptions(getConfiguredCliFlavor()).displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMissingCliError(): Error {
|
||||||
|
if (getConfiguredCliFlavor() === 'agent_teams_orchestrator') {
|
||||||
|
return new Error(
|
||||||
|
'Multimodel runtime not found. The packaged app must include resources/runtime/claude-multimodel, or development must provide CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Error('Claude CLI not found; install it or provide a valid path');
|
||||||
|
}
|
||||||
|
|
||||||
function buildProviderCliCommandArgs(providerArgs: string[], args: string[]): string[] {
|
function buildProviderCliCommandArgs(providerArgs: string[], args: string[]): string[] {
|
||||||
return mergeJsonSettingsArgs([...providerArgs, ...args]);
|
return mergeJsonSettingsArgs([...providerArgs, ...args]);
|
||||||
}
|
}
|
||||||
|
|
@ -1861,6 +1870,9 @@ interface ProvisioningRun {
|
||||||
fsPhase: 'waiting_config' | 'waiting_members' | 'waiting_tasks' | 'all_files_found';
|
fsPhase: 'waiting_config' | 'waiting_members' | 'waiting_tasks' | 'all_files_found';
|
||||||
waitingTasksSince: number | null;
|
waitingTasksSince: number | null;
|
||||||
provisioningComplete: boolean;
|
provisioningComplete: boolean;
|
||||||
|
processClosed: boolean;
|
||||||
|
requiresFirstRealTurnSuccess: boolean;
|
||||||
|
firstRealTurnSucceeded: boolean;
|
||||||
/** Path to the generated MCP config file for later cleanup. */
|
/** Path to the generated MCP config file for later cleanup. */
|
||||||
mcpConfigPath: string | null;
|
mcpConfigPath: string | null;
|
||||||
/** Path to the deterministic bootstrap spec file for later cleanup. */
|
/** Path to the deterministic bootstrap spec file for later cleanup. */
|
||||||
|
|
@ -15344,7 +15356,7 @@ export class TeamProvisioningService {
|
||||||
const providerId = resolveTeamProviderId(input.configuredMember.providerId);
|
const providerId = resolveTeamProviderId(input.configuredMember.providerId);
|
||||||
const claudePath = await ClaudeBinaryResolver.resolve();
|
const claudePath = await ClaudeBinaryResolver.resolve();
|
||||||
if (!claudePath) {
|
if (!claudePath) {
|
||||||
throw new Error('Claude CLI not found; install it or provide a valid path');
|
throw buildMissingCliError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = this.resolveDirectRestartRuntimeCwd({
|
const cwd = this.resolveDirectRestartRuntimeCwd({
|
||||||
|
|
@ -15486,7 +15498,7 @@ export class TeamProvisioningService {
|
||||||
const providerId = resolveTeamProviderId(input.configuredMember.providerId);
|
const providerId = resolveTeamProviderId(input.configuredMember.providerId);
|
||||||
const claudePath = input.run.spawnContext?.claudePath ?? (await ClaudeBinaryResolver.resolve());
|
const claudePath = input.run.spawnContext?.claudePath ?? (await ClaudeBinaryResolver.resolve());
|
||||||
if (!claudePath) {
|
if (!claudePath) {
|
||||||
throw new Error('Claude CLI not found; install it or provide a valid path');
|
throw buildMissingCliError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = this.resolveDirectRestartRuntimeCwd({
|
const cwd = this.resolveDirectRestartRuntimeCwd({
|
||||||
|
|
@ -17895,7 +17907,7 @@ export class TeamProvisioningService {
|
||||||
const cached = this.getFreshCachedProbeResult(targetCwdForValidation, providerId);
|
const cached = this.getFreshCachedProbeResult(targetCwdForValidation, providerId);
|
||||||
const probeResult = cached ?? (await this.getCachedOrProbeResult(targetCwd, providerId));
|
const probeResult = cached ?? (await this.getCachedOrProbeResult(targetCwd, providerId));
|
||||||
if (!probeResult?.claudePath) {
|
if (!probeResult?.claudePath) {
|
||||||
throw new Error('Claude CLI not found; install it or provide a valid path');
|
throw buildMissingCliError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerLabel = getTeamProviderLabel(providerId);
|
const providerLabel = getTeamProviderLabel(providerId);
|
||||||
|
|
@ -19594,6 +19606,7 @@ export class TeamProvisioningService {
|
||||||
`[${run.teamName}] Respawned CLI process after auth failure (pid=${child.pid ?? '?'})`
|
`[${run.teamName}] Respawned CLI process after auth failure (pid=${child.pid ?? '?'})`
|
||||||
);
|
);
|
||||||
run.child = child;
|
run.child = child;
|
||||||
|
run.processClosed = false;
|
||||||
run.authRetryInProgress = false;
|
run.authRetryInProgress = false;
|
||||||
|
|
||||||
updateProgress(run, 'spawning', 'CLI respawned — sending prompt', {
|
updateProgress(run, 'spawning', 'CLI respawned — sending prompt', {
|
||||||
|
|
@ -19897,7 +19910,7 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
const claudePath = await ClaudeBinaryResolver.resolve();
|
const claudePath = await ClaudeBinaryResolver.resolve();
|
||||||
if (!claudePath) {
|
if (!claudePath) {
|
||||||
throw new Error('Claude CLI not found; install it or provide a valid path');
|
throw buildMissingCliError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeAuthMaterialId = randomUUID();
|
const runtimeAuthMaterialId = randomUUID();
|
||||||
|
|
@ -20087,6 +20100,9 @@ export class TeamProvisioningService {
|
||||||
apiErrorWarningEmitted: false,
|
apiErrorWarningEmitted: false,
|
||||||
waitingTasksSince: null,
|
waitingTasksSince: null,
|
||||||
provisioningComplete: false,
|
provisioningComplete: false,
|
||||||
|
processClosed: false,
|
||||||
|
requiresFirstRealTurnSuccess: false,
|
||||||
|
firstRealTurnSucceeded: false,
|
||||||
mcpConfigPath: null,
|
mcpConfigPath: null,
|
||||||
bootstrapSpecPath: null,
|
bootstrapSpecPath: null,
|
||||||
bootstrapUserPromptPath: null,
|
bootstrapUserPromptPath: null,
|
||||||
|
|
@ -20250,6 +20266,7 @@ export class TeamProvisioningService {
|
||||||
bootstrapUserPromptPath =
|
bootstrapUserPromptPath =
|
||||||
await writeDeterministicBootstrapUserPromptFile(initialUserPrompt);
|
await writeDeterministicBootstrapUserPromptFile(initialUserPrompt);
|
||||||
run.bootstrapUserPromptPath = bootstrapUserPromptPath;
|
run.bootstrapUserPromptPath = bootstrapUserPromptPath;
|
||||||
|
run.requiresFirstRealTurnSuccess = true;
|
||||||
}
|
}
|
||||||
emitProvisioningCheckpoint(run, 'Writing MCP config file');
|
emitProvisioningCheckpoint(run, 'Writing MCP config file');
|
||||||
mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile(request.cwd);
|
mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile(request.cwd);
|
||||||
|
|
@ -20397,6 +20414,7 @@ export class TeamProvisioningService {
|
||||||
});
|
});
|
||||||
run.onProgress(run.progress);
|
run.onProgress(run.progress);
|
||||||
run.child = child;
|
run.child = child;
|
||||||
|
run.processClosed = false;
|
||||||
run.spawnContext = {
|
run.spawnContext = {
|
||||||
claudePath,
|
claudePath,
|
||||||
args: spawnArgs,
|
args: spawnArgs,
|
||||||
|
|
@ -21127,7 +21145,7 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
claudePath = await ClaudeBinaryResolver.resolve();
|
claudePath = await ClaudeBinaryResolver.resolve();
|
||||||
if (!claudePath) {
|
if (!claudePath) {
|
||||||
throw new Error('Claude CLI not found; install it or provide a valid path');
|
throw buildMissingCliError();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Restore pre-launch backup so config.json is not left in normalized (lead-only) state
|
// Restore pre-launch backup so config.json is not left in normalized (lead-only) state
|
||||||
|
|
@ -21352,6 +21370,9 @@ export class TeamProvisioningService {
|
||||||
apiErrorWarningEmitted: false,
|
apiErrorWarningEmitted: false,
|
||||||
waitingTasksSince: null,
|
waitingTasksSince: null,
|
||||||
provisioningComplete: false,
|
provisioningComplete: false,
|
||||||
|
processClosed: false,
|
||||||
|
requiresFirstRealTurnSuccess: false,
|
||||||
|
firstRealTurnSucceeded: false,
|
||||||
mcpConfigPath: null,
|
mcpConfigPath: null,
|
||||||
bootstrapSpecPath: null,
|
bootstrapSpecPath: null,
|
||||||
bootstrapUserPromptPath: null,
|
bootstrapUserPromptPath: null,
|
||||||
|
|
@ -21510,6 +21531,7 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
bootstrapUserPromptPath = await writeDeterministicBootstrapUserPromptFile(prompt);
|
bootstrapUserPromptPath = await writeDeterministicBootstrapUserPromptFile(prompt);
|
||||||
run.bootstrapUserPromptPath = bootstrapUserPromptPath;
|
run.bootstrapUserPromptPath = bootstrapUserPromptPath;
|
||||||
|
run.requiresFirstRealTurnSuccess = true;
|
||||||
emitProvisioningCheckpoint(run, 'Writing MCP config file');
|
emitProvisioningCheckpoint(run, 'Writing MCP config file');
|
||||||
mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile(request.cwd);
|
mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile(request.cwd);
|
||||||
run.mcpConfigPath = mcpConfigPath;
|
run.mcpConfigPath = mcpConfigPath;
|
||||||
|
|
@ -21688,6 +21710,7 @@ export class TeamProvisioningService {
|
||||||
});
|
});
|
||||||
run.onProgress(run.progress);
|
run.onProgress(run.progress);
|
||||||
run.child = child;
|
run.child = child;
|
||||||
|
run.processClosed = false;
|
||||||
run.spawnContext = {
|
run.spawnContext = {
|
||||||
claudePath,
|
claudePath,
|
||||||
args: finalLaunchArgs,
|
args: finalLaunchArgs,
|
||||||
|
|
@ -26200,6 +26223,8 @@ export class TeamProvisioningService {
|
||||||
run.processKilled ||
|
run.processKilled ||
|
||||||
isTerminalFailureProvisioningState(run.progress.state) ||
|
isTerminalFailureProvisioningState(run.progress.state) ||
|
||||||
this.isProvisioningRunPromotedToAlive(run) ||
|
this.isProvisioningRunPromotedToAlive(run) ||
|
||||||
|
this.hasPendingDeterministicFirstRealTurn(run) ||
|
||||||
|
!this.isProvisioningRunStillPromotable(run) ||
|
||||||
this.provisioningRunByTeam.get(run.teamName) !== run.runId
|
this.provisioningRunByTeam.get(run.teamName) !== run.runId
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -26210,6 +26235,9 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = await readBootstrapLaunchSnapshot(run.teamName).catch(() => null);
|
const snapshot = await readBootstrapLaunchSnapshot(run.teamName).catch(() => null);
|
||||||
|
if (!this.isProvisioningRunStillPromotable(run)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!snapshot ||
|
!snapshot ||
|
||||||
(snapshot.launchPhase !== 'finished' && snapshot.launchPhase !== 'reconciled')
|
(snapshot.launchPhase !== 'finished' && snapshot.launchPhase !== 'reconciled')
|
||||||
|
|
@ -26240,6 +26268,9 @@ export class TeamProvisioningService {
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
if (!this.isProvisioningRunStillPromotable(run)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const failedSpawnMembers = memberNames
|
const failedSpawnMembers = memberNames
|
||||||
.filter((memberName) => snapshot.members[memberName]?.launchState === 'failed_to_start')
|
.filter((memberName) => snapshot.members[memberName]?.launchState === 'failed_to_start')
|
||||||
|
|
@ -26300,6 +26331,46 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasPendingDeterministicFirstRealTurn(run: ProvisioningRun): boolean {
|
||||||
|
return (
|
||||||
|
run.deterministicBootstrap && run.requiresFirstRealTurnSuccess && !run.firstRealTurnSucceeded
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isProvisioningRunStillPromotable(run: ProvisioningRun): boolean {
|
||||||
|
if (this.runs.get(run.runId) !== run) return false;
|
||||||
|
if (this.provisioningRunByTeam.get(run.teamName) !== run.runId) return false;
|
||||||
|
if (
|
||||||
|
run.cancelRequested ||
|
||||||
|
run.processKilled ||
|
||||||
|
run.processClosed ||
|
||||||
|
run.finalizingByTimeout ||
|
||||||
|
run.authRetryInProgress
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
run.progress.state === 'ready' ||
|
||||||
|
run.progress.state === 'disconnected' ||
|
||||||
|
run.progress.state === 'cancelled' ||
|
||||||
|
isTerminalFailureProvisioningState(run.progress.state)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!run.child || run.child.killed) return false;
|
||||||
|
const stdin = run.child.stdin as
|
||||||
|
| (NodeJS.WritableStream & {
|
||||||
|
destroyed?: boolean;
|
||||||
|
writableEnded?: boolean;
|
||||||
|
writable?: boolean;
|
||||||
|
})
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
if (!stdin) return false;
|
||||||
|
if (stdin.destroyed || stdin.writableEnded || stdin.writable === false) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private syncRunMemberSpawnStatusesFromSnapshot(
|
private syncRunMemberSpawnStatusesFromSnapshot(
|
||||||
run: ProvisioningRun,
|
run: ProvisioningRun,
|
||||||
snapshot: PersistedTeamLaunchSnapshot
|
snapshot: PersistedTeamLaunchSnapshot
|
||||||
|
|
@ -30027,7 +30098,7 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!run.provisioningComplete && !run.cancelRequested) {
|
if (!run.requiresFirstRealTurnSuccess && !run.provisioningComplete && !run.cancelRequested) {
|
||||||
void this.handleProvisioningTurnComplete(run).catch((error: unknown) => {
|
void this.handleProvisioningTurnComplete(run).catch((error: unknown) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`[${run.teamName}] deterministic bootstrap completion handler failed: ${
|
`[${run.teamName}] deterministic bootstrap completion handler failed: ${
|
||||||
|
|
@ -30305,6 +30376,9 @@ export class TeamProvisioningService {
|
||||||
})();
|
})();
|
||||||
if (subtype === 'success') {
|
if (subtype === 'success') {
|
||||||
logger.info(`[${run.teamName}] stream-json result: success — turn complete, process alive`);
|
logger.info(`[${run.teamName}] stream-json result: success — turn complete, process alive`);
|
||||||
|
if (!run.provisioningComplete) {
|
||||||
|
run.firstRealTurnSucceeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract contextWindow from modelUsage if available (SDKResultSuccess.modelUsage)
|
// Extract contextWindow from modelUsage if available (SDKResultSuccess.modelUsage)
|
||||||
const modelUsageObj = (msg.modelUsage ??
|
const modelUsageObj = (msg.modelUsage ??
|
||||||
|
|
@ -31741,8 +31815,8 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the first stream-json turn completes successfully.
|
* Called once provisioning has a promotable readiness signal.
|
||||||
* Verifies provisioning files exist and marks as ready.
|
* For deterministic runs with a deferred first task, that signal must be result.success.
|
||||||
* Process stays alive for subsequent tasks.
|
* Process stays alive for subsequent tasks.
|
||||||
*/
|
*/
|
||||||
private async handleProvisioningTurnComplete(run: ProvisioningRun): Promise<void> {
|
private async handleProvisioningTurnComplete(run: ProvisioningRun): Promise<void> {
|
||||||
|
|
@ -31755,6 +31829,12 @@ export class TeamProvisioningService {
|
||||||
run.progress.state === 'failed'
|
run.progress.state === 'failed'
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
if (
|
||||||
|
this.hasPendingDeterministicFirstRealTurn(run) ||
|
||||||
|
!this.isProvisioningRunStillPromotable(run)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent false "ready" when auth failure was printed in CLI output but the filesystem monitor
|
// Prevent false "ready" when auth failure was printed in CLI output but the filesystem monitor
|
||||||
// already observed files on disk. We only re-check stderr plus a trailing non-JSON stdout
|
// already observed files on disk. We only re-check stderr plus a trailing non-JSON stdout
|
||||||
|
|
@ -31856,7 +31936,10 @@ export class TeamProvisioningService {
|
||||||
const hasPendingBootstrap =
|
const hasPendingBootstrap =
|
||||||
!hasSpawnFailures &&
|
!hasSpawnFailures &&
|
||||||
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
|
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
|
||||||
if (this.isProvisioningRunPromotedToAlive(run)) {
|
if (
|
||||||
|
this.isProvisioningRunPromotedToAlive(run) ||
|
||||||
|
!this.isProvisioningRunStillPromotable(run)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const readyMessage = hasSpawnFailures
|
const readyMessage = hasSpawnFailures
|
||||||
|
|
@ -32039,7 +32122,7 @@ export class TeamProvisioningService {
|
||||||
const hasPendingBootstrap =
|
const hasPendingBootstrap =
|
||||||
!hasSpawnFailures &&
|
!hasSpawnFailures &&
|
||||||
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
|
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
|
||||||
if (this.isProvisioningRunPromotedToAlive(run)) {
|
if (this.isProvisioningRunPromotedToAlive(run) || !this.isProvisioningRunStillPromotable(run)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const progress = updateProgress(
|
const progress = updateProgress(
|
||||||
|
|
@ -32894,9 +32977,6 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
if (registeredMembers >= primaryProvisioningMemberCount) {
|
if (registeredMembers >= primaryProvisioningMemberCount) {
|
||||||
run.fsPhase = 'all_files_found';
|
run.fsPhase = 'all_files_found';
|
||||||
if (!run.provisioningComplete) {
|
|
||||||
void this.handleProvisioningTurnComplete(run);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32904,9 +32984,6 @@ export class TeamProvisioningService {
|
||||||
if (primaryProvisioningMemberCount === 0) {
|
if (primaryProvisioningMemberCount === 0) {
|
||||||
if (run.deterministicBootstrap) {
|
if (run.deterministicBootstrap) {
|
||||||
run.fsPhase = 'all_files_found';
|
run.fsPhase = 'all_files_found';
|
||||||
if (!run.provisioningComplete) {
|
|
||||||
void this.handleProvisioningTurnComplete(run);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
run.fsPhase = 'waiting_tasks';
|
run.fsPhase = 'waiting_tasks';
|
||||||
const progress = updateProgress(run, 'finalizing', 'Solo team, preparing workspace');
|
const progress = updateProgress(run, 'finalizing', 'Solo team, preparing workspace');
|
||||||
|
|
@ -32946,10 +33023,9 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
if (taskFound || taskFallbackExpired) {
|
if (taskFound || taskFallbackExpired) {
|
||||||
run.fsPhase = 'all_files_found';
|
run.fsPhase = 'all_files_found';
|
||||||
// Mark provisioning complete early — files are on disk,
|
// Legacy filesystem fallback - deterministic bootstrap waits for stream-json success.
|
||||||
// no need to wait for stream-json result.success.
|
|
||||||
// The process stays alive for subsequent tasks.
|
// The process stays alive for subsequent tasks.
|
||||||
if (!run.provisioningComplete) {
|
if (!run.deterministicBootstrap && !run.provisioningComplete) {
|
||||||
void this.handleProvisioningTurnComplete(run);
|
void this.handleProvisioningTurnComplete(run);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32996,7 +33072,6 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(typeof run.stdoutParserCarry === 'string' ? run.stdoutParserCarry.trim() : '') &&
|
(typeof run.stdoutParserCarry === 'string' ? run.stdoutParserCarry.trim() : '') &&
|
||||||
!run.stdoutParserCarryIsCompleteJson &&
|
!run.stdoutParserCarryIsCompleteJson &&
|
||||||
|
|
@ -33008,6 +33083,7 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.flushStdoutParserCarry(run);
|
this.flushStdoutParserCarry(run);
|
||||||
|
run.processClosed = true;
|
||||||
if (
|
if (
|
||||||
this.isProvisioningRunFailed(run) ||
|
this.isProvisioningRunFailed(run) ||
|
||||||
run.cancelRequested ||
|
run.cancelRequested ||
|
||||||
|
|
@ -33571,14 +33647,17 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.controlApiBaseUrlResolver();
|
const baseUrl = await this.controlApiBaseUrlResolver();
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error('Team control API resolver returned no base URL after startup.');
|
||||||
|
}
|
||||||
|
return baseUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
`Failed to resolve team control API base URL: ${
|
logger.error(`Failed to resolve team control API base URL: ${message}`);
|
||||||
error instanceof Error ? error.message : String(error)
|
throw new Error(
|
||||||
}`
|
`Team control API failed to start or publish its base URL. Team runtime commands require the desktop Control API. ${message}`
|
||||||
);
|
);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5336,9 +5336,9 @@ describe('TeamProvisioningService', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
|
||||||
await vi.waitFor(() => {
|
await run.mixedSecondaryLaneLaunchQueue;
|
||||||
expect(adapterLaunch).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
expect(adapterLaunch).toHaveBeenCalledTimes(1);
|
||||||
expect(adapterLaunch).toHaveBeenCalledWith(
|
expect(adapterLaunch).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
laneId: 'secondary:opencode:bob',
|
laneId: 'secondary:opencode:bob',
|
||||||
|
|
@ -12798,7 +12798,7 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('safe app launch matrix', () => {
|
describe('safe app launch matrix', () => {
|
||||||
it('does not wait for OpenCode secondary inboxes before completing primary filesystem readiness', async () => {
|
it('does not wait for OpenCode secondary inboxes before marking primary filesystem readiness', async () => {
|
||||||
const teamName = 'mixed-secondary-fs-readiness';
|
const teamName = 'mixed-secondary-fs-readiness';
|
||||||
const teamDir = path.join(tempTeamsBase, teamName);
|
const teamDir = path.join(tempTeamsBase, teamName);
|
||||||
fs.mkdirSync(path.join(teamDir, 'inboxes'), { recursive: true });
|
fs.mkdirSync(path.join(teamDir, 'inboxes'), { recursive: true });
|
||||||
|
|
@ -12848,8 +12848,8 @@ describe('TeamProvisioningService', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await vi.waitFor(() => expect(complete).toHaveBeenCalledTimes(1));
|
await vi.waitFor(() => expect(run.fsPhase).toBe('all_files_found'));
|
||||||
expect(run.fsPhase).toBe('all_files_found');
|
expect(complete).not.toHaveBeenCalled();
|
||||||
expect(run.onProgress).not.toHaveBeenCalledWith(
|
expect(run.onProgress).not.toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message: 'Prepared communication channels for 1/2 members',
|
message: 'Prepared communication channels for 1/2 members',
|
||||||
|
|
@ -14802,7 +14802,7 @@ describe('TeamProvisioningService', () => {
|
||||||
await svc.cancelProvisioning(runId);
|
await svc.cancelProvisioning(runId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('flushes a final newline-less bootstrap completion event before handling launch close', async () => {
|
it('flushes a final newline-less bootstrap completion event without promoting launch ready', async () => {
|
||||||
allowConsoleLogs();
|
allowConsoleLogs();
|
||||||
const teamName = 'launch-close-flushes-final-json-team';
|
const teamName = 'launch-close-flushes-final-json-team';
|
||||||
const leadSessionId = 'lead-session-final-json-flush';
|
const leadSessionId = 'lead-session-final-json-flush';
|
||||||
|
|
@ -14837,11 +14837,11 @@ describe('TeamProvisioningService', () => {
|
||||||
);
|
);
|
||||||
const complete = vi
|
const complete = vi
|
||||||
.spyOn(svc as any, 'handleProvisioningTurnComplete')
|
.spyOn(svc as any, 'handleProvisioningTurnComplete')
|
||||||
.mockImplementation(async (run: any) => {
|
.mockResolvedValue(undefined);
|
||||||
run.provisioningComplete = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { runId } = await svc.launchTeam({ teamName, cwd: tempClaudeRoot }, () => {});
|
const { runId } = await svc.launchTeam({ teamName, cwd: tempClaudeRoot }, () => {});
|
||||||
|
const run = (svc as any).runs.get(runId);
|
||||||
|
expect(run).toBeTruthy();
|
||||||
|
|
||||||
child.stdout.emit(
|
child.stdout.emit(
|
||||||
'data',
|
'data',
|
||||||
|
|
@ -14861,14 +14861,236 @@ describe('TeamProvisioningService', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(complete).not.toHaveBeenCalled();
|
expect(complete).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
(svc as any).flushStdoutParserCarry(run);
|
||||||
|
|
||||||
|
expect(complete).not.toHaveBeenCalled();
|
||||||
|
expect(run.lastDeterministicBootstrapSeq).toBe(1);
|
||||||
|
await svc.cancelProvisioning(runId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flushes a final newline-less success result and completes deterministic launch', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'launch-close-flushes-final-success-team';
|
||||||
|
const leadSessionId = 'lead-session-final-success-flush';
|
||||||
|
writeLaunchConfig(teamName, tempClaudeRoot, leadSessionId, ['alice']);
|
||||||
|
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
||||||
|
const child = createRunningChild();
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService(undefined, undefined, undefined, undefined, {
|
||||||
|
writeConfigFile: vi.fn(async () => '/mock/mcp-config-launch.json'),
|
||||||
|
removeConfigFile: vi.fn(async () => {}),
|
||||||
|
} as any);
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||||
|
env: { ANTHROPIC_API_KEY: 'test' },
|
||||||
|
authSource: 'anthropic_api_key',
|
||||||
|
}));
|
||||||
|
(svc as any).resolveLaunchExpectedMembers = vi.fn(async () => ({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
source: 'members-meta',
|
||||||
|
warning: undefined,
|
||||||
|
}));
|
||||||
|
(svc as any).normalizeTeamConfigForLaunch = vi.fn(async () => {});
|
||||||
|
(svc as any).assertConfigLeadOnlyForLaunch = vi.fn(async () => {});
|
||||||
|
(svc as any).updateConfigProjectPath = vi.fn(async () => {});
|
||||||
|
(svc as any).restorePrelaunchConfig = vi.fn(async () => {});
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).persistLaunchStateSnapshot = vi.fn(async () => {});
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
(svc as any).pathExists = vi.fn(async (targetPath: string) =>
|
||||||
|
targetPath.endsWith(`${leadSessionId}.jsonl`)
|
||||||
|
);
|
||||||
|
const complete = vi
|
||||||
|
.spyOn(svc as any, 'handleProvisioningTurnComplete')
|
||||||
|
.mockImplementation(async (run: any) => {
|
||||||
|
expect(run.processClosed).toBe(false);
|
||||||
|
expect(run.firstRealTurnSucceeded).toBe(true);
|
||||||
|
run.provisioningComplete = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { runId } = await svc.launchTeam({ teamName, cwd: tempClaudeRoot }, () => {});
|
||||||
|
|
||||||
|
child.stdout.emit(
|
||||||
|
'data',
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'result',
|
||||||
|
subtype: 'success',
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(complete).not.toHaveBeenCalled();
|
||||||
|
|
||||||
child.emit('close', 0);
|
child.emit('close', 0);
|
||||||
|
|
||||||
await vi.waitFor(() => expect(complete).toHaveBeenCalledTimes(1));
|
await vi.waitFor(() => expect(complete).toHaveBeenCalledTimes(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recovers ready progress when deterministic create finalization stalls after completed bootstrap-state', async () => {
|
it('does not promote deterministic launch from bootstrap completed before first real turn succeeds', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'bootstrap-completed-before-first-turn-team';
|
||||||
|
const leadSessionId = 'lead-session-bootstrap-only';
|
||||||
|
writeLaunchConfig(teamName, tempClaudeRoot, leadSessionId, ['alice']);
|
||||||
|
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
||||||
|
const child = createRunningChild();
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService(undefined, undefined, undefined, undefined, {
|
||||||
|
writeConfigFile: vi.fn(async () => '/mock/mcp-config-launch.json'),
|
||||||
|
removeConfigFile: vi.fn(async () => {}),
|
||||||
|
} as any);
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||||
|
env: { ANTHROPIC_API_KEY: 'test' },
|
||||||
|
authSource: 'anthropic_api_key',
|
||||||
|
}));
|
||||||
|
(svc as any).resolveLaunchExpectedMembers = vi.fn(async () => ({
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
source: 'members-meta',
|
||||||
|
warning: undefined,
|
||||||
|
}));
|
||||||
|
(svc as any).normalizeTeamConfigForLaunch = vi.fn(async () => {});
|
||||||
|
(svc as any).assertConfigLeadOnlyForLaunch = vi.fn(async () => {});
|
||||||
|
(svc as any).updateConfigProjectPath = vi.fn(async () => {});
|
||||||
|
(svc as any).restorePrelaunchConfig = vi.fn(async () => {});
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).persistLaunchStateSnapshot = vi.fn(async () => {});
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
(svc as any).pathExists = vi.fn(async (targetPath: string) =>
|
||||||
|
targetPath.endsWith(`${leadSessionId}.jsonl`)
|
||||||
|
);
|
||||||
|
const complete = vi
|
||||||
|
.spyOn(svc as any, 'handleProvisioningTurnComplete')
|
||||||
|
.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
let runId = '';
|
||||||
|
try {
|
||||||
|
const launch = await svc.launchTeam({ teamName, cwd: tempClaudeRoot }, () => {});
|
||||||
|
runId = launch.runId;
|
||||||
|
|
||||||
|
child.stdout.emit(
|
||||||
|
'data',
|
||||||
|
Buffer.from(
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: 'system',
|
||||||
|
subtype: 'team_bootstrap',
|
||||||
|
event: 'completed',
|
||||||
|
run_id: runId,
|
||||||
|
team_name: teamName,
|
||||||
|
seq: 1,
|
||||||
|
failed_members: [],
|
||||||
|
})}\n`,
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(complete).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
if (runId) {
|
||||||
|
await svc.cancelProvisioning(runId).catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('promotes deterministic create bootstrap completion when no first turn was enqueued', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'bootstrap-completed-no-first-turn-team';
|
||||||
|
const child = createRunningChild();
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
|
||||||
|
const mcpConfigBuilder = {
|
||||||
|
writeConfigFile: vi.fn(async () => '/mock/mcp-config-create.json'),
|
||||||
|
removeConfigFile: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
const membersMetaStore = {
|
||||||
|
writeMembers: vi.fn(async () => {}),
|
||||||
|
getMembers: vi.fn(async () => []),
|
||||||
|
};
|
||||||
|
const teamMetaStore = {
|
||||||
|
writeMeta: vi.fn(async () => {}),
|
||||||
|
deleteMeta: vi.fn(async () => {}),
|
||||||
|
getMeta: vi.fn(async () => null),
|
||||||
|
};
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
membersMetaStore as any,
|
||||||
|
undefined,
|
||||||
|
mcpConfigBuilder as any,
|
||||||
|
teamMetaStore as any
|
||||||
|
);
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||||
|
env: { CODEX_API_KEY: 'test' },
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
}));
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).pathExists = vi.fn(async () => false);
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
(svc as any).startStallWatchdog = vi.fn();
|
||||||
|
(svc as any).stopStallWatchdog = vi.fn();
|
||||||
|
(svc as any).resolveAndValidateLaunchIdentity = vi.fn(async () => ({
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
selectedModel: 'gpt-5.5',
|
||||||
|
selectedModelKind: 'explicit',
|
||||||
|
resolvedLaunchModel: 'gpt-5.5',
|
||||||
|
catalogId: 'gpt-5.5',
|
||||||
|
catalogSource: 'test',
|
||||||
|
catalogFetchedAt: '2026-05-07T00:00:00.000Z',
|
||||||
|
selectedEffort: 'medium',
|
||||||
|
resolvedEffort: 'medium',
|
||||||
|
selectedFastMode: null,
|
||||||
|
resolvedFastMode: null,
|
||||||
|
fastResolutionReason: null,
|
||||||
|
}));
|
||||||
|
const complete = vi
|
||||||
|
.spyOn(svc as any, 'handleProvisioningTurnComplete')
|
||||||
|
.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const { runId } = await svc.createTeam(
|
||||||
|
{
|
||||||
|
teamName,
|
||||||
|
cwd: tempClaudeRoot,
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
model: 'gpt-5.5',
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
const run = (svc as any).runs.get(runId);
|
||||||
|
expect(run).toBeTruthy();
|
||||||
|
expect(run.requiresFirstRealTurnSuccess).toBe(false);
|
||||||
|
|
||||||
|
child.stdout.emit(
|
||||||
|
'data',
|
||||||
|
Buffer.from(
|
||||||
|
`${JSON.stringify({
|
||||||
|
type: 'system',
|
||||||
|
subtype: 'team_bootstrap',
|
||||||
|
event: 'completed',
|
||||||
|
run_id: runId,
|
||||||
|
team_name: teamName,
|
||||||
|
seq: 1,
|
||||||
|
failed_members: [],
|
||||||
|
})}\n`,
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(complete).toHaveBeenCalledTimes(1));
|
||||||
|
expect(complete).toHaveBeenCalledWith(run);
|
||||||
|
await svc.cancelProvisioning(runId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recovers ready progress when deterministic finalization stalls after first real turn success', async () => {
|
||||||
allowConsoleLogs();
|
allowConsoleLogs();
|
||||||
vi.useFakeTimers();
|
|
||||||
const teamName = 'create-completed-bootstrap-finalization-stall';
|
const teamName = 'create-completed-bootstrap-finalization-stall';
|
||||||
const child = createRunningChild();
|
const child = createRunningChild();
|
||||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
||||||
|
|
@ -14920,9 +15142,6 @@ describe('TeamProvisioningService', () => {
|
||||||
resolvedFastMode: null,
|
resolvedFastMode: null,
|
||||||
fastResolutionReason: null,
|
fastResolutionReason: null,
|
||||||
}));
|
}));
|
||||||
const waitForValidConfig = vi.fn(() => new Promise(() => {}));
|
|
||||||
(svc as any).waitForValidConfig = waitForValidConfig;
|
|
||||||
|
|
||||||
const progressStates: string[] = [];
|
const progressStates: string[] = [];
|
||||||
const { runId } = await svc.createTeam(
|
const { runId } = await svc.createTeam(
|
||||||
{
|
{
|
||||||
|
|
@ -14940,10 +15159,9 @@ describe('TeamProvisioningService', () => {
|
||||||
const run = (svc as any).runs.get(runId);
|
const run = (svc as any).runs.get(runId);
|
||||||
expect(run).toBeTruthy();
|
expect(run).toBeTruthy();
|
||||||
run.deterministicBootstrap = true;
|
run.deterministicBootstrap = true;
|
||||||
const scheduleRecovery = vi.spyOn(
|
run.requiresFirstRealTurnSuccess = true;
|
||||||
svc as any,
|
run.firstRealTurnSucceeded = true;
|
||||||
'scheduleDeterministicBootstrapCompletionRecovery'
|
run.provisioningComplete = true;
|
||||||
);
|
|
||||||
|
|
||||||
writeBootstrapState(
|
writeBootstrapState(
|
||||||
teamName,
|
teamName,
|
||||||
|
|
@ -14954,26 +15172,6 @@ describe('TeamProvisioningService', () => {
|
||||||
new Date(Date.now() + 1_000).toISOString()
|
new Date(Date.now() + 1_000).toISOString()
|
||||||
);
|
);
|
||||||
|
|
||||||
child.stdout.emit(
|
|
||||||
'data',
|
|
||||||
Buffer.from(
|
|
||||||
`${JSON.stringify({
|
|
||||||
type: 'system',
|
|
||||||
subtype: 'team_bootstrap',
|
|
||||||
event: 'completed',
|
|
||||||
run_id: runId,
|
|
||||||
team_name: teamName,
|
|
||||||
seq: 1,
|
|
||||||
failed_members: [],
|
|
||||||
})}\n`,
|
|
||||||
'utf8'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.resolve();
|
|
||||||
await Promise.resolve();
|
|
||||||
expect(waitForValidConfig).toHaveBeenCalledTimes(1);
|
|
||||||
expect(scheduleRecovery).toHaveBeenCalledWith(run);
|
|
||||||
expect(progressStates.at(-1)).not.toBe('ready');
|
expect(progressStates.at(-1)).not.toBe('ready');
|
||||||
|
|
||||||
await (svc as any).recoverDeterministicBootstrapCompletion(run);
|
await (svc as any).recoverDeterministicBootstrapCompletion(run);
|
||||||
|
|
@ -14982,6 +15180,92 @@ describe('TeamProvisioningService', () => {
|
||||||
expect((svc as any).aliveRunByTeam.get(teamName)).toBe(runId);
|
expect((svc as any).aliveRunByTeam.get(teamName)).toBe(runId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not recover ready progress from completed bootstrap-state when the lead child is gone', async () => {
|
||||||
|
allowConsoleLogs();
|
||||||
|
const teamName = 'create-completed-bootstrap-dead-lead';
|
||||||
|
const child = createRunningChild();
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
|
||||||
|
const mcpConfigBuilder = {
|
||||||
|
writeConfigFile: vi.fn(async () => '/mock/mcp-config-create.json'),
|
||||||
|
removeConfigFile: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
const membersMetaStore = {
|
||||||
|
writeMembers: vi.fn(async () => {}),
|
||||||
|
getMembers: vi.fn(async () => []),
|
||||||
|
};
|
||||||
|
const teamMetaStore = {
|
||||||
|
writeMeta: vi.fn(async () => {}),
|
||||||
|
deleteMeta: vi.fn(async () => {}),
|
||||||
|
getMeta: vi.fn(async () => null),
|
||||||
|
};
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
membersMetaStore as any,
|
||||||
|
undefined,
|
||||||
|
mcpConfigBuilder as any,
|
||||||
|
teamMetaStore as any
|
||||||
|
);
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async () => ({
|
||||||
|
env: { CODEX_API_KEY: 'test' },
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
}));
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).pathExists = vi.fn(async () => false);
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
(svc as any).startStallWatchdog = vi.fn();
|
||||||
|
(svc as any).stopStallWatchdog = vi.fn();
|
||||||
|
(svc as any).resolveAndValidateLaunchIdentity = vi.fn(async () => ({
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
selectedModel: 'gpt-5.5',
|
||||||
|
selectedModelKind: 'explicit',
|
||||||
|
resolvedLaunchModel: 'gpt-5.5',
|
||||||
|
catalogId: 'gpt-5.5',
|
||||||
|
catalogSource: 'test',
|
||||||
|
catalogFetchedAt: '2026-05-07T00:00:00.000Z',
|
||||||
|
selectedEffort: 'medium',
|
||||||
|
resolvedEffort: 'medium',
|
||||||
|
selectedFastMode: null,
|
||||||
|
resolvedFastMode: null,
|
||||||
|
fastResolutionReason: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const progressStates: string[] = [];
|
||||||
|
const { runId } = await svc.createTeam(
|
||||||
|
{
|
||||||
|
teamName,
|
||||||
|
cwd: tempClaudeRoot,
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
model: 'gpt-5.5',
|
||||||
|
members: [{ name: 'alice' }],
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
progressStates.push(progress.state);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const run = (svc as any).runs.get(runId);
|
||||||
|
expect(run).toBeTruthy();
|
||||||
|
run.deterministicBootstrap = true;
|
||||||
|
run.provisioningComplete = true;
|
||||||
|
run.child = null;
|
||||||
|
|
||||||
|
writeBootstrapState(
|
||||||
|
teamName,
|
||||||
|
[{ name: 'alice', status: 'bootstrap_confirmed' }],
|
||||||
|
new Date(Date.now() + 1_000).toISOString()
|
||||||
|
);
|
||||||
|
|
||||||
|
await (svc as any).recoverDeterministicBootstrapCompletion(run);
|
||||||
|
|
||||||
|
expect(progressStates).not.toContain('ready');
|
||||||
|
expect((svc as any).aliveRunByTeam.get(teamName)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not verify provisioning again after flushing a final newline-less error result', async () => {
|
it('does not verify provisioning again after flushing a final newline-less error result', async () => {
|
||||||
allowConsoleLogs();
|
allowConsoleLogs();
|
||||||
const teamName = 'launch-close-flushes-final-error-team';
|
const teamName = 'launch-close-flushes-final-error-team';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue