fix(team): streamline opencode secondary lane launch

This commit is contained in:
777genius 2026-05-03 09:30:56 +03:00
parent ad7c4e24ad
commit 511e4178be
5 changed files with 339 additions and 33 deletions

View file

@ -1591,6 +1591,42 @@ interface MixedSecondaryRuntimeLaneState {
result: TeamRuntimeLaunchResult | null;
warnings: string[];
diagnostics: string[];
launchScheduled?: boolean;
queuedAtMs?: number;
launchStartedAtMs?: number;
launchFinishedAtMs?: number;
}
function formatOpenCodeLaneTimingMs(value: number | null | undefined): string {
return typeof value === 'number' && Number.isFinite(value)
? `${Math.max(0, Math.round(value))}ms`
: 'n/a';
}
function appendDiagnosticOnce(diagnostics: readonly string[], diagnostic: string | null): string[] {
if (!diagnostic || diagnostics.includes(diagnostic)) {
return [...diagnostics];
}
return [...diagnostics, diagnostic];
}
function buildOpenCodeSecondaryLaneTimingDiagnostic(
lane: MixedSecondaryRuntimeLaneState
): string | null {
if (
typeof lane.queuedAtMs !== 'number' ||
typeof lane.launchStartedAtMs !== 'number' ||
typeof lane.launchFinishedAtMs !== 'number'
) {
return null;
}
return [
'OpenCode secondary lane timing:',
`member=${lane.member.name}`,
`queueWaitMs=${formatOpenCodeLaneTimingMs(lane.launchStartedAtMs - lane.queuedAtMs)}`,
`launchMs=${formatOpenCodeLaneTimingMs(lane.launchFinishedAtMs - lane.launchStartedAtMs)}`,
`totalMs=${formatOpenCodeLaneTimingMs(lane.launchFinishedAtMs - lane.queuedAtMs)}`,
].join(' ');
}
function createUnexpectedMixedSecondaryLaneFailureResult(input: {
@ -1738,6 +1774,18 @@ function collectRuntimeLaunchFailureDiagnostics(
);
}
function collectOpenCodeSecondaryLaneFailureDiagnostics(
result: TeamRuntimeLaunchResult,
memberName: string,
prefixDiagnostics: readonly string[]
): string[] {
const diagnostics = [
...prefixDiagnostics,
...collectRuntimeLaunchFailureDiagnostics(result, memberName),
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
return diagnostics.length > 0 ? diagnostics : ['OpenCode bridge reported member launch failure'];
}
function isReconciliableOpenCodeUnknownOutcome(diagnostics: readonly string[]): boolean {
return diagnostics.some((diagnostic) =>
/outcome must be reconciled before retry/i.test(diagnostic)
@ -1760,7 +1808,10 @@ function isDefinitiveOpenCodePreLaunchFailure(
member.agentToolAccepted ||
member.runtimeAlive ||
member.bootstrapConfirmed ||
typeof member.sessionId === 'string';
(typeof member.sessionId === 'string' && member.sessionId.trim().length > 0) ||
(typeof member.runtimePid === 'number' &&
Number.isFinite(member.runtimePid) &&
member.runtimePid > 0);
if (runtimeMaterialized) {
return false;
}
@ -19229,6 +19280,8 @@ export class TeamProvisioningService {
run: ProvisioningRun,
lane: MixedSecondaryRuntimeLaneState
): Promise<void> {
lane.launchStartedAtMs = Date.now();
lane.queuedAtMs = lane.queuedAtMs ?? lane.launchStartedAtMs;
const requestedDiagnostics = [...lane.diagnostics];
const shouldAbortLaunch = (): boolean =>
run.cancelRequested ||
@ -19250,6 +19303,8 @@ export class TeamProvisioningService {
const adapter = this.getOpenCodeRuntimeAdapter();
if (!adapter) {
const message = 'OpenCode runtime adapter is not registered for mixed team launch.';
lane.launchFinishedAtMs = Date.now();
const timingDiagnostic = buildOpenCodeSecondaryLaneTimingDiagnostic(lane);
lane.state = 'finished';
lane.result = {
runId: lane.runId ?? randomUUID(),
@ -19266,14 +19321,14 @@ export class TeamProvisioningService {
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: 'opencode_runtime_adapter_missing',
diagnostics: [message],
diagnostics: appendDiagnosticOnce([message], timingDiagnostic),
},
},
warnings: [],
diagnostics: [...requestedDiagnostics, message],
diagnostics: appendDiagnosticOnce([...requestedDiagnostics, message], timingDiagnostic),
};
lane.warnings = [];
lane.diagnostics = [...requestedDiagnostics, message];
lane.diagnostics = appendDiagnosticOnce([...requestedDiagnostics, message], timingDiagnostic);
await this.publishMixedSecondaryLaneStatusChange(run, lane);
lane.state = 'finished';
return;
@ -19340,6 +19395,8 @@ export class TeamProvisioningService {
providerId: 'opencode',
model: lane.member.model,
effort: lane.member.effort,
runtimeOnly: true,
skipReadinessPreflight: true,
skipPermissions: run.request.skipPermissions !== false,
expectedMembers: [
{
@ -19369,16 +19426,49 @@ export class TeamProvisioningService {
await finishCancelledLane();
return;
}
lane.result = result;
lane.warnings = [...result.warnings];
lane.diagnostics = [...requestedDiagnostics, ...migration.diagnostics, ...result.diagnostics];
lane.launchFinishedAtMs = Date.now();
const timingDiagnostic = buildOpenCodeSecondaryLaneTimingDiagnostic(lane);
const memberEvidence = result.members[lane.member.name];
const resultWithTiming: TeamRuntimeLaunchResult = timingDiagnostic
? {
...result,
diagnostics: appendDiagnosticOnce(result.diagnostics, timingDiagnostic),
members: {
...result.members,
...(memberEvidence
? {
[lane.member.name]: {
...memberEvidence,
diagnostics: appendDiagnosticOnce(
memberEvidence.diagnostics ?? [],
timingDiagnostic
),
},
}
: {}),
},
}
: result;
lane.result = resultWithTiming;
lane.warnings = [...resultWithTiming.warnings];
const launchDiagnostics = appendDiagnosticOnce(
[...requestedDiagnostics, ...migration.diagnostics, ...resultWithTiming.diagnostics],
timingDiagnostic
);
lane.diagnostics = launchDiagnostics;
if (isDefinitiveOpenCodePreLaunchFailure(result, lane.member.name)) {
const diagnostics = [
...requestedDiagnostics,
...migration.diagnostics,
...collectRuntimeLaunchFailureDiagnostics(result, lane.member.name),
];
if (
isDefinitiveOpenCodePreLaunchFailure(resultWithTiming, lane.member.name) ||
resultWithTiming.teamLaunchState === 'partial_failure'
) {
const diagnostics = collectOpenCodeSecondaryLaneFailureDiagnostics(
resultWithTiming,
lane.member.name,
appendDiagnosticOnce(
[...requestedDiagnostics, ...migration.diagnostics],
timingDiagnostic
)
);
await upsertOpenCodeRuntimeLaneIndexEntry({
teamsBasePath: getTeamsBasePath(),
teamName: run.teamName,
@ -19387,8 +19477,6 @@ export class TeamProvisioningService {
diagnostics,
}).catch(() => undefined);
this.deleteSecondaryRuntimeRun(run.teamName, lane.laneId);
} else if (result.teamLaunchState === 'partial_failure') {
this.deleteSecondaryRuntimeRun(run.teamName, lane.laneId);
}
} catch (error) {
if (shouldAbortLaunch()) {
@ -19396,6 +19484,8 @@ export class TeamProvisioningService {
return;
}
const message = error instanceof Error ? error.message : String(error);
lane.launchFinishedAtMs = Date.now();
const timingDiagnostic = buildOpenCodeSecondaryLaneTimingDiagnostic(lane);
lane.result = {
runId: lane.runId,
teamName: run.teamName,
@ -19411,20 +19501,23 @@ export class TeamProvisioningService {
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: message,
diagnostics: [message],
diagnostics: appendDiagnosticOnce([message], timingDiagnostic),
},
},
warnings: [],
diagnostics: [message],
diagnostics: appendDiagnosticOnce([message], timingDiagnostic),
};
lane.warnings = [];
lane.diagnostics = [...requestedDiagnostics, ...migration.diagnostics, message];
lane.diagnostics = appendDiagnosticOnce(
[...requestedDiagnostics, ...migration.diagnostics, message],
timingDiagnostic
);
await upsertOpenCodeRuntimeLaneIndexEntry({
teamsBasePath: getTeamsBasePath(),
teamName: run.teamName,
laneId: lane.laneId,
state: 'degraded',
diagnostics: [message],
diagnostics: appendDiagnosticOnce([message], timingDiagnostic),
}).catch(() => undefined);
this.deleteSecondaryRuntimeRun(run.teamName, lane.laneId);
}
@ -19486,11 +19579,12 @@ export class TeamProvisioningService {
run: ProvisioningRun,
lane: MixedSecondaryRuntimeLaneState
): void {
if (lane.state !== 'queued') {
if (lane.state !== 'queued' || lane.launchScheduled) {
return;
}
lane.state = 'launching';
lane.queuedAtMs = lane.queuedAtMs ?? Date.now();
lane.launchScheduled = true;
lane.runId = lane.runId ?? randomUUID();
const launch = async () => {
@ -19505,6 +19599,7 @@ export class TeamProvisioningService {
lane.state = 'finished';
return;
}
lane.state = 'launching';
await this.launchSingleMixedSecondaryLane(run, lane);
} catch (error) {
if (run.cancelRequested || run.processKilled) {

View file

@ -132,25 +132,33 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter {
);
}
const prepared = await this.prepare(input);
if (!prepared.ok) {
return blockedLaunchResult(input, prepared.reason, prepared.diagnostics, prepared.warnings);
const skipReadinessPreflight = input.skipReadinessPreflight === true;
let selectedModel = input.model?.trim() ?? '';
let launchWarnings: string[] = [];
if (!skipReadinessPreflight) {
const prepared = await this.prepare(input);
if (!prepared.ok) {
return blockedLaunchResult(input, prepared.reason, prepared.diagnostics, prepared.warnings);
}
selectedModel = prepared.modelId ?? selectedModel;
launchWarnings = prepared.warnings;
}
if (!this.bridge.launchOpenCodeTeam) {
return blockedLaunchResult(input, 'opencode_launch_bridge_missing', [
'OpenCode readiness passed, but the state-changing launch bridge is not registered.',
'OpenCode state-changing launch bridge is not registered.',
]);
}
const selectedModel = prepared.modelId ?? input.model?.trim() ?? '';
if (!selectedModel) {
return blockedLaunchResult(input, 'opencode_model_unavailable', [
'OpenCode launch requires a selected raw model id.',
]);
}
const runtimeSnapshot = this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null;
const runtimeSnapshot = skipReadinessPreflight
? null
: (this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null);
this.lastProjectPathByTeamName.set(input.teamName, input.cwd);
const data = await this.bridge.launchOpenCodeTeam({
runId: input.runId,
@ -169,7 +177,7 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter {
manifestHighWatermark: null,
});
return mapOpenCodeLaunchDataToRuntimeResult(input, data, prepared.warnings);
return mapOpenCodeLaunchDataToRuntimeResult(input, data, launchWarnings);
}
async reconcile(input: TeamRuntimeReconcileInput): Promise<TeamRuntimeReconcileResult> {
@ -430,6 +438,9 @@ function mapOpenCodeLaunchDataToRuntimeResult(
prepareWarnings: string[]
): TeamRuntimeLaunchResult {
const bridgeDiagnostics = data.diagnostics.map(formatOpenCodeBridgeDiagnostic);
const memberBridgeDiagnostics = bridgeDiagnostics.filter(
(diagnostic) => !isOpenCodeLaunchTimingDiagnostic(diagnostic)
);
const checkpointNames = extractCheckpointNames(data);
const readyCheckpointsPresent = [...REQUIRED_READY_CHECKPOINTS].every((name) =>
checkpointNames.has(name)
@ -492,7 +503,7 @@ function mapOpenCodeLaunchDataToRuntimeResult(
...(bridgeMember?.evidence ?? []).map(
(evidence) => `${evidence.kind} at ${evidence.observedAt}`
),
...bridgeDiagnostics,
...memberBridgeDiagnostics,
...checkpointDiagnostic,
...(missingExpectedMembers.includes(member.name) ? incompleteReadyDiagnostic : []),
]
@ -724,6 +735,13 @@ function formatOpenCodeBridgeDiagnostic(diagnostic: {
return `${diagnostic.severity}:${diagnostic.code}: ${diagnostic.message}`;
}
function isOpenCodeLaunchTimingDiagnostic(diagnostic: string): boolean {
return (
diagnostic.startsWith('info:opencode_launch_member_timing:') ||
diagnostic.startsWith('info:opencode_launch_total_timing:')
);
}
function blockedLaunchResult(
input: TeamRuntimeLaunchInput,
reason: string,

View file

@ -35,10 +35,15 @@ export interface TeamRuntimeLaunchInput {
model?: string;
effort?: EffortLevel;
/**
* Runtime-only preflight skips model-scoped execution/evidence checks.
* Use only for warm-up diagnostics before a concrete launch model is selected.
* Runtime-only readiness skips extra model execution probes.
* Use when a separate preflight or the real launch prompt is the authority.
*/
runtimeOnly?: boolean;
/**
* Skip the adapter-level readiness bridge before launch.
* Use only when the launch bridge performs equivalent runtime/provider/MCP validation.
*/
skipReadinessPreflight?: boolean;
skipPermissions: boolean;
expectedMembers: TeamRuntimeMemberSpec[];
previousLaunchState: PersistedTeamLaunchSnapshot | null;

View file

@ -86,6 +86,76 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
});
});
it('can rely on the launch bridge as the only readiness authority', async () => {
const launchOpenCodeTeam = vi.fn<
NonNullable<OpenCodeTeamRuntimeBridgePort['launchOpenCodeTeam']>
>(
async () =>
({
runId: 'run-1',
teamLaunchState: 'ready',
members: {
alice: {
sessionId: 'oc-session-1',
launchState: 'confirmed_alive',
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: 'member_ready', observedAt: '2026-04-21T00:00:00.000Z' },
{ kind: 'run_ready', observedAt: '2026-04-21T00:00:00.000Z' },
],
},
},
warnings: [],
diagnostics: [
{
code: 'opencode_launch_total_timing',
severity: 'info',
message: 'total=12ms provisioningProbe=3ms members=1',
},
{
code: 'member_reconcile',
severity: 'warning',
message: 'alice: sample reconcile diagnostic',
},
],
}) satisfies OpenCodeLaunchTeamCommandData
);
const bridge = bridgePort(
readiness({
state: 'unknown_error',
launchAllowed: false,
diagnostics: ['readiness should be skipped'],
}),
{ launchOpenCodeTeam }
);
const adapter = new OpenCodeTeamRuntimeAdapter(bridge);
const result = await adapter.launch(launchInput({ skipReadinessPreflight: true }));
expect(result.teamLaunchState).toBe('clean_success');
expect(bridge.checkOpenCodeTeamLaunchReadiness).not.toHaveBeenCalled();
expect(launchOpenCodeTeam).toHaveBeenCalledWith(
expect.objectContaining({
selectedModel: 'openai/gpt-5.4-mini',
expectedCapabilitySnapshotId: null,
})
);
expect(result.diagnostics).toEqual(
expect.arrayContaining([
'info:opencode_launch_total_timing: total=12ms provisioningProbe=3ms members=1',
])
);
expect(result.members.alice?.diagnostics).not.toContain(
'info:opencode_launch_total_timing: total=12ms provisioningProbe=3ms members=1'
);
expect(result.members.alice?.diagnostics).toContain(
'warning:member_reconcile: alice: sample reconcile diagnostic'
);
});
it('rejects non-OpenCode members before readiness or launch bridge dispatch', async () => {
const launchOpenCodeTeam = vi.fn();
const bridge = bridgePort(readiness({ state: 'ready', launchAllowed: true }), {

View file

@ -3837,6 +3837,8 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode',
model: 'minimax-m2.5-free',
effort: 'medium',
runtimeOnly: true,
skipReadinessPreflight: true,
cwd: '/tmp/mixed-team',
expectedMembers: [
expect.objectContaining({
@ -6552,6 +6554,117 @@ describe('TeamProvisioningService', () => {
state: 'degraded',
diagnostics: expect.arrayContaining([
'OpenCode readiness bridge failed: timeout: OpenCode bridge command timed out',
expect.stringMatching(
/^OpenCode secondary lane timing: member=bob queueWaitMs=\d+ms launchMs=\d+ms totalMs=\d+ms$/
),
]),
},
},
});
},
{ timeout: 5000 }
);
});
it('marks an OpenCode secondary lane degraded when launch fails after runtime materializes', async () => {
const teamName = 'mixed-runtime-materialized-failure';
const svc = new TeamProvisioningService();
const adapterLaunch = vi.fn(async (input: Record<string, unknown>) => ({
runId: String(input.runId),
teamName: String(input.teamName),
launchPhase: 'active',
teamLaunchState: 'partial_failure',
members: {
tom: {
memberName: 'tom',
providerId: 'opencode',
launchState: 'failed_to_start',
agentToolAccepted: true,
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: 'OpenCode bridge reported member launch failure',
sessionId: 'ses_tom_materialized_without_bootstrap',
runtimePid: 71388,
livenessKind: 'runtime_process',
diagnostics: [
'OpenCode bootstrap MCP did not complete required tools before assistant response: runtime_bootstrap_checkin, member_briefing',
],
},
},
warnings: [],
diagnostics: ['OpenCode bridge reported member launch failure'],
}));
svc.setRuntimeAdapterRegistry(
new TeamRuntimeAdapterRegistry([
{
providerId: 'opencode',
prepare: vi.fn(),
launch: adapterLaunch,
reconcile: vi.fn(),
stop: vi.fn(),
} as any,
])
);
(svc as any).launchStateStore = {
read: vi.fn(async () => null),
write: vi.fn(async () => {}),
clear: vi.fn(async () => {}),
};
const run = createMemberSpawnRun({
teamName,
expectedMembers: ['bob'],
});
run.isLaunch = true;
run.request = {
teamName,
cwd: '/tmp/mixed-runtime-materialized-failure',
providerId: 'codex',
model: 'gpt-5.4',
effort: 'high',
skipPermissions: true,
};
run.effectiveMembers = [
{
name: 'bob',
role: 'Developer',
providerId: 'codex',
model: 'gpt-5.4',
effort: 'high',
},
];
run.mixedSecondaryLanes = [
{
laneId: 'secondary:opencode:tom',
providerId: 'opencode',
member: {
name: 'tom',
role: 'Developer',
providerId: 'opencode',
model: 'minimax-m2.5-free',
effort: 'medium',
},
runId: null,
state: 'queued',
result: null,
warnings: [],
diagnostics: [],
},
];
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
await vi.waitFor(
async () => {
expect(adapterLaunch).toHaveBeenCalledTimes(1);
await expect(readOpenCodeRuntimeLaneIndex(tempTeamsBase, teamName)).resolves.toMatchObject({
lanes: {
'secondary:opencode:tom': {
state: 'degraded',
diagnostics: expect.arrayContaining([
'OpenCode bridge reported member launch failure',
'OpenCode bootstrap MCP did not complete required tools before assistant response: runtime_bootstrap_checkin, member_briefing',
]),
},
},
@ -6670,13 +6783,18 @@ describe('TeamProvisioningService', () => {
expect(launchSingleMixedSecondaryLane).toHaveBeenCalledTimes(1);
expect(run.mixedSecondaryLanes.map((lane: { state: string }) => lane.state)).toEqual([
'launching',
'launching',
'launching',
'queued',
'queued',
]);
await expect(resultPromise).resolves.toBeNull();
expect(persistLaunchStateSnapshot).toHaveBeenCalledTimes(1);
await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
await Promise.resolve();
expect(launchSingleMixedSecondaryLane).toHaveBeenCalledTimes(1);
expect(persistLaunchStateSnapshot).toHaveBeenCalledTimes(2);
resolveFirstLaunch();
await Promise.resolve();
await vi.waitFor(() => expect(launchSingleMixedSecondaryLane).toHaveBeenCalledTimes(3));