From 98751a2cfc35075a90b0bcac4b367431059bea8a Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 04:00:17 +0300 Subject: [PATCH] fix(team): prefer richer persisted launch snapshots --- .../services/team/TeamBootstrapStateReader.ts | 19 ++--- .../team/TeamBootstrapStateReader.test.ts | 81 +++++++++++++++++++ 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/main/services/team/TeamBootstrapStateReader.ts b/src/main/services/team/TeamBootstrapStateReader.ts index 25835fa5..4937e3ba 100644 --- a/src/main/services/team/TeamBootstrapStateReader.ts +++ b/src/main/services/team/TeamBootstrapStateReader.ts @@ -822,6 +822,7 @@ function isLaunchSnapshotLike(value: unknown): value is PersistedTeamLaunchSnaps } function getLaunchSnapshotRichness(snapshot: PersistedTeamLaunchSnapshot): number { + const persistedMemberCount = getPersistedLaunchMemberNames(snapshot).length; let metadataScore = 0; for (const member of Object.values(snapshot.members)) { if (!member || typeof member !== 'object') continue; @@ -835,13 +836,17 @@ function getLaunchSnapshotRichness(snapshot: PersistedTeamLaunchSnapshot): numbe if (member.launchIdentity) metadataScore += 6; } return ( - snapshot.expectedMembers.length * 10 + + persistedMemberCount * 10 + Object.keys(snapshot.members).length * 5 + metadataScore + (snapshot.bootstrapExpectedMembers?.length ? 20 : 0) ); } +function getPersistedLaunchMemberNames(snapshot: PersistedTeamLaunchSnapshot): string[] { + return Array.from(new Set([...snapshot.expectedMembers, ...Object.keys(snapshot.members)])); +} + export function shouldIgnoreTerminalBootstrapOnlyPendingSnapshot( snapshot: Pick, nowMs: number = Date.now() @@ -875,16 +880,12 @@ export function choosePreferredLaunchSnapshot( if (isLaunchSnapshotLike(bootstrapSnapshot) && isLaunchSnapshotLike(launchSnapshot)) { const bootstrapRichness = getLaunchSnapshotRichness(bootstrapSnapshot); const launchRichness = getLaunchSnapshotRichness(launchSnapshot); - if ( - launchRichness > bootstrapRichness && - launchSnapshot.expectedMembers.length >= bootstrapSnapshot.expectedMembers.length - ) { + const bootstrapMemberCount = getPersistedLaunchMemberNames(bootstrapSnapshot).length; + const launchMemberCount = getPersistedLaunchMemberNames(launchSnapshot).length; + if (launchRichness > bootstrapRichness && launchMemberCount >= bootstrapMemberCount) { return launchSnapshot as T; } - if ( - bootstrapRichness > launchRichness && - bootstrapSnapshot.expectedMembers.length >= launchSnapshot.expectedMembers.length - ) { + if (bootstrapRichness > launchRichness && bootstrapMemberCount >= launchMemberCount) { return bootstrapSnapshot as T; } } diff --git a/test/main/services/team/TeamBootstrapStateReader.test.ts b/test/main/services/team/TeamBootstrapStateReader.test.ts index 48c31258..2267c540 100644 --- a/test/main/services/team/TeamBootstrapStateReader.test.ts +++ b/test/main/services/team/TeamBootstrapStateReader.test.ts @@ -522,4 +522,85 @@ describe('TeamBootstrapStateReader', () => { expect(preferred).toBeNull(); nowSpy.mockRestore(); }); + + it('prefers richer canonical launch snapshots when persisted members outgrow stale expectedMembers', () => { + const preferred = choosePreferredLaunchSnapshot( + { + version: 2, + teamName: 'demo', + updatedAt: '2026-04-23T10:05:00.000Z', + launchPhase: 'running', + expectedMembers: ['alice', 'bob'], + members: { + alice: { + name: 'alice', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + lastEvaluatedAt: '2026-04-23T10:05:00.000Z', + }, + }, + summary: { + confirmedCount: 1, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_pending', + }, + { + version: 2, + teamName: 'demo', + updatedAt: '2026-04-23T10:00:00.000Z', + launchPhase: 'running', + expectedMembers: ['alice'], + members: { + alice: { + name: 'alice', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + lastEvaluatedAt: '2026-04-23T10:00:00.000Z', + launchIdentity: { + providerId: 'codex', + providerBackendId: 'codex-native', + source: 'codex-runtime', + }, + }, + bob: { + name: 'bob', + launchState: 'runtime_pending_bootstrap', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: false, + hardFailure: false, + lastEvaluatedAt: '2026-04-23T10:00:00.000Z', + laneId: 'secondary:opencode:bob', + laneKind: 'secondary', + laneOwnerProviderId: 'opencode', + }, + }, + summary: { + confirmedCount: 1, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_pending', + } + ); + + expect(preferred).toMatchObject({ + updatedAt: '2026-04-23T10:00:00.000Z', + members: { + bob: { + laneId: 'secondary:opencode:bob', + }, + }, + }); + }); });