fix(team): prefer live launch truth over stale summary
This commit is contained in:
parent
2db49d694c
commit
9005deb05c
2 changed files with 133 additions and 28 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue