fix(team): avoid stale launch join mismatches

This commit is contained in:
777genius 2026-04-23 00:51:34 +03:00
parent d3baf501f6
commit 2b96adda33
4 changed files with 99 additions and 7 deletions

View file

@ -385,6 +385,7 @@ function mapOpenCodeLaunchDataToRuntimeResult(
fallbackLaunchState,
bridgeMember?.sessionId,
bridgeMember?.runtimePid,
bridgeMember != null,
[
...(bridgeMember
? []
@ -425,11 +426,13 @@ function mapBridgeMemberToRuntimeEvidence(
launchState: OpenCodeTeamMemberLaunchBridgeState,
sessionId: string | undefined,
runtimePid: number | undefined,
runtimeMaterialized: boolean,
diagnostics: string[]
): TeamRuntimeMemberLaunchEvidence {
const confirmed = launchState === 'confirmed_alive';
const createdOrBlocked = launchState === 'created' || launchState === 'permission_blocked';
const failed = launchState === 'failed';
const pendingRuntimeObserved = createdOrBlocked && runtimeMaterialized;
return {
memberName,
providerId: 'opencode',
@ -438,8 +441,8 @@ function mapBridgeMemberToRuntimeEvidence(
: confirmed
? 'confirmed_alive'
: 'runtime_pending_bootstrap',
agentToolAccepted: confirmed || createdOrBlocked,
runtimeAlive: confirmed || createdOrBlocked,
agentToolAccepted: confirmed || pendingRuntimeObserved,
runtimeAlive: confirmed || pendingRuntimeObserved,
bootstrapConfirmed: confirmed,
hardFailure: failed,
hardFailureReason: failed ? 'OpenCode bridge reported member launch failure' : undefined,

View file

@ -106,13 +106,19 @@ export function getLaunchJoinMilestonesFromMembers({
memberSpawnSnapshot?: Pick<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'>;
}): LaunchJoinMilestones {
const teammates = members.filter((member) => !member.removedAt && !isLeadMember(member));
const activeTeammateNames = new Set(teammates.map((member) => member.name));
const activeTeammateNames = teammates.map((member) => member.name);
const activeTeammateNameSet = new Set(activeTeammateNames);
const teammateNames =
memberSpawnSnapshot?.expectedMembers?.length && memberSpawnSnapshot.expectedMembers.length > 0
? memberSpawnSnapshot.expectedMembers.filter((memberName) =>
activeTeammateNames.has(memberName)
? Array.from(
new Set([
...memberSpawnSnapshot.expectedMembers.filter((memberName) =>
activeTeammateNameSet.has(memberName)
),
...activeTeammateNames,
])
)
: teammates.map((member) => member.name);
: activeTeammateNames;
const expectedTeammateCount = teammateNames.length;
const snapshotSummary = memberSpawnSnapshot?.summary;
const liveSummary = summarizeLiveLaunchJoinMilestones({
@ -145,6 +151,8 @@ export function getLaunchJoinMilestonesFromMembers({
liveSummary.failedSpawnCount > snapshotMilestones.failedSpawnCount ||
liveSummary.heartbeatConfirmedCount > snapshotMilestones.heartbeatConfirmedCount ||
liveSummary.processOnlyAliveCount > snapshotMilestones.processOnlyAliveCount ||
(snapshotMilestones.failedSpawnCount === 0 &&
liveSummary.pendingSpawnCount > snapshotMilestones.pendingSpawnCount) ||
liveAccountedFor > snapshotAccountedFor;
return liveSummaryIsMoreAdvanced

View file

@ -209,7 +209,8 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
expect(result.members.bob).toMatchObject({
providerId: 'opencode',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: true,
runtimeAlive: false,
agentToolAccepted: false,
bootstrapConfirmed: false,
hardFailure: false,
});

View file

@ -456,4 +456,84 @@ describe('buildTeamProvisioningPresentation', () => {
expect(presentation?.panelMessage).toBeNull();
expect(presentation?.currentStepIndex).toBe(4);
});
it('keeps active teammates that are missing from persisted expectedMembers', () => {
const presentation = buildTeamProvisioningPresentation({
progress: {
runId: 'run-7',
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: 'alice',
agentType: 'reviewer',
status: 'active',
currentTaskId: null,
taskCount: 0,
lastActiveAt: null,
messageCount: 0,
},
{
name: 'bob',
agentType: 'developer',
status: 'unknown',
currentTaskId: null,
taskCount: 0,
lastActiveAt: null,
messageCount: 0,
},
],
memberSpawnStatuses: {
alice: {
status: 'online',
launchState: 'confirmed_alive',
updatedAt: '2026-04-13T10:00:07.000Z',
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
agentToolAccepted: true,
},
bob: {
status: 'waiting',
launchState: 'starting',
updatedAt: '2026-04-13T10:00:07.000Z',
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: false,
agentToolAccepted: false,
},
},
memberSpawnSnapshot: {
expectedMembers: ['alice'],
summary: {
confirmedCount: 1,
pendingCount: 0,
failedCount: 0,
runtimeAlivePendingCount: 0,
},
},
});
expect(presentation?.compactTitle).toBe('Finishing launch');
expect(presentation?.compactDetail).toBe('1 teammate still joining');
expect(presentation?.panelMessage).toBe('1 teammate still joining');
expect(presentation?.currentStepIndex).toBe(2);
});
});