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

View file

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

View file

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

View file

@ -69,6 +69,7 @@ export interface TeamRuntimeMemberLaunchEvidence {
bootstrapConfirmed: boolean; bootstrapConfirmed: boolean;
hardFailure: boolean; hardFailure: boolean;
hardFailureReason?: string; hardFailureReason?: string;
pendingPermissionRequestIds?: string[];
sessionId?: string; sessionId?: string;
backendType?: TeamAgentRuntimeBackendType; backendType?: TeamAgentRuntimeBackendType;
runtimePid?: number; 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( function bridgePort(