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');
|
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;
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue