fix(team): support OpenCode lead runtime sessions

This commit is contained in:
777genius 2026-05-28 23:38:35 +03:00
parent 9af0a0df2b
commit 961770b7a7
8 changed files with 709 additions and 29 deletions

View file

@ -9660,6 +9660,28 @@ export class TeamProvisioningService {
};
}
private async hasDeliverableOpenCodeRuntimeSessionForRecipient(
teamName: string,
memberName: string
): Promise<boolean> {
const identity = await this.resolveOpenCodeMemberDeliveryIdentity(teamName, memberName).catch(
() => null
);
if (!identity?.ok) {
return false;
}
const runId = await this.resolveCurrentOpenCodeRuntimeRunId(teamName, identity.laneId);
if (!runId) {
return false;
}
return this.hasDeliverableOpenCodeRuntimeBootstrapSessionEvidence({
teamName,
runId,
laneId: identity.laneId,
memberName: identity.canonicalMemberName,
});
}
private async resolveOpenCodeMembersForRuntimeLane(
teamName: string,
laneId: string
@ -14300,7 +14322,27 @@ export class TeamProvisioningService {
if (!memberName) continue;
const isLead = isLeadMember({ name: memberName, agentType: member.agentType });
if (isLead) {
const leadRuntimeMember = isLead ? getLiveRuntimeMember(memberName) : undefined;
const leadLaunchMember = isLead ? launchSnapshot?.members[memberName] : undefined;
const leadActiveRunMember = isLead ? activeRunMemberByName.get(memberName) : undefined;
const leadRuntimeModel =
leadRuntimeMember?.model?.trim() ||
leadActiveRunMember?.model?.trim() ||
leadLaunchMember?.model?.trim() ||
member.model?.trim() ||
undefined;
const leadProviderId =
normalizeOptionalTeamProviderId(leadActiveRunMember?.providerId) ??
normalizeOptionalTeamProviderId(leadRuntimeMember?.providerId) ??
normalizeOptionalTeamProviderId(leadLaunchMember?.providerId) ??
normalizeOptionalTeamProviderId(member.providerId) ??
inferTeamProviderIdFromModel(leadRuntimeModel) ??
(currentRuntimeAdapterRun?.providerId === 'opencode' &&
currentRuntimeAdapterRun.members?.[memberName]
? 'opencode'
: undefined);
const useRuntimeSnapshotForLead = isLead && leadProviderId === 'opencode';
if (isLead && !useRuntimeSnapshotForLead) {
const pid = run?.child?.pid;
const usageStats = pid
? this.buildRuntimeProcessLoadStatsSafely(teamName, memberName, {
@ -15986,6 +16028,7 @@ export class TeamProvisioningService {
extraCliArgs: teamMeta?.extraCliArgs,
},
members: effectiveMembers,
leadName: leadMember?.name?.trim() || 'team-lead',
prompt: [
`Restarting OpenCode teammate "${targetRuntimeMember.name}" by user request.`,
'This is an app-managed OpenCode-only runtime refresh. Re-establish the team sessions and continue from persisted team context.',
@ -18806,6 +18849,31 @@ export class TeamProvisioningService {
);
}
private buildOpenCodePrimaryRuntimeMembers(input: {
request: TeamCreateRequest | TeamLaunchRequest;
members: TeamCreateRequest['members'];
launchCwd: string;
leadName?: string;
}): TeamCreateRequest['members'] {
const leadName = input.leadName?.trim() || 'team-lead';
const normalizedLeadName = leadName.toLowerCase();
const runtimeMembers = input.members.filter(
(member) => member.name.trim().toLowerCase() !== normalizedLeadName
);
return [
{
name: leadName,
role: 'Team Lead',
providerId: 'opencode',
model: input.request.model,
effort: input.request.effort,
cwd: input.launchCwd,
},
...runtimeMembers,
];
}
private async materializeOpenCodeRuntimeAdapterDefaults<
TRequest extends TeamCreateRequest | TeamLaunchRequest,
>(params: {
@ -20582,6 +20650,7 @@ export class TeamProvisioningService {
return this.runOpenCodeTeamRuntimeAdapterLaunch({
request: launchRequest,
members: effectiveMembers,
leadName: 'team-lead',
prompt: launchRequest.prompt?.trim() ?? '',
sourceWarning: undefined,
onProgress,
@ -20637,6 +20706,7 @@ export class TeamProvisioningService {
return this.runOpenCodeTeamRuntimeAdapterLaunch({
request: launchRequest,
members: effectiveMembers,
leadName: this.extractLeadNameFromConfigRaw(configRaw) ?? 'team-lead',
prompt,
sourceWarning: warning,
onProgress,
@ -20646,6 +20716,7 @@ export class TeamProvisioningService {
private async runOpenCodeTeamRuntimeAdapterLaunch(input: {
request: TeamCreateRequest | TeamLaunchRequest;
members: TeamCreateRequest['members'];
leadName?: string;
prompt: string;
sourceWarning?: string;
onProgress: (progress: TeamProvisioningProgress) => void;
@ -20707,6 +20778,12 @@ export class TeamProvisioningService {
state: 'active',
});
const launchCwd = this.getOpenCodeRuntimeLaunchCwd(input.request.cwd, input.members);
const runtimeMembers = this.buildOpenCodePrimaryRuntimeMembers({
request: input.request,
members: input.members,
launchCwd,
leadName: input.leadName,
});
const launchInput: TeamRuntimeLaunchInput = {
runId,
laneId: 'primary',
@ -20717,7 +20794,7 @@ export class TeamProvisioningService {
model: input.request.model,
effort: input.request.effort,
skipPermissions: input.request.skipPermissions !== false,
expectedMembers: input.members.map((member) => ({
expectedMembers: runtimeMembers.map((member) => ({
name: member.name,
role: member.role,
workflow: member.workflow,
@ -20912,7 +20989,10 @@ export class TeamProvisioningService {
result,
});
const members: Record<string, PersistedTeamLaunchMemberState> = {};
for (const member of input.expectedMembers) {
const persistedExpectedMembers = input.expectedMembers.filter(
(member) => !isLeadMember({ name: member.name })
);
for (const member of persistedExpectedMembers) {
const evidence = committedResult.members[member.name];
members[member.name] = this.toOpenCodePersistedLaunchMember(
member,
@ -20922,8 +21002,8 @@ export class TeamProvisioningService {
}
const snapshot = createPersistedLaunchSnapshot({
teamName: input.teamName,
expectedMembers: input.expectedMembers.map((member) => member.name),
bootstrapExpectedMembers: input.expectedMembers.map((member) => member.name),
expectedMembers: persistedExpectedMembers.map((member) => member.name),
bootstrapExpectedMembers: persistedExpectedMembers.map((member) => member.name),
leadSessionId: result.leadSessionId,
launchPhase: committedResult.launchPhase,
members,
@ -22457,19 +22537,26 @@ export class TeamProvisioningService {
);
if (inboxName.trim().toLowerCase() === leadName?.toLowerCase()) {
if (isOpenCodeRecipient) {
const diagnostic =
'opencode_lead_runtime_session_missing: OpenCode lead inbox relay is unsupported in v1; leaving inbox unread for durable retry/diagnostics.';
logger.warn(`[${teamName}] ${diagnostic} inbox=${inboxName}`);
const hasLeadSession = await this.hasDeliverableOpenCodeRuntimeSessionForRecipient(
teamName,
inboxName
);
if (!hasLeadSession) {
const diagnostic =
'opencode_lead_runtime_session_missing: OpenCode lead runtime session is not available; leaving inbox unread for durable retry/diagnostics.';
logger.warn(`[${teamName}] ${diagnostic} inbox=${inboxName}`);
return {
kind: 'opencode_lead_unsupported',
relayed: 0,
diagnostics: [diagnostic],
};
}
} else {
return {
kind: 'opencode_lead_unsupported',
relayed: 0,
diagnostics: [diagnostic],
kind: 'native_lead',
relayed: this.isTeamAlive(teamName) ? await this.relayLeadInboxMessages(teamName) : 0,
};
}
return {
kind: 'native_lead',
relayed: this.isTeamAlive(teamName) ? await this.relayLeadInboxMessages(teamName) : 0,
};
}
if (isOpenCodeRecipient) {
@ -25313,6 +25400,20 @@ export class TeamProvisioningService {
);
const activeRuntimeRunId =
run?.runId?.trim() || currentRuntimeAdapterRun?.runId?.trim() || runId?.trim() || '';
const getCurrentRuntimeAdapterEvidence = (
memberName: string
): TeamRuntimeMemberLaunchEvidence | undefined => {
const direct = currentRuntimeAdapterRun?.members?.[memberName];
if (direct) {
return direct;
}
return Object.entries(currentRuntimeAdapterRun?.members ?? {}).find(
([candidateName, evidence]) =>
matchesTeamMemberIdentity(candidateName, memberName) ||
matchesTeamMemberIdentity(evidence.memberName ?? '', memberName)
)?.[1];
};
const committedPrimarySessionStatusByMember = new Map<string, MemberSpawnStatusEntry>();
for (const persistedMember of Object.values(persistedLaunchSnapshot?.members ?? {})) {
const memberName = persistedMember.name?.trim() ?? '';
if (!memberName || this.isMemberRemovedInMeta(metaMembers, memberName)) {
@ -25320,12 +25421,12 @@ export class TeamProvisioningService {
}
const activeRunMember = this.findEffectiveRunMember(run, memberName);
const activeRunModel = activeRunMember?.model?.trim();
const evidenceModel = currentRuntimeAdapterRun?.members?.[memberName]?.model?.trim();
const currentRuntimeAdapterEvidence = getCurrentRuntimeAdapterEvidence(memberName);
const evidenceModel = currentRuntimeAdapterEvidence?.model?.trim();
const activeRunProviderId =
normalizeOptionalTeamProviderId(activeRunMember?.providerId) ??
inferTeamProviderIdFromModel(activeRunModel ?? evidenceModel);
const effectiveProviderId = activeRunProviderId ?? persistedMember.providerId;
const currentRuntimeAdapterEvidence = currentRuntimeAdapterRun?.members?.[memberName];
upsertMetadata(memberName, {
backendType:
effectiveProviderId === 'opencode'
@ -25363,6 +25464,128 @@ export class TeamProvisioningService {
});
}
for (const [rawMemberName, evidence] of Object.entries(
currentRuntimeAdapterRun?.members ?? {}
)) {
const memberName = evidence.memberName?.trim() || rawMemberName.trim();
if (!memberName || memberName.toLowerCase() === 'user') {
continue;
}
if (this.isMemberRemovedInMeta(metaMembers, memberName)) {
continue;
}
const evidenceProviderId =
normalizeOptionalTeamProviderId(evidence.providerId) ??
currentRuntimeAdapterRun?.providerId;
const runtimeModel =
evidence.model?.trim() ||
this.findEffectiveRunMemberModel(run, memberName) ||
this.findConfiguredMemberModel(configuredMembers, memberName) ||
this.findMetaMemberModel(metaMembers, memberName);
upsertMetadata(memberName, {
backendType:
evidence.backendType ??
(evidenceProviderId === 'opencode'
? 'process'
: metadataByMember.get(memberName)?.backendType),
providerId: evidenceProviderId,
alive: false,
livenessKind: evidence.livenessKind,
pidSource: evidence.pidSource,
runtimeDiagnostic: evidence.runtimeDiagnostic,
runtimeDiagnosticSeverity: evidence.runtimeDiagnosticSeverity,
...(runtimeModel ? { model: runtimeModel } : {}),
...(typeof evidence.runtimePid === 'number' && evidence.runtimePid > 0
? { metricsPid: evidence.runtimePid }
: {}),
...(evidence.sessionId ? { runtimeSessionId: evidence.sessionId } : {}),
});
}
const configuredLeadForRuntime = configuredMembers.find((member) => isLeadMember(member));
const primaryLeadProviderId =
normalizeOptionalTeamProviderId(configuredLeadForRuntime?.providerId) ??
inferTeamProviderIdFromModel(configuredLeadForRuntime?.model);
const shouldReadPrimaryOpenCodeCommittedSessions =
currentRuntimeAdapterRun?.providerId === 'opencode' || primaryLeadProviderId === 'opencode';
if (shouldReadPrimaryOpenCodeCommittedSessions) {
const primaryRunId = await this.resolveCurrentOpenCodeRuntimeRunId(teamName, 'primary').catch(
() => null
);
const committedEvidence = primaryRunId
? await readCommittedOpenCodeBootstrapSessionEvidence({
teamsBasePath: getTeamsBasePath(),
teamName,
laneId: 'primary',
}).catch(() => null)
: null;
const committedActiveRunId = committedEvidence?.activeRunId?.trim() || null;
if (committedEvidence?.committed === true && committedActiveRunId === primaryRunId) {
for (const session of committedEvidence.sessions) {
if (session.runId !== primaryRunId) {
continue;
}
const memberName = session.memberName.trim();
if (
!memberName ||
memberName.toLowerCase() === 'user' ||
this.isMemberRemovedInMeta(metaMembers, memberName) ||
getCurrentRuntimeAdapterEvidence(memberName)
) {
continue;
}
const configuredMember = configuredMembers.find((member) =>
matchesTeamMemberIdentity(member.name ?? '', memberName)
);
const metaMember = metaMembers.find((member) =>
matchesTeamMemberIdentity(member.name ?? '', memberName)
);
const runtimeModel =
this.findEffectiveRunMemberModel(run, memberName) ||
metaMember?.model?.trim() ||
configuredMember?.model?.trim() ||
undefined;
const memberProviderId =
normalizeOptionalTeamProviderId(metaMember?.providerId) ??
normalizeOptionalTeamProviderId(configuredMember?.providerId) ??
inferTeamProviderIdFromModel(runtimeModel) ??
(isLeadMember({
name: memberName,
agentType: metaMember?.agentType ?? configuredMember?.agentType,
}) && primaryLeadProviderId === 'opencode'
? 'opencode'
: undefined);
if (memberProviderId !== 'opencode') {
continue;
}
const observedAt = session.observedAt ?? nowIso();
committedPrimarySessionStatusByMember.set(memberName, {
status: 'online',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
updatedAt: observedAt,
firstSpawnAcceptedAt: observedAt,
lastHeartbeatAt: observedAt,
});
upsertMetadata(memberName, {
backendType: 'process',
providerId: 'opencode',
alive: true,
livenessKind: 'confirmed_bootstrap',
pidSource: 'runtime_bootstrap',
runtimeDiagnostic: 'bootstrap confirmed',
runtimeDiagnosticSeverity: 'info',
...(runtimeModel ? { model: runtimeModel } : {}),
runtimeSessionId: session.id,
runtimeLastSeenAt: observedAt,
});
}
}
}
const paneIds = [...metadataByMember.values()]
.filter((metadata) => metadata.backendType === 'tmux' || metadata.backendType === undefined)
.map((metadata) => metadata.tmuxPaneId?.trim() ?? '')
@ -25425,7 +25648,7 @@ export class TeamProvisioningService {
for (const [memberName, metadata] of metadataByMember.entries()) {
const paneId = metadata.tmuxPaneId?.trim() ?? '';
const launchMember = persistedLaunchSnapshot?.members[memberName];
const adapterEvidence = currentRuntimeAdapterRun?.members?.[memberName];
const adapterEvidence = getCurrentRuntimeAdapterEvidence(memberName);
const adapterStatus: MemberSpawnStatusEntry | undefined = adapterEvidence
? {
status: adapterEvidence.hardFailure
@ -25474,11 +25697,12 @@ export class TeamProvisioningService {
launchMember
? this.buildLaunchMemberSpawnStatus(launchMember, metadata.model)
: undefined;
const committedPrimarySessionStatus = committedPrimarySessionStatusByMember.get(memberName);
const status = this.shouldPreferCurrentLaunchMemberStatus(trackedStatus, launchStatus)
? launchStatus
: this.shouldPreferCurrentLaunchMemberStatus(trackedStatus, adapterStatus)
? adapterStatus
: (trackedStatus ?? adapterStatus ?? launchStatus);
: (trackedStatus ?? adapterStatus ?? launchStatus ?? committedPrimarySessionStatus);
const resolved = resolveTeamMemberRuntimeLiveness({
teamName,
memberName,
@ -37298,6 +37522,19 @@ export class TeamProvisioningService {
}
}
private extractLeadNameFromConfigRaw(configRaw: string): string | null {
try {
const parsed = JSON.parse(configRaw) as { members?: TeamConfig['members'] };
if (!Array.isArray(parsed.members)) {
return null;
}
const lead = parsed.members.find((member) => isLeadMember(member));
return lead?.name?.trim() || null;
} catch {
return null;
}
}
/**
* Two-stage preflight check:
* 1. `claude --version` verifies the binary is executable.

View file

@ -1116,10 +1116,19 @@ function buildMemberBootstrapPrompt(
const teamPrompt = input.prompt?.trim();
const role = member.role?.trim() || member.workflow?.trim() || 'teammate';
const workflow = member.workflow?.trim();
const isTeamLead =
member.name.trim().toLowerCase() === 'team-lead' || role.trim().toLowerCase() === 'team lead';
const identityLine = isTeamLead
? `You are ${member.name}, the team lead for team "${input.teamName}".`
: `You are ${member.name}, a ${role} on team "${input.teamName}".`;
const messageTargets = isTeamLead
? 'the human user or a teammate'
: 'the human user, team lead, or another teammate';
const senderRole = isTeamLead ? 'team lead' : 'OpenCode teammate';
return [
'<agent_teams_app_managed_bootstrap_briefing>',
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
`You are ${member.name}, a ${role} on team "${input.teamName}".`,
identityLine,
teamPrompt ? `Team launch context:\n${teamPrompt}` : null,
workflow ? `Workflow:\n${workflow}` : null,
'',
@ -1131,8 +1140,8 @@ function buildMemberBootstrapPrompt(
'Do not call task_briefing, message_send, or cross_team_send just to announce readiness, say understood, report no tasks, or ask for work.',
'If the briefing says there are no actionable tasks, stay idle silently.',
'',
'When you need to message the human user, team lead, or another teammate, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.',
`Always set from="${member.name}" when sending a team message from this OpenCode teammate.`,
`When you need to message ${messageTargets}, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.`,
`Always set from="${member.name}" when sending a team message from this ${senderRole}.`,
'Do not answer team/app messages only as plain assistant text when agent-teams_message_send is available.',
'</agent_teams_app_managed_bootstrap_briefing>',
]

View file

@ -63,7 +63,11 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
const launchInput = captureAdapter.launchInputs[0];
expect(launchInput).toBeDefined();
expect(launchInput?.prompt ?? '').toContain('production desktop app');
expect(launchInput?.expectedMembers.map((member) => member.name)).toEqual(['bob', 'jack']);
expect(launchInput?.expectedMembers.map((member) => member.name)).toEqual([
'team-lead',
'bob',
'jack',
]);
expect(launchInput?.prompt?.length ?? 0).toBeGreaterThan(1_500);
const bridgeCapture = createCapturingOpenCodeBridge(selectedModel);
@ -78,7 +82,11 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
expect(launchCommand?.leadPrompt).toContain('OpenCode members bootstrap silently');
expect(launchCommand?.leadPrompt.length ?? 0).toBeGreaterThan(1_500);
expect(launchCommand?.leadPrompt.length ?? 0).toBeLessThan(80_000);
expect(launchCommand?.members.map((member) => member.name)).toEqual(['bob', 'jack']);
expect(launchCommand?.members.map((member) => member.name)).toEqual([
'team-lead',
'bob',
'jack',
]);
for (const member of launchCommand?.members ?? []) {
expect(member.prompt).toContain(`You are ${member.name}`);

View file

@ -1,11 +1,12 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { readOpenCodeRuntimeLaneIndex } from '../../../../src/main/services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader';
import { TeamInboxWriter } from '../../../../src/main/services/team/TeamInboxWriter';
import { getTeamsBasePath, setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';
import {
createOpenCodeLiveHarness,
getRuntimeTranscript,
@ -162,6 +163,140 @@ liveDescribe('OpenCode semantic messaging live e2e', () => {
300_000
);
it(
'relays a desktop inbox message to the OpenCode lead session and records the lead reply',
async () => {
const { bridgeClient, selectedModel, svc, dispose } = await createOpenCodeLiveHarness({
tempDir,
selectedModel: process.env.OPENCODE_E2E_MODEL?.trim() || DEFAULT_MODEL,
projectPath: PROJECT_PATH,
});
const teamName = `opencode-lead-message-${Date.now()}`;
const leadName = 'team-lead';
const memberName = 'bob';
const expectedReply = `opencode-lead-message-e2e-${Date.now()}`;
const progressEvents: TeamProvisioningProgress[] = [];
try {
const { runId } = await svc.createTeam(
{
teamName,
cwd: PROJECT_PATH,
providerId: 'opencode',
model: selectedModel,
skipPermissions: true,
members: [
{
name: memberName,
role: 'Developer',
providerId: 'opencode',
model: selectedModel,
},
],
},
(progress) => {
progressEvents.push(progress);
}
);
expect(runId).toBeTruthy();
const progressDump = progressEvents
.map((progress) =>
[
progress.state,
progress.message,
progress.messageSeverity,
progress.error,
progress.cliLogsTail,
]
.filter(Boolean)
.join(' | ')
)
.join('\n');
expect(
progressEvents.some((progress) =>
progress.message.includes('OpenCode team launch is ready')
),
progressDump
).toBe(true);
const runtimeSnapshot = await svc.getTeamAgentRuntimeSnapshot(teamName);
expect(runtimeSnapshot.members[leadName]).toMatchObject({
alive: true,
runtimeModel: selectedModel,
});
const written = await new TeamInboxWriter().sendMessage(teamName, {
member: leadName,
from: 'user',
to: leadName,
source: 'user_sent',
text: [
`Reply to the app Messages UI with exactly: ${expectedReply}`,
`Use agent-teams_message_send with to="user" and from="${leadName}".`,
'Do not answer only as plain assistant text.',
].join('\n'),
});
let lastRelay: Awaited<ReturnType<typeof svc.relayInboxFileToLiveRecipient>> | null = null;
const deadline = Date.now() + 90_000;
while (Date.now() < deadline) {
lastRelay = await svc.relayInboxFileToLiveRecipient(teamName, leadName, {
onlyMessageId: written.messageId,
source: 'ui-send',
deliveryMetadata: { replyRecipient: 'user' },
});
if (lastRelay.relayed >= 1) {
break;
}
if (
lastRelay.kind === 'opencode_lead_unsupported' ||
(lastRelay.lastDelivery?.delivered === false &&
lastRelay.lastDelivery.responsePending !== true)
) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 3_000));
}
expect(lastRelay).toMatchObject({
kind: 'opencode_member',
relayed: 1,
});
let reply: InboxMessage;
try {
reply = await waitForUserInboxReply(teamName, leadName, expectedReply, 90_000);
} catch (error) {
const transcript = await getRuntimeTranscript({
bridgeClient,
teamName,
memberName: leadName,
projectPath: PROJECT_PATH,
});
throw new Error(
`${error instanceof Error ? error.message : String(error)}\nLast relay: ${JSON.stringify(
lastRelay,
null,
2
)}\nTranscript: ${JSON.stringify(transcript, null, 2)}`
);
}
expect(reply).toMatchObject({
from: leadName,
to: 'user',
});
expect(reply.text).toContain(expectedReply);
} finally {
await svc.stopTeam(teamName).catch(() => undefined);
await dispose();
await waitForOpenCodeLanesStopped(teamName);
}
},
300_000
);
it(
'relays an OpenCode teammate message into another OpenCode member runtime and records the reply',
async () => {

View file

@ -202,6 +202,75 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
);
});
it('builds a lead-specific OpenCode bootstrap prompt for team-lead sessions', async () => {
const launchOpenCodeTeam = vi.fn<
NonNullable<OpenCodeTeamRuntimeBridgePort['launchOpenCodeTeam']>
>(async () => ({
runId: 'run-1',
teamLaunchState: 'ready',
members: {
'team-lead': {
sessionId: 'oc-lead-session',
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' },
],
},
alice: {
sessionId: 'oc-alice-session',
launchState: 'confirmed_alive',
runtimePid: 124,
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: [],
}));
const bridge = bridgePort(readiness({ state: 'ready', launchAllowed: true }), {
getLastOpenCodeRuntimeSnapshot: vi.fn(() => runtimeSnapshot('cap-lead')),
launchOpenCodeTeam,
});
const adapter = new OpenCodeTeamRuntimeAdapter(bridge);
await adapter.launch(
launchInput({
expectedMembers: [
{
name: 'team-lead',
role: 'Team Lead',
providerId: 'opencode',
model: 'openai/gpt-5.4-mini',
cwd: '/repo',
},
{
name: 'alice',
providerId: 'opencode',
model: 'openai/gpt-5.4-mini',
cwd: '/repo',
},
],
})
);
const command = launchOpenCodeTeam.mock.calls[0]?.[0];
const leadPrompt = command?.members.find((member) => member.name === 'team-lead')?.prompt;
expect(leadPrompt).toContain('You are team-lead, the team lead');
expect(leadPrompt).toContain('message the human user or a teammate');
expect(leadPrompt).toContain('Always set from="team-lead"');
expect(leadPrompt).not.toContain('human user, team lead, or another teammate');
});
it('retries transient MCP readiness transport failures before prepare succeeds', async () => {
const firstReadiness = readiness({
state: 'mcp_unavailable',

View file

@ -144,6 +144,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(runId).toBe(adapter.launchInputs[0]?.runId);
expect(adapter.launchInputs).toHaveLength(1);
expect(adapter.launchInputs[0]?.expectedMembers.map((member) => member.name)).toEqual([
'team-lead',
'alice',
'bob',
]);
@ -164,11 +165,28 @@ describe('Team agent launch matrix safe e2e', () => {
runtimeModel: 'opencode/big-pickle',
});
await expect(
fs.readFile(path.join(getTeamsBasePath(), 'pure-opencode-safe-e2e', 'launch-state.json'), {
const launchState = JSON.parse(
await fs.readFile(path.join(getTeamsBasePath(), 'pure-opencode-safe-e2e', 'launch-state.json'), {
encoding: 'utf8',
})
).resolves.toContain('"teamLaunchState": "clean_success"');
) as { expectedMembers: string[]; members: Record<string, unknown>; teamLaunchState: string };
expect(launchState.teamLaunchState).toBe('clean_success');
expect(launchState.expectedMembers).toEqual(['alice', 'bob']);
expect(Object.keys(launchState.members)).toEqual(['alice', 'bob']);
await expect(
readCommittedOpenCodeBootstrapSessionEvidence({
teamsBasePath: getTeamsBasePath(),
teamName: 'pure-opencode-safe-e2e',
laneId: 'primary',
})
).resolves.toMatchObject({
committed: true,
sessions: expect.arrayContaining([
expect.objectContaining({ memberName: 'team-lead' }),
expect.objectContaining({ memberName: 'alice' }),
expect.objectContaining({ memberName: 'bob' }),
]),
});
});
it('accepts pure OpenCode runtime bootstrap check-ins during adapter launch', async () => {
@ -191,7 +209,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(runId).toBe(adapter.launchInputs[0]?.runId);
expect(adapter.bootstrapCheckins).toEqual([
{
memberName: 'alice',
memberName: 'team-lead',
runId,
state: 'accepted',
},
@ -263,6 +281,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(runId).toBe(adapter.launchInputs[0]?.runId);
expect(adapter.launchInputs[0]?.expectedMembers.map((member) => member.name)).toEqual([
'team-lead',
'alice',
'bob',
]);

View file

@ -4848,6 +4848,128 @@ describe('TeamProvisioningService', () => {
});
});
it('reports a runtime-backed OpenCode lead as a process member', async () => {
const teamName = 'pure-opencode-runtime-lead-team';
const projectPath = '/Users/test/project';
const runId = 'opencode-runtime-run';
writeLaunchConfig(teamName, projectPath, 'lead-session', []);
writeLaunchState(teamName, 'lead-session', {
'team-lead': {
providerId: 'opencode',
model: 'opencode/big-pickle',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
hardFailureReason: undefined,
runtimePid: 333,
runtimeRunId: runId,
runtimeSessionId: 'session-team-lead',
},
});
vi.mocked(listRuntimeProcessTableForCurrentPlatform).mockResolvedValue([
{
pid: 333,
ppid: 1,
command: 'node /tmp/opencode-bridge.js --team-name pure-opencode-runtime-lead-team',
},
]);
vi.mocked(pidusage).mockResolvedValueOnce({
'333': createPidusageStat(333, 456_000_000),
} as any);
const svc = new TeamProvisioningService();
(svc as any).runtimeAdapterRunByTeam.set(teamName, {
runId,
providerId: 'opencode',
cwd: projectPath,
members: {
'team-lead': {
memberName: 'team-lead',
providerId: 'opencode',
model: 'opencode/big-pickle',
launchState: 'confirmed_alive',
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
runtimePid: 333,
sessionId: 'session-team-lead',
},
},
});
(svc as any).aliveRunByTeam.set(teamName, runId);
const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName);
expect(snapshot.members['team-lead']).toMatchObject({
alive: true,
backendType: 'process',
providerId: 'opencode',
runtimeModel: 'opencode/big-pickle',
runtimeSessionId: 'session-team-lead',
});
});
it('restores OpenCode lead runtime liveness from committed primary session evidence', async () => {
const teamName = 'pure-opencode-runtime-lead-restart-team';
const projectPath = '/Users/test/project';
const runId = 'opencode-runtime-run-after-restart';
const teamDir = path.join(tempTeamsBase, teamName);
fs.mkdirSync(teamDir, { recursive: true });
fs.writeFileSync(
path.join(teamDir, 'config.json'),
JSON.stringify({
name: teamName,
projectPath,
members: [
{
name: 'team-lead',
role: 'Team Lead',
agentType: 'team-lead',
providerId: 'opencode',
model: 'opencode/big-pickle',
},
],
}),
'utf8'
);
await upsertOpenCodeRuntimeLaneIndexEntry({
teamsBasePath: tempTeamsBase,
teamName,
laneId: 'primary',
state: 'active',
});
await writeCommittedOpenCodeSessionStore({
teamName,
laneId: 'primary',
runId,
sessions: [
{
id: 'session-team-lead-after-restart',
teamName,
memberName: 'team-lead',
laneId: 'primary',
runId,
observedAt: '2026-04-22T12:00:00.000Z',
source: 'runtime_bootstrap_checkin',
},
],
});
const svc = new TeamProvisioningService();
const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName);
expect(snapshot.members['team-lead']).toMatchObject({
alive: true,
backendType: 'process',
providerId: 'opencode',
runtimeModel: 'opencode/big-pickle',
runtimeSessionId: 'session-team-lead-after-restart',
livenessKind: 'confirmed_bootstrap',
});
});
it('reconciles persisted launch state before building runtime snapshot metadata', async () => {
const teamName = 'zz-runtime-snapshot-reconciles-before-live-metadata';
const leadSessionId = 'lead-session';
@ -14848,6 +14970,24 @@ describe('TeamProvisioningService', () => {
})
).rejects.toThrow('launch boom');
expect(adapterLaunch).toHaveBeenCalledWith(
expect.objectContaining({
expectedMembers: [
expect.objectContaining({
name: 'team-lead',
role: 'Team Lead',
providerId: 'opencode',
model: 'minimax-m2.5-free',
cwd: '/tmp/opencode-team',
}),
expect.objectContaining({
name: 'alice',
providerId: 'opencode',
model: 'minimax-m2.5-free',
}),
],
})
);
await expect(readOpenCodeRuntimeLaneIndex(tempTeamsBase, teamName)).resolves.toMatchObject({
lanes: {},
});
@ -16798,6 +16938,12 @@ describe('TeamProvisioningService', () => {
effort: 'medium',
cwd: tempClaudeRoot,
expectedMembers: [
expect.objectContaining({
name: 'team-lead',
role: 'Team Lead',
providerId: 'opencode',
model: 'big-pickle',
}),
expect.objectContaining({
name: 'bob',
providerId: 'opencode',

View file

@ -3905,6 +3905,63 @@ Messages:
expect(rows[0].read).toBe(false);
});
it('routes OpenCode lead inbox rows through runtime relay when a lead session exists', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
hoisted.files.set(
`/mock/teams/${teamName}/config.json`,
JSON.stringify({
name: teamName,
projectPath: '/tmp/my-team',
members: [
{
name: 'team-lead',
agentType: 'team-lead',
providerId: 'opencode',
model: 'openrouter/test',
},
],
})
);
seedLeadInbox(teamName, [
{
from: 'user',
to: 'team-lead',
text: 'Please coordinate.',
timestamp: '2026-02-23T17:06:00.000Z',
read: false,
messageId: 'opencode-lead-runtime-1',
},
]);
vi.spyOn(service as any, 'hasDeliverableOpenCodeRuntimeSessionForRecipient').mockResolvedValue(
true
);
vi.spyOn(service, 'deliverOpenCodeMemberMessage').mockResolvedValue({
delivered: true,
diagnostics: [],
});
const relay = await service.relayInboxFileToLiveRecipient(teamName, 'team-lead');
expect(relay).toMatchObject({ kind: 'opencode_member', relayed: 1 });
expect((service as any).hasDeliverableOpenCodeRuntimeSessionForRecipient).toHaveBeenCalledWith(
teamName,
'team-lead'
);
expect(service.deliverOpenCodeMemberMessage).toHaveBeenCalledWith(
teamName,
expect.objectContaining({
memberName: 'team-lead',
messageId: 'opencode-lead-runtime-1',
replyRecipient: 'user',
})
);
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/team-lead.json`) ?? '[]'
);
expect(rows[0].read).toBe(true);
});
it('keeps failed OpenCode member inbox relay rows unread for retry', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';