fix(team): preserve opencode permission-blocked launch state
This commit is contained in:
parent
8cd3f04c20
commit
cfc50df5a1
6 changed files with 148 additions and 3 deletions
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -12017,6 +12017,7 @@ export class TeamProvisioningService {
|
|||
bootstrapConfirmed: evidenceEntry.bootstrapConfirmed,
|
||||
hardFailure: evidenceEntry.hardFailure,
|
||||
hardFailureReason: evidenceEntry.hardFailureReason,
|
||||
pendingPermissionRequestIds: evidenceEntry.pendingPermissionRequestIds,
|
||||
diagnostics: evidenceEntry.diagnostics,
|
||||
}
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export interface TeamRuntimeMemberLaunchEvidence {
|
|||
bootstrapConfirmed: boolean;
|
||||
hardFailure: boolean;
|
||||
hardFailureReason?: string;
|
||||
pendingPermissionRequestIds?: string[];
|
||||
sessionId?: string;
|
||||
backendType?: TeamAgentRuntimeBackendType;
|
||||
runtimePid?: number;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue