From 2b96adda33beba221e733e60ca3fdebf1b60d6d5 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 00:51:34 +0300 Subject: [PATCH] fix(team): avoid stale launch join mismatches --- .../runtime/OpenCodeTeamRuntimeAdapter.ts | 7 +- .../components/team/provisioningSteps.ts | 16 +++- .../team/OpenCodeTeamRuntimeAdapter.test.ts | 3 +- .../teamProvisioningPresentation.test.ts | 80 +++++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) diff --git a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts index 4f58d3cc..ea2f2d6d 100644 --- a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts +++ b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts @@ -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, diff --git a/src/renderer/components/team/provisioningSteps.ts b/src/renderer/components/team/provisioningSteps.ts index 31715201..23233e5a 100644 --- a/src/renderer/components/team/provisioningSteps.ts +++ b/src/renderer/components/team/provisioningSteps.ts @@ -106,13 +106,19 @@ export function getLaunchJoinMilestonesFromMembers({ memberSpawnSnapshot?: Pick; }): 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 diff --git a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts index bd696284..0572c841 100644 --- a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts +++ b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts @@ -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, }); diff --git a/test/renderer/utils/teamProvisioningPresentation.test.ts b/test/renderer/utils/teamProvisioningPresentation.test.ts index b973844a..b136824d 100644 --- a/test/renderer/utils/teamProvisioningPresentation.test.ts +++ b/test/renderer/utils/teamProvisioningPresentation.test.ts @@ -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); + }); });