fix(team): prefer live launch truth over stale summary

This commit is contained in:
777genius 2026-04-23 00:30:01 +03:00
parent 2db49d694c
commit 9005deb05c
2 changed files with 133 additions and 28 deletions

View file

@ -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<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'>;
}): 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<LaunchJoinMilestones, 'expectedTeammateCount'> {
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<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'>;
}): 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,

View file

@ -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);
});
});