fix(team): sync mixed launch state back into live runs

This commit is contained in:
777genius 2026-04-23 11:14:28 +03:00
parent b955901e15
commit fe830da37d
2 changed files with 115 additions and 1 deletions

View file

@ -6175,6 +6175,9 @@ export class TeamProvisioningService {
await this.persistLaunchStateSnapshot(run, run.provisioningComplete ? 'finished' : 'active');
const persisted = await this.launchStateStore.read(teamName);
if (persisted) {
this.syncRunMemberSpawnStatusesFromSnapshot(run, persisted);
}
const liveSnapshot =
this.buildLiveLaunchSnapshotForRun(run, run.provisioningComplete ? 'finished' : 'active') ??
snapshotFromRuntimeMemberStatuses({
@ -12005,6 +12008,21 @@ export class TeamProvisioningService {
return statuses;
}
private syncRunMemberSpawnStatusesFromSnapshot(
run: ProvisioningRun,
snapshot: PersistedTeamLaunchSnapshot
): void {
const memberNames = this.getPersistedLaunchMemberNames(snapshot);
const snapshotStatuses = snapshotToMemberSpawnStatuses(snapshot);
run.expectedMembers = memberNames;
for (const memberName of memberNames) {
const entry = snapshotStatuses[memberName];
if (entry) {
run.memberSpawnStatuses.set(memberName, entry);
}
}
}
private countRunPermissionPendingMembers(run: ProvisioningRun): number {
let count = 0;
for (const expected of run.expectedMembers ?? []) {
@ -12088,8 +12106,12 @@ export class TeamProvisioningService {
run: ProvisioningRun,
lane: MixedSecondaryRuntimeLaneState
): Promise<void> {
let snapshot: PersistedTeamLaunchSnapshot | null = null;
if (run.isLaunch) {
await this.persistLaunchStateSnapshot(run, this.getMixedSecondaryLaunchPhase(run));
snapshot = await this.persistLaunchStateSnapshot(run, this.getMixedSecondaryLaunchPhase(run));
}
if (snapshot) {
this.syncRunMemberSpawnStatusesFromSnapshot(run, snapshot);
}
if (!this.isCurrentTrackedRun(run)) {
return;

View file

@ -6299,4 +6299,96 @@ describe('TeamProvisioningService', () => {
launchState: 'starting',
});
});
it('syncs stale live mixed-lane failures from a healthier persisted snapshot', async () => {
const svc = new TeamProvisioningService();
const run = createMemberSpawnRun({
teamName: 'forge-labs-4',
runId: 'run-mixed-sync-1',
expectedMembers: ['alice', 'jack'],
memberSpawnStatuses: new Map([
[
'alice',
createMemberSpawnStatusEntry({
status: 'waiting',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: true,
bootstrapConfirmed: false,
hardFailure: false,
}),
],
[
'jack',
createMemberSpawnStatusEntry({
status: 'error',
launchState: 'failed_to_start',
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: true,
error: 'Teammate was never spawned during launch.',
hardFailureReason: 'Teammate was never spawned during launch.',
}),
],
]),
});
run.isLaunch = true;
const snapshot = createPersistedLaunchSnapshot({
teamName: 'forge-labs-4',
leadSessionId: 'lead-session',
launchPhase: 'finished',
expectedMembers: ['alice', 'jack'],
members: {
alice: {
name: 'alice',
launchState: 'runtime_pending_bootstrap',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: false,
hardFailure: false,
lastEvaluatedAt: '2026-04-23T08:08:27.067Z',
},
jack: {
name: 'jack',
providerId: 'opencode',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
lastEvaluatedAt: '2026-04-23T08:08:27.067Z',
},
},
updatedAt: '2026-04-23T08:08:27.067Z',
});
vi.spyOn(svc as any, 'persistLaunchStateSnapshot').mockResolvedValue(snapshot);
vi.spyOn(svc as any, 'isCurrentTrackedRun').mockReturnValue(true);
await (svc as any).publishMixedSecondaryLaneStatusChange(run, {
laneId: 'secondary:opencode:jack',
providerId: 'opencode',
member: {
name: 'jack',
providerId: 'opencode',
model: 'opencode/ling-2.6-flash-free',
},
runId: 'lane-run-jack',
state: 'finished',
result: null,
warnings: [],
diagnostics: [],
});
expect(run.memberSpawnStatuses.get('jack')).toMatchObject({
status: 'online',
launchState: 'confirmed_alive',
hardFailure: false,
hardFailureReason: undefined,
error: undefined,
bootstrapConfirmed: true,
runtimeAlive: true,
});
expect(run.expectedMembers).toEqual(['alice', 'jack']);
});
});