fix(team): preserve opencode permission-blocked launch state

This commit is contained in:
777genius 2026-04-23 01:10:05 +03:00
parent 8cd3f04c20
commit cfc50df5a1
6 changed files with 148 additions and 3 deletions

View file

@ -230,4 +230,79 @@ describe('buildMixedPersistedLaunchSnapshot', () => {
});
expect(snapshot.teamLaunchState).toBe('partial_failure');
});
it('preserves permission-blocked side-lane members as runtime_pending_permission', () => {
const snapshot = buildMixedPersistedLaunchSnapshot({
teamName: 'mixed-team',
launchPhase: 'active',
updatedAt: '2026-04-22T10:05:00.000Z',
leadDefaults: {
providerId: 'codex',
providerBackendId: 'codex-native',
selectedFastMode: 'off',
resolvedFastMode: false,
launchIdentity: null,
},
primaryMembers: [{ name: 'alice', providerId: 'codex', model: 'gpt-5.4', effort: 'high' }],
primaryStatuses: {
alice: {
launchState: 'confirmed_alive',
status: 'online',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
livenessSource: 'heartbeat',
firstSpawnAcceptedAt: '2026-04-22T10:00:00.000Z',
lastHeartbeatAt: '2026-04-22T10:01:00.000Z',
updatedAt: '2026-04-22T10:05:00.000Z',
} as never,
},
secondaryMembers: [
{
laneId: 'secondary:opencode:bob',
member: {
name: 'bob',
providerId: 'opencode',
model: 'minimax-m2.5-free',
effort: 'medium',
},
leadDefaults: {
providerId: 'codex',
providerBackendId: 'codex-native',
selectedFastMode: 'off',
resolvedFastMode: false,
launchIdentity: null,
},
evidence: {
launchState: 'runtime_pending_permission',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: false,
hardFailure: false,
pendingPermissionRequestIds: ['opencode:run-1:perm_1'],
},
},
],
});
expect(snapshot.members.bob).toMatchObject({
laneKind: 'secondary',
laneOwnerProviderId: 'opencode',
launchState: 'runtime_pending_permission',
runtimeAlive: true,
agentToolAccepted: true,
bootstrapConfirmed: false,
pendingPermissionRequestIds: ['opencode:run-1:perm_1'],
hardFailure: false,
});
expect(snapshot.members.bob.diagnostics).toContain('waiting for permission approval');
expect(snapshot.summary).toEqual({
confirmedCount: 1,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 1,
});
expect(snapshot.teamLaunchState).toBe('partial_pending');
});
});

View file

@ -36,6 +36,7 @@ export interface MixedSecondaryLaneMemberStateInput {
bootstrapConfirmed?: boolean;
hardFailure?: boolean;
hardFailureReason?: string;
pendingPermissionRequestIds?: string[];
diagnostics?: string[];
} | null;
pendingReason?: string;
@ -46,6 +47,7 @@ function deriveMemberLaunchState(params: {
bootstrapConfirmed?: boolean;
runtimeAlive?: boolean;
agentToolAccepted?: boolean;
pendingPermissionRequestIds?: string[];
}): MemberLaunchState {
if (params.hardFailure) {
return 'failed_to_start';
@ -53,6 +55,9 @@ function deriveMemberLaunchState(params: {
if (params.bootstrapConfirmed) {
return 'confirmed_alive';
}
if ((params.pendingPermissionRequestIds?.length ?? 0) > 0) {
return 'runtime_pending_permission';
}
if (params.runtimeAlive || params.agentToolAccepted) {
return 'runtime_pending_bootstrap';
}
@ -62,14 +67,21 @@ function deriveMemberLaunchState(params: {
function buildDiagnostics(
member: Pick<
PersistedTeamLaunchMemberState,
'agentToolAccepted' | 'runtimeAlive' | 'bootstrapConfirmed' | 'hardFailureReason' | 'sources'
| 'agentToolAccepted'
| 'runtimeAlive'
| 'bootstrapConfirmed'
| 'hardFailureReason'
| 'sources'
| 'pendingPermissionRequestIds'
>
): string[] {
const diagnostics: string[] = [];
if (member.agentToolAccepted) diagnostics.push('spawn accepted');
if (member.runtimeAlive) diagnostics.push('runtime alive');
if (member.bootstrapConfirmed) diagnostics.push('late heartbeat received');
if (member.runtimeAlive && !member.bootstrapConfirmed) {
if ((member.pendingPermissionRequestIds?.length ?? 0) > 0) {
diagnostics.push('waiting for permission approval');
} else if (member.runtimeAlive && !member.bootstrapConfirmed) {
diagnostics.push('waiting for teammate check-in');
}
if (member.hardFailureReason)
@ -140,6 +152,7 @@ function createPrimaryLaneMemberState(params: {
bootstrapConfirmed: runtime?.bootstrapConfirmed,
runtimeAlive: runtime?.runtimeAlive,
agentToolAccepted: runtime?.agentToolAccepted,
pendingPermissionRequestIds: runtime?.pendingPermissionRequestIds,
}),
agentToolAccepted: runtime?.agentToolAccepted === true,
runtimeAlive: runtime?.runtimeAlive === true,
@ -171,6 +184,7 @@ function createSecondaryLaneMemberState(
bootstrapConfirmed: evidence?.bootstrapConfirmed,
runtimeAlive: evidence?.runtimeAlive,
agentToolAccepted: evidence?.agentToolAccepted,
pendingPermissionRequestIds: evidence?.pendingPermissionRequestIds,
});
const base: PersistedTeamLaunchMemberState = {
name: params.member.name.trim(),
@ -200,6 +214,9 @@ function createSecondaryLaneMemberState(
bootstrapConfirmed: evidence?.bootstrapConfirmed === true,
hardFailure: evidence?.hardFailure === true || launchState === 'failed_to_start',
hardFailureReason,
pendingPermissionRequestIds: evidence?.pendingPermissionRequestIds?.length
? [...new Set(evidence.pendingPermissionRequestIds)]
: undefined,
firstSpawnAcceptedAt: evidence?.agentToolAccepted ? params.updatedAt : undefined,
lastHeartbeatAt: evidence?.bootstrapConfirmed ? params.updatedAt : undefined,
lastRuntimeAliveAt: evidence?.runtimeAlive ? params.updatedAt : undefined,

View file

@ -12017,6 +12017,7 @@ export class TeamProvisioningService {
bootstrapConfirmed: evidenceEntry.bootstrapConfirmed,
hardFailure: evidenceEntry.hardFailure,
hardFailureReason: evidenceEntry.hardFailureReason,
pendingPermissionRequestIds: evidenceEntry.pendingPermissionRequestIds,
diagnostics: evidenceEntry.diagnostics,
}
: null,

View file

@ -440,7 +440,9 @@ function mapBridgeMemberToRuntimeEvidence(
? 'failed_to_start'
: confirmed
? 'confirmed_alive'
: 'runtime_pending_bootstrap',
: launchState === 'permission_blocked'
? 'runtime_pending_permission'
: 'runtime_pending_bootstrap',
agentToolAccepted: confirmed || pendingRuntimeObserved,
runtimeAlive: confirmed || pendingRuntimeObserved,
bootstrapConfirmed: confirmed,

View file

@ -69,6 +69,7 @@ export interface TeamRuntimeMemberLaunchEvidence {
bootstrapConfirmed: boolean;
hardFailure: boolean;
hardFailureReason?: string;
pendingPermissionRequestIds?: string[];
sessionId?: string;
backendType?: TeamAgentRuntimeBackendType;
runtimePid?: number;

View file

@ -242,6 +242,55 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
},
});
});
it('maps permission-blocked bridge members to runtime_pending_permission instead of bootstrap pending', async () => {
const launchOpenCodeTeam = vi.fn(async () => ({
runId: 'run-1',
teamLaunchState: 'permission_blocked',
members: {
alice: {
sessionId: 'oc-session-1',
launchState: 'permission_blocked',
runtimePid: 123,
model: 'openai/gpt-5.4-mini',
evidence: [
{ kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'permission_blocked', observedAt: '2026-04-21T00:00:00.000Z' },
],
},
},
warnings: [],
diagnostics: [],
}) satisfies OpenCodeLaunchTeamCommandData);
const adapter = new OpenCodeTeamRuntimeAdapter(
bridgePort(readiness({ state: 'ready', launchAllowed: true }), {
getLastOpenCodeRuntimeSnapshot: vi.fn(() => ({
providerId: 'opencode' as const,
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'version:1.14.19',
version: '1.14.19',
capabilitySnapshotId: 'cap-1',
})),
launchOpenCodeTeam,
}),
{ launchMode: 'dogfood' }
);
await expect(adapter.launch(launchInput())).resolves.toMatchObject({
teamLaunchState: 'partial_pending',
members: {
alice: {
providerId: 'opencode',
launchState: 'runtime_pending_permission',
runtimeAlive: true,
agentToolAccepted: true,
bootstrapConfirmed: false,
hardFailure: false,
},
},
});
});
});
function bridgePort(