From 34dd669d88b41c704d148224f4fb380ecff0a84d Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 02:31:56 +0300 Subject: [PATCH] fix(team): trust persisted permission launch state --- .../services/team/TeamProvisioningService.ts | 26 +++++++-- .../team/TeamProvisioningService.test.ts | 53 +++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index d5e74dba..284845a6 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -11855,9 +11855,12 @@ export class TeamProvisioningService { confirmedCount: number; pendingCount: number; runtimeAlivePendingCount: number; - } + }, + snapshot?: PersistedTeamLaunchSnapshot | null ): string { - const permissionPendingCount = this.countRunPermissionPendingMembers(run); + const permissionPendingCount = snapshot + ? this.countSnapshotPermissionPendingMembers(snapshot) + : this.countRunPermissionPendingMembers(run); if ( launchSummary.pendingCount > 0 && permissionPendingCount > 0 && @@ -11901,7 +11904,7 @@ export class TeamProvisioningService { ): string { const mixedSecondaryLanes = run.mixedSecondaryLanes ?? []; if (!snapshot || mixedSecondaryLanes.length === 0) { - return this.buildPendingBootstrapStatusMessage(prefix, run, launchSummary); + return this.buildPendingBootstrapStatusMessage(prefix, run, launchSummary, snapshot); } const allPendingMembers = snapshot.expectedMembers.filter((memberName) => { @@ -11970,6 +11973,23 @@ export class TeamProvisioningService { return count; } + private countSnapshotPermissionPendingMembers(snapshot: PersistedTeamLaunchSnapshot): number { + let count = 0; + for (const memberName of snapshot.expectedMembers) { + const member = snapshot.members[memberName]; + if (!member) { + continue; + } + if ( + member.launchState === 'runtime_pending_permission' || + (member.pendingPermissionRequestIds?.length ?? 0) > 0 + ) { + count += 1; + } + } + return count; + } + private hasPendingLaunchMembers( run: ProvisioningRun, launchSummary: { diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 38f0d561..aa3ee3af 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -2121,6 +2121,59 @@ describe('TeamProvisioningService', () => { ); }); + it('trusts persisted snapshot permission state for pure teams when live run statuses are absent', () => { + const svc = new TeamProvisioningService(); + const run = createMemberSpawnRun({ + teamName: 'pure-team', + expectedMembers: ['alice'], + memberSpawnStatuses: new Map(), + }); + + const message = (svc as any).buildAggregatePendingLaunchMessage( + 'Finishing launch', + run, + { + confirmedCount: 0, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 1, + }, + { + version: 2, + teamName: 'pure-team', + updatedAt: '2026-04-22T12:00:00.000Z', + launchPhase: 'active', + expectedMembers: ['alice'], + bootstrapExpectedMembers: ['alice'], + members: { + alice: { + name: 'alice', + providerId: 'opencode', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'opencode', + launchState: 'runtime_pending_bootstrap', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: false, + hardFailure: false, + pendingPermissionRequestIds: ['perm-1'], + lastEvaluatedAt: '2026-04-22T12:00:00.000Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 1, + }, + teamLaunchState: 'partial_pending', + } + ); + + expect(message).toBe('Finishing launch — 1 teammate awaiting permission approval'); + }); + it('launches the OpenCode secondary lane with side-lane provider and member runtime identity', async () => { const svc = new TeamProvisioningService(); const adapterLaunch = vi.fn(async (input: Record) => ({