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];
|
return memberSpawnStatuses[memberName];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLaunchJoinMilestonesFromMembers({
|
function summarizeLiveLaunchJoinMilestones(params: {
|
||||||
members,
|
teammateNames: readonly string[];
|
||||||
memberSpawnStatuses,
|
|
||||||
memberSpawnSnapshot,
|
|
||||||
}: {
|
|
||||||
members: readonly LaunchJoinMemberLike[];
|
|
||||||
memberSpawnStatuses?: MemberSpawnStatusCollection;
|
memberSpawnStatuses?: MemberSpawnStatusCollection;
|
||||||
memberSpawnSnapshot?: Pick<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'>;
|
}): Omit<LaunchJoinMilestones, 'expectedTeammateCount'> {
|
||||||
}): LaunchJoinMilestones {
|
const { teammateNames, memberSpawnStatuses } = params;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let heartbeatConfirmedCount = 0;
|
let heartbeatConfirmedCount = 0;
|
||||||
let processOnlyAliveCount = 0;
|
let processOnlyAliveCount = 0;
|
||||||
let pendingSpawnCount = 0;
|
let pendingSpawnCount = 0;
|
||||||
let failedSpawnCount = 0;
|
let failedSpawnCount = 0;
|
||||||
|
|
||||||
for (const member of teammates) {
|
for (const memberName of teammateNames) {
|
||||||
const entry = getSpawnEntry(memberSpawnStatuses, member.name);
|
const entry = getSpawnEntry(memberSpawnStatuses, memberName);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
pendingSpawnCount += 1;
|
pendingSpawnCount += 1;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -110,7 +89,6 @@ export function getLaunchJoinMilestonesFromMembers({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
expectedTeammateCount,
|
|
||||||
heartbeatConfirmedCount,
|
heartbeatConfirmedCount,
|
||||||
processOnlyAliveCount,
|
processOnlyAliveCount,
|
||||||
pendingSpawnCount,
|
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({
|
export function getLaunchJoinState({
|
||||||
expectedTeammateCount,
|
expectedTeammateCount,
|
||||||
heartbeatConfirmedCount,
|
heartbeatConfirmedCount,
|
||||||
|
|
|
||||||
|
|
@ -217,4 +217,69 @@ describe('buildTeamProvisioningPresentation', () => {
|
||||||
expect(presentation?.compactDetail).toBe('1 teammate still joining');
|
expect(presentation?.compactDetail).toBe('1 teammate still joining');
|
||||||
expect(presentation?.panelMessage).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