fix(team): sync mixed launch state back into live runs
This commit is contained in:
parent
b955901e15
commit
fe830da37d
2 changed files with 115 additions and 1 deletions
|
|
@ -6175,6 +6175,9 @@ export class TeamProvisioningService {
|
||||||
await this.persistLaunchStateSnapshot(run, run.provisioningComplete ? 'finished' : 'active');
|
await this.persistLaunchStateSnapshot(run, run.provisioningComplete ? 'finished' : 'active');
|
||||||
|
|
||||||
const persisted = await this.launchStateStore.read(teamName);
|
const persisted = await this.launchStateStore.read(teamName);
|
||||||
|
if (persisted) {
|
||||||
|
this.syncRunMemberSpawnStatusesFromSnapshot(run, persisted);
|
||||||
|
}
|
||||||
const liveSnapshot =
|
const liveSnapshot =
|
||||||
this.buildLiveLaunchSnapshotForRun(run, run.provisioningComplete ? 'finished' : 'active') ??
|
this.buildLiveLaunchSnapshotForRun(run, run.provisioningComplete ? 'finished' : 'active') ??
|
||||||
snapshotFromRuntimeMemberStatuses({
|
snapshotFromRuntimeMemberStatuses({
|
||||||
|
|
@ -12005,6 +12008,21 @@ export class TeamProvisioningService {
|
||||||
return statuses;
|
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 {
|
private countRunPermissionPendingMembers(run: ProvisioningRun): number {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const expected of run.expectedMembers ?? []) {
|
for (const expected of run.expectedMembers ?? []) {
|
||||||
|
|
@ -12088,8 +12106,12 @@ export class TeamProvisioningService {
|
||||||
run: ProvisioningRun,
|
run: ProvisioningRun,
|
||||||
lane: MixedSecondaryRuntimeLaneState
|
lane: MixedSecondaryRuntimeLaneState
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
let snapshot: PersistedTeamLaunchSnapshot | null = null;
|
||||||
if (run.isLaunch) {
|
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)) {
|
if (!this.isCurrentTrackedRun(run)) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -6299,4 +6299,96 @@ describe('TeamProvisioningService', () => {
|
||||||
launchState: 'starting',
|
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']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue