From 9005deb05cfc53787d4f9fad758d0422904606e7 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 00:30:01 +0300 Subject: [PATCH] fix(team): prefer live launch truth over stale summary --- .../components/team/provisioningSteps.ts | 96 +++++++++++++------ .../teamProvisioningPresentation.test.ts | 65 +++++++++++++ 2 files changed, 133 insertions(+), 28 deletions(-) diff --git a/src/renderer/components/team/provisioningSteps.ts b/src/renderer/components/team/provisioningSteps.ts index 92d7df18..568174e0 100644 --- a/src/renderer/components/team/provisioningSteps.ts +++ b/src/renderer/components/team/provisioningSteps.ts @@ -51,39 +51,18 @@ function getSpawnEntry( return memberSpawnStatuses[memberName]; } -export function getLaunchJoinMilestonesFromMembers({ - members, - memberSpawnStatuses, - memberSpawnSnapshot, -}: { - members: readonly LaunchJoinMemberLike[]; +function summarizeLiveLaunchJoinMilestones(params: { + teammateNames: readonly string[]; memberSpawnStatuses?: MemberSpawnStatusCollection; - memberSpawnSnapshot?: Pick; -}): LaunchJoinMilestones { - const teammates = members.filter((member) => !member.removedAt && !isLeadMember(member)); - const expectedTeammateCount = memberSpawnSnapshot?.expectedMembers?.length ?? teammates.length; - const snapshotSummary = memberSpawnSnapshot?.summary; - - if (snapshotSummary) { - return { - expectedTeammateCount, - heartbeatConfirmedCount: snapshotSummary.confirmedCount, - processOnlyAliveCount: snapshotSummary.runtimeAlivePendingCount, - pendingSpawnCount: Math.max( - 0, - snapshotSummary.pendingCount - snapshotSummary.runtimeAlivePendingCount - ), - failedSpawnCount: snapshotSummary.failedCount, - }; - } - +}): Omit { + const { teammateNames, memberSpawnStatuses } = params; let heartbeatConfirmedCount = 0; let processOnlyAliveCount = 0; let pendingSpawnCount = 0; let failedSpawnCount = 0; - for (const member of teammates) { - const entry = getSpawnEntry(memberSpawnStatuses, member.name); + for (const memberName of teammateNames) { + const entry = getSpawnEntry(memberSpawnStatuses, memberName); if (!entry) { pendingSpawnCount += 1; continue; @@ -110,7 +89,6 @@ export function getLaunchJoinMilestonesFromMembers({ } return { - expectedTeammateCount, heartbeatConfirmedCount, processOnlyAliveCount, pendingSpawnCount, @@ -118,6 +96,68 @@ export function getLaunchJoinMilestonesFromMembers({ }; } +export function getLaunchJoinMilestonesFromMembers({ + members, + memberSpawnStatuses, + memberSpawnSnapshot, +}: { + members: readonly LaunchJoinMemberLike[]; + memberSpawnStatuses?: MemberSpawnStatusCollection; + memberSpawnSnapshot?: Pick; +}): LaunchJoinMilestones { + const teammates = members.filter((member) => !member.removedAt && !isLeadMember(member)); + const teammateNames = + memberSpawnSnapshot?.expectedMembers?.length && memberSpawnSnapshot.expectedMembers.length > 0 + ? memberSpawnSnapshot.expectedMembers + : teammates.map((member) => member.name); + const expectedTeammateCount = teammateNames.length; + const snapshotSummary = memberSpawnSnapshot?.summary; + const liveSummary = summarizeLiveLaunchJoinMilestones({ + teammateNames, + memberSpawnStatuses, + }); + + if (snapshotSummary) { + const snapshotMilestones = { + expectedTeammateCount, + heartbeatConfirmedCount: snapshotSummary.confirmedCount, + processOnlyAliveCount: snapshotSummary.runtimeAlivePendingCount, + pendingSpawnCount: Math.max( + 0, + snapshotSummary.pendingCount - snapshotSummary.runtimeAlivePendingCount + ), + failedSpawnCount: snapshotSummary.failedCount, + }; + + const snapshotAccountedFor = + snapshotMilestones.heartbeatConfirmedCount + + snapshotMilestones.processOnlyAliveCount + + snapshotMilestones.failedSpawnCount; + const liveAccountedFor = + liveSummary.heartbeatConfirmedCount + + liveSummary.processOnlyAliveCount + + liveSummary.failedSpawnCount; + + const liveSummaryIsMoreAdvanced = + liveSummary.failedSpawnCount > snapshotMilestones.failedSpawnCount || + liveSummary.heartbeatConfirmedCount > snapshotMilestones.heartbeatConfirmedCount || + liveSummary.processOnlyAliveCount > snapshotMilestones.processOnlyAliveCount || + liveAccountedFor > snapshotAccountedFor; + + return liveSummaryIsMoreAdvanced + ? { + expectedTeammateCount, + ...liveSummary, + } + : snapshotMilestones; + } + + return { + expectedTeammateCount, + ...liveSummary, + }; +} + export function getLaunchJoinState({ expectedTeammateCount, heartbeatConfirmedCount, diff --git a/test/renderer/utils/teamProvisioningPresentation.test.ts b/test/renderer/utils/teamProvisioningPresentation.test.ts index 045b67dc..fb6dae4a 100644 --- a/test/renderer/utils/teamProvisioningPresentation.test.ts +++ b/test/renderer/utils/teamProvisioningPresentation.test.ts @@ -217,4 +217,69 @@ describe('buildTeamProvisioningPresentation', () => { expect(presentation?.compactDetail).toBe('1 teammate still joining'); expect(presentation?.panelMessage).toBe('1 teammate still joining'); }); + + it('prefers live confirmed teammates over a stale persisted launch summary', () => { + const presentation = buildTeamProvisioningPresentation({ + progress: { + runId: 'run-5', + teamName: 'codex-team', + state: 'ready', + startedAt: '2026-04-13T10:00:00.000Z', + updatedAt: '2026-04-13T10:00:08.000Z', + message: 'Launch completed', + messageSeverity: undefined, + pid: 4321, + cliLogsTail: '', + assistantOutput: '', + }, + members: [ + { + name: 'team-lead', + agentType: 'team-lead', + status: 'active', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + }, + { + name: 'bob', + agentType: 'engineer', + status: 'unknown', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + }, + ], + memberSpawnStatuses: { + bob: { + status: 'online', + launchState: 'confirmed_alive', + updatedAt: '2026-04-13T10:00:07.000Z', + runtimeAlive: true, + livenessSource: 'heartbeat', + bootstrapConfirmed: true, + hardFailure: false, + agentToolAccepted: true, + firstSpawnAcceptedAt: '2026-04-13T10:00:01.000Z', + lastHeartbeatAt: '2026-04-13T10:00:07.000Z', + }, + }, + memberSpawnSnapshot: { + expectedMembers: ['bob'], + summary: { + confirmedCount: 0, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 0, + }, + }, + }); + + expect(presentation?.compactTitle).toBe('Team launched'); + expect(presentation?.compactDetail).toBe('All 1 teammates joined'); + expect(presentation?.panelMessage).toBeNull(); + expect(presentation?.currentStepIndex).toBe(4); + }); });