fix(team): keep permission-pending launches from reading ready

This commit is contained in:
777genius 2026-04-23 02:26:50 +03:00
parent e8ebe68576
commit 146b839b9c
2 changed files with 181 additions and 12 deletions

View file

@ -11857,6 +11857,19 @@ export class TeamProvisioningService {
runtimeAlivePendingCount: number;
}
): string {
const permissionPendingCount = this.countRunPermissionPendingMembers(run);
if (
launchSummary.pendingCount > 0 &&
permissionPendingCount > 0 &&
permissionPendingCount === launchSummary.pendingCount
) {
return `${prefix}${
permissionPendingCount === 1
? '1 teammate awaiting permission approval'
: `${permissionPendingCount} teammates awaiting permission approval`
}`;
}
const stillStartingCount = Math.max(
0,
launchSummary.pendingCount - launchSummary.runtimeAlivePendingCount
@ -11891,6 +11904,30 @@ export class TeamProvisioningService {
return this.buildPendingBootstrapStatusMessage(prefix, run, launchSummary);
}
const allPendingMembers = snapshot.expectedMembers.filter((memberName) => {
const member = snapshot.members[memberName];
if (!member) {
return false;
}
return member.launchState !== 'confirmed_alive' && member.launchState !== 'failed_to_start';
});
if (
allPendingMembers.length > 0 &&
allPendingMembers.every((memberName) => {
const member = snapshot.members[memberName];
return (
member?.launchState === 'runtime_pending_permission' ||
(member?.pendingPermissionRequestIds?.length ?? 0) > 0
);
})
) {
return `${prefix}${
allPendingMembers.length === 1
? '1 teammate awaiting permission approval'
: `${allPendingMembers.length} teammates awaiting permission approval`
}`;
}
const primaryExpectedMembers = new Set(
snapshot.bootstrapExpectedMembers ?? run.expectedMembers
);
@ -11922,6 +11959,28 @@ export class TeamProvisioningService {
return statuses;
}
private countRunPermissionPendingMembers(run: ProvisioningRun): number {
let count = 0;
for (const expected of run.expectedMembers ?? []) {
const entry = run.memberSpawnStatuses.get(expected) ?? createInitialMemberSpawnStatusEntry();
if (entry.launchState === 'runtime_pending_permission') {
count += 1;
}
}
return count;
}
private hasPendingLaunchMembers(
run: ProvisioningRun,
launchSummary: {
pendingCount: number;
},
snapshot?: PersistedTeamLaunchSnapshot | null
): boolean {
const expectedCount = snapshot?.expectedMembers.length ?? run.expectedMembers?.length ?? 0;
return launchSummary.pendingCount > 0 && expectedCount > 0;
}
private buildLiveLaunchSnapshotForRun(
run: ProvisioningRun,
launchPhase: PersistedTeamLaunchPhase = run.provisioningComplete ? 'finished' : 'active'
@ -15443,14 +15502,9 @@ export class TeamProvisioningService {
: this.getFailedSpawnMembers(run);
const launchSummary = persistedLaunchSnapshot?.summary ?? this.getMemberLaunchSummary(run);
const hasSpawnFailures = failedSpawnMembers.length > 0;
const stillStartingCount = Math.max(
0,
launchSummary.pendingCount - launchSummary.runtimeAlivePendingCount
);
const hasPendingBootstrap =
!hasSpawnFailures &&
stillStartingCount > 0 &&
(persistedLaunchSnapshot?.expectedMembers.length ?? run.expectedMembers?.length ?? 0) > 0;
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
const readyMessage = hasSpawnFailures
? `Launch completed with teammate errors — ${failedSpawnMembers
.map((member) => member.name)
@ -15622,14 +15676,9 @@ export class TeamProvisioningService {
: this.getFailedSpawnMembers(run);
const launchSummary = persistedLaunchSnapshot?.summary ?? this.getMemberLaunchSummary(run);
const hasSpawnFailures = failedSpawnMembers.length > 0;
const stillStartingCount = Math.max(
0,
launchSummary.pendingCount - launchSummary.runtimeAlivePendingCount
);
const hasPendingBootstrap =
!hasSpawnFailures &&
stillStartingCount > 0 &&
(persistedLaunchSnapshot?.expectedMembers.length ?? run.expectedMembers.length) > 0;
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
const progress = updateProgress(
run,
'ready',

View file

@ -2001,6 +2001,126 @@ describe('TeamProvisioningService', () => {
expect(message).toBe('Finishing launch - waiting for secondary runtime lane: bob');
});
it('uses permission-pending copy when the remaining mixed-team member is awaiting approval', async () => {
const svc = new TeamProvisioningService();
const run = createMemberSpawnRun({
teamName: 'mixed-team',
expectedMembers: ['alice'],
memberSpawnStatuses: new Map([
[
'alice',
createMemberSpawnStatusEntry({
status: 'online',
launchState: 'confirmed_alive',
runtimeAlive: true,
bootstrapConfirmed: true,
}),
],
]),
});
run.isLaunch = true;
run.mixedSecondaryLanes = [
{
laneId: 'secondary:opencode:bob',
providerId: 'opencode',
member: {
name: 'bob',
providerId: 'opencode',
model: 'minimax-m2.5-free',
effort: 'medium',
},
runId: 'opencode-run-1',
state: 'launching',
result: null,
warnings: [],
diagnostics: [],
},
];
const message = (svc as any).buildAggregatePendingLaunchMessage(
'Finishing launch',
run,
{
confirmedCount: 1,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 1,
},
{
version: 2,
teamName: 'mixed-team',
updatedAt: '2026-04-22T12:00:00.000Z',
launchPhase: 'active',
expectedMembers: ['alice', 'bob'],
bootstrapExpectedMembers: ['alice'],
members: {
alice: {
name: 'alice',
providerId: 'codex',
laneId: 'primary',
laneKind: 'primary',
laneOwnerProviderId: 'codex',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
},
bob: {
name: 'bob',
providerId: 'opencode',
laneId: 'secondary:opencode:bob',
laneKind: 'secondary',
laneOwnerProviderId: 'opencode',
launchState: 'runtime_pending_permission',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: false,
hardFailure: false,
pendingPermissionRequestIds: ['perm-1'],
lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
},
},
summary: {
confirmedCount: 1,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 1,
},
teamLaunchState: 'partial_pending',
}
);
expect(message).toBe('Finishing launch — 1 teammate awaiting permission approval');
});
it('keeps launch pending when the only remaining teammate is permission-blocked but already online', () => {
const svc = new TeamProvisioningService();
const run = createMemberSpawnRun({
expectedMembers: ['alice'],
memberSpawnStatuses: new Map([
[
'alice',
createMemberSpawnStatusEntry({
status: 'online',
launchState: 'runtime_pending_permission',
runtimeAlive: true,
agentToolAccepted: true,
bootstrapConfirmed: false,
pendingPermissionRequestIds: ['perm-1'],
}),
],
]),
});
const launchSummary = (svc as any).getMemberLaunchSummary(run);
expect((svc as any).hasPendingLaunchMembers(run, launchSummary, null)).toBe(true);
expect((svc as any).buildPendingBootstrapStatusMessage('Finishing launch', run, launchSummary)).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<string, unknown>) => ({