fix(opencode): suppress recovered app mcp advisory

This commit is contained in:
777genius 2026-05-18 20:45:19 +03:00
parent fed152bd25
commit 3bb8e18982
11 changed files with 408 additions and 78 deletions

View file

@ -573,6 +573,9 @@ export class TeamGraphAdapter {
spawnRuntimeAlive: spawn?.runtimeAlive,
spawnBootstrapConfirmed: spawn?.bootstrapConfirmed,
spawnBootstrapStalled: spawn?.bootstrapStalled,
spawnAgentToolAccepted: spawn?.agentToolAccepted,
spawnHardFailure: spawn?.hardFailure,
spawnLivenessKind: spawn?.livenessKind,
runtimeAdvisory: member.runtimeAdvisory,
isLaunchSettling,
isTeamAlive: data.isAlive,

View file

@ -355,6 +355,9 @@ const MemberPopoverContent = ({
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
spawnHardFailure: spawnEntry?.hardFailure,
spawnLivenessKind: spawnEntry?.livenessKind,
runtimeAdvisory: member.runtimeAdvisory,
isLaunchSettling: provisioningPresentation?.hasMembersStillJoining ?? false,
isTeamAlive: teamData?.isAlive,

View file

@ -665,6 +665,9 @@ export const MemberCard = memo(function MemberCard({
spawnRuntimeAlive,
spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
spawnHardFailure: spawnEntry?.hardFailure,
spawnLivenessKind: spawnEntry?.livenessKind,
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
spawnUpdatedAt: spawnEntry?.updatedAt,
runtimeEntry,

View file

@ -290,6 +290,9 @@ export const MemberDetailDialog = ({
spawnRuntimeAlive={spawnEntry?.runtimeAlive}
spawnBootstrapConfirmed={spawnEntry?.bootstrapConfirmed}
spawnBootstrapStalled={spawnEntry?.bootstrapStalled}
spawnAgentToolAccepted={spawnEntry?.agentToolAccepted}
spawnHardFailure={spawnEntry?.hardFailure}
spawnLivenessKind={spawnEntry?.livenessKind}
spawnFirstSpawnAcceptedAt={spawnEntry?.firstSpawnAcceptedAt}
spawnUpdatedAt={spawnEntry?.updatedAt}
runtimeEntry={runtimeEntry}

View file

@ -40,6 +40,9 @@ interface MemberDetailHeaderProps {
spawnRuntimeAlive?: boolean;
spawnBootstrapConfirmed?: boolean;
spawnBootstrapStalled?: boolean;
spawnAgentToolAccepted?: boolean;
spawnHardFailure?: boolean;
spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind'];
spawnFirstSpawnAcceptedAt?: string;
spawnUpdatedAt?: string;
isLaunchSettling?: boolean;
@ -60,6 +63,9 @@ export const MemberDetailHeader = ({
spawnRuntimeAlive,
spawnBootstrapConfirmed,
spawnBootstrapStalled,
spawnAgentToolAccepted,
spawnHardFailure,
spawnLivenessKind,
spawnFirstSpawnAcceptedAt,
spawnUpdatedAt,
isLaunchSettling,
@ -89,6 +95,9 @@ export const MemberDetailHeader = ({
spawnRuntimeAlive,
spawnBootstrapConfirmed,
spawnBootstrapStalled,
spawnAgentToolAccepted,
spawnHardFailure,
spawnLivenessKind,
spawnFirstSpawnAcceptedAt,
spawnUpdatedAt,
runtimeEntry,

View file

@ -159,6 +159,9 @@ export const MemberHoverCard = memo(function MemberHoverCard({
spawnRuntimeAlive: spawnEntry?.runtimeAlive,
spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
spawnBootstrapStalled: spawnEntry?.bootstrapStalled,
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
spawnHardFailure: spawnEntry?.hardFailure,
spawnLivenessKind: spawnEntry?.livenessKind,
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
spawnUpdatedAt: spawnEntry?.updatedAt,
runtimeEntry,

View file

@ -6,6 +6,7 @@ import {
LEAD_PARTICIPANT_AVATAR_URL,
PARTICIPANT_AVATAR_URLS,
} from './memberAvatarCatalog';
import { isHealthyOpenCodeAppMcpConnectivityAdvisory } from './openCodeAdvisoryHealth';
import type {
LeadActivityState,
@ -1170,6 +1171,9 @@ export function buildMemberLaunchPresentation({
spawnRuntimeAlive,
spawnBootstrapConfirmed,
spawnBootstrapStalled,
spawnAgentToolAccepted,
spawnHardFailure,
spawnLivenessKind,
spawnFirstSpawnAcceptedAt,
spawnUpdatedAt,
runtimeAdvisory,
@ -1187,6 +1191,9 @@ export function buildMemberLaunchPresentation({
spawnRuntimeAlive: boolean | undefined;
spawnBootstrapConfirmed?: boolean;
spawnBootstrapStalled?: boolean;
spawnAgentToolAccepted?: boolean;
spawnHardFailure?: boolean;
spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind'];
spawnFirstSpawnAcceptedAt?: string;
spawnUpdatedAt?: string;
runtimeAdvisory: MemberRuntimeAdvisory | undefined;
@ -1205,6 +1212,19 @@ export function buildMemberLaunchPresentation({
);
const hasConfirmedSpawnLaunch =
spawnLaunchState === 'confirmed_alive' && spawnBootstrapConfirmed === true;
const suppressOpenCodeAppMcpAdvisory = isHealthyOpenCodeAppMcpConnectivityAdvisory({
providerId: member.providerId,
runtimeAdvisory,
spawnStatus,
launchState: spawnLaunchState,
runtimeAlive: spawnRuntimeAlive,
bootstrapConfirmed: spawnBootstrapConfirmed,
agentToolAccepted: spawnAgentToolAccepted,
hardFailure: spawnHardFailure,
livenessKind: spawnLivenessKind ?? runtimeEntry?.livenessKind,
runtimeEntry,
});
const displayRuntimeAdvisory = suppressOpenCodeAppMcpAdvisory ? undefined : runtimeAdvisory;
const effectiveSpawnStatus =
hasConfirmedSpawnLaunch &&
currentRuntimeOfflineVisualState == null &&
@ -1223,7 +1243,7 @@ export function buildMemberLaunchPresentation({
spawnLaunchState,
spawnLivenessSource,
effectiveSpawnRuntimeAlive,
runtimeAdvisory,
displayRuntimeAdvisory,
isLaunchSettling,
isTeamAlive,
isTeamProvisioning,
@ -1247,9 +1267,18 @@ export function buildMemberLaunchPresentation({
isTeamAlive,
isTeamProvisioning
);
const runtimeAdvisoryLabel = getMemberRuntimeAdvisoryLabel(runtimeAdvisory, member.providerId);
const runtimeAdvisoryTitle = getMemberRuntimeAdvisoryTitle(runtimeAdvisory, member.providerId);
const runtimeAdvisoryTone = getMemberRuntimeAdvisoryTone(runtimeAdvisory, member.providerId);
const runtimeAdvisoryLabel = getMemberRuntimeAdvisoryLabel(
displayRuntimeAdvisory,
member.providerId
);
const runtimeAdvisoryTitle = getMemberRuntimeAdvisoryTitle(
displayRuntimeAdvisory,
member.providerId
);
const runtimeAdvisoryTone = getMemberRuntimeAdvisoryTone(
displayRuntimeAdvisory,
member.providerId
);
const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling;
const startingIsStale =
!hasConfirmedSpawnLaunch &&

View file

@ -1,3 +1,5 @@
import { isHealthyOpenCodeAppMcpConnectivityAdvisory } from './openCodeAdvisoryHealth';
import type {
MemberLaunchState,
MemberRuntimeAdvisory,
@ -443,12 +445,31 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
const providerBackendId = runtimeEntry?.providerBackendId ?? params.member?.providerBackendId;
const laneId = runtimeEntry?.laneId ?? params.member?.laneId;
const laneKind = runtimeEntry?.laneKind ?? params.member?.laneKind;
const livenessKind = spawnEntry?.livenessKind ?? runtimeEntry?.livenessKind;
const launchState = spawnEntry?.launchState ?? params.launchState;
const spawnStatus = spawnEntry?.status ?? params.spawnStatus;
const runtimeAdvisoryTitle = boundedString(params.runtimeAdvisoryTitle);
const runtimeAdvisoryLabel = boundedString(params.runtimeAdvisoryLabel ?? undefined);
const runtimeAdvisoryMessage = boundedString(runtimeAdvisory?.message);
const runtimeAdvisoryCardError = isRuntimeAdvisoryCardError(runtimeAdvisory, providerId)
? (runtimeAdvisoryTitle ?? runtimeAdvisoryLabel ?? runtimeAdvisoryMessage)
: undefined;
const suppressOpenCodeAppMcpAdvisory = isHealthyOpenCodeAppMcpConnectivityAdvisory({
providerId,
runtimeAdvisory,
runtimeAdvisoryLabel,
runtimeAdvisoryTitle,
runtimeAdvisoryMessage,
spawnStatus,
launchState,
runtimeAlive: spawnEntry?.runtimeAlive,
bootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
agentToolAccepted: spawnEntry?.agentToolAccepted,
hardFailure: spawnEntry?.hardFailure,
livenessKind,
runtimeEntry,
});
const runtimeAdvisoryCardError =
!suppressOpenCodeAppMcpAdvisory && isRuntimeAdvisoryCardError(runtimeAdvisory, providerId)
? (runtimeAdvisoryTitle ?? runtimeAdvisoryLabel ?? runtimeAdvisoryMessage)
: undefined;
const runtimeDiagnosticSeverity =
spawnEntry?.runtimeDiagnosticSeverity ?? runtimeEntry?.runtimeDiagnosticSeverity;
const spawnRuntimeDiagnosticCardError = isRuntimeDiagnosticCardError({
@ -505,9 +526,6 @@ export function buildMemberLaunchDiagnosticsPayload(params: {
const runId = boundedString(params.runId ?? undefined);
const runtimeUpdatedAt = maybeString(runtimeEntry?.updatedAt);
const spawnUpdatedAt = maybeString(spawnEntry?.updatedAt);
const livenessKind = spawnEntry?.livenessKind ?? runtimeEntry?.livenessKind;
const launchState = spawnEntry?.launchState ?? params.launchState;
const spawnStatus = spawnEntry?.status ?? params.spawnStatus;
const diagnosticHints = buildDiagnosticHints({
memberCardError,
runtimeDiagnostic,

View file

@ -0,0 +1,70 @@
import type {
MemberLaunchState,
MemberRuntimeAdvisory,
MemberSpawnStatus,
TeamAgentRuntimeEntry,
TeamAgentRuntimeLivenessKind,
} from '@shared/types';
const OPENCODE_APP_MCP_CONNECTIVITY_NEEDLES = [
'attach_failed',
'readiness check failed',
'unable to connect',
] as const;
const OPENCODE_NON_HEALTHY_LIVENESS_KINDS = new Set<TeamAgentRuntimeLivenessKind>([
'runtime_process_candidate',
'permission_blocked',
'shell_only',
'registered_only',
'stale_metadata',
'not_found',
]);
function hasOpenCodeAppMcpConnectivityEvidence(values: readonly (string | undefined)[]): boolean {
const text = values
.filter((value): value is string => Boolean(value?.trim()))
.join('\n')
.toLowerCase();
return (
text.includes('opencode app mcp') &&
OPENCODE_APP_MCP_CONNECTIVITY_NEEDLES.some((needle) => text.includes(needle))
);
}
export function isHealthyOpenCodeAppMcpConnectivityAdvisory(input: {
providerId?: string;
runtimeAdvisory?: MemberRuntimeAdvisory;
runtimeAdvisoryLabel?: string | null;
runtimeAdvisoryTitle?: string;
runtimeAdvisoryMessage?: string;
spawnStatus?: MemberSpawnStatus;
launchState?: MemberLaunchState;
runtimeAlive?: boolean;
bootstrapConfirmed?: boolean;
agentToolAccepted?: boolean;
hardFailure?: boolean;
livenessKind?: TeamAgentRuntimeLivenessKind;
runtimeEntry?: TeamAgentRuntimeEntry;
}): boolean {
const livenessKind = input.livenessKind ?? input.runtimeEntry?.livenessKind;
return (
input.providerId === 'opencode' &&
input.runtimeAdvisory?.kind === 'api_error' &&
input.runtimeAdvisory.reasonCode === 'network_error' &&
hasOpenCodeAppMcpConnectivityEvidence([
input.runtimeAdvisoryTitle,
input.runtimeAdvisoryLabel ?? undefined,
input.runtimeAdvisoryMessage,
input.runtimeAdvisory.message,
]) &&
input.spawnStatus === 'online' &&
input.launchState === 'confirmed_alive' &&
input.runtimeAlive === true &&
input.bootstrapConfirmed === true &&
input.agentToolAccepted === true &&
input.hardFailure !== true &&
input.runtimeEntry?.alive !== false &&
(livenessKind == null || !OPENCODE_NON_HEALTHY_LIVENESS_KINDS.has(livenessKind))
);
}

View file

@ -897,11 +897,7 @@ describe('memberHelpers spawn-aware presence', () => {
};
expect(
getMemberRuntimeAdvisoryLabel(
advisory,
'opencode',
Date.parse('2026-05-17T21:45:00.000Z')
)
getMemberRuntimeAdvisoryLabel(advisory, 'opencode', Date.parse('2026-05-17T21:45:00.000Z'))
).toBe('OpenCode quota error · retry 2h 15m');
const title = getMemberRuntimeAdvisoryTitle(advisory, 'opencode');
@ -923,9 +919,7 @@ describe('memberHelpers spawn-aware presence', () => {
const title = getMemberRuntimeAdvisoryTitle(advisory, 'opencode');
expect(title).toContain(
'OpenCode delivery completed without required visible/progress proof.'
);
expect(title).toContain('OpenCode delivery completed without required visible/progress proof.');
expect(title).toContain('OpenCode responded, but did not create a visible message_send reply.');
expect(title).not.toContain('visible_reply_still_required');
});
@ -954,16 +948,12 @@ describe('memberHelpers spawn-aware presence', () => {
message: 'opencode_prompt_acceptance_unknown_after_bridge_timeout',
};
expect(getMemberRuntimeAdvisoryLabel(advisory, 'opencode')).toBe(
'OpenCode delivery error'
);
expect(getMemberRuntimeAdvisoryLabel(advisory, 'opencode')).toBe('OpenCode delivery error');
const title = getMemberRuntimeAdvisoryTitle(advisory, 'opencode');
expect(title).toContain('OpenCode runtime delivery error.');
expect(title).toContain(
'OpenCode bridge outcome unknown after timeout, retrying/observing.'
);
expect(title).toContain('OpenCode bridge outcome unknown after timeout, retrying/observing.');
expect(title).not.toContain('Network or connectivity error');
expect(title).not.toContain('opencode_prompt_acceptance_unknown_after_bridge_timeout');
});
@ -1110,25 +1100,32 @@ describe('memberHelpers spawn-aware presence', () => {
expect(title).toContain('permission_denied');
});
it.each(['permission_denied', 'error', 'failed', 'failure', 'aborted', 'canceled', 'cancelled', 'interrupted', 'enospc'])(
'does not let refresh pattern consume directly attached failure token _%s',
(suffix) => {
const message = `resolved_behavior_changed:old->new_${suffix}`;
const advisory = {
kind: 'api_error' as const,
observedAt: '2026-05-18T08:31:46.075Z',
reasonCode: 'backend_error' as const,
message,
};
it.each([
'permission_denied',
'error',
'failed',
'failure',
'aborted',
'canceled',
'cancelled',
'interrupted',
'enospc',
])('does not let refresh pattern consume directly attached failure token _%s', (suffix) => {
const message = `resolved_behavior_changed:old->new_${suffix}`;
const advisory = {
kind: 'api_error' as const,
observedAt: '2026-05-18T08:31:46.075Z',
reasonCode: 'backend_error' as const,
message,
};
expect(getMemberRuntimeAdvisoryLabel(advisory, 'opencode')).toBe('OpenCode API error');
expect(getMemberRuntimeAdvisoryTone(advisory, 'opencode')).toBe('error');
expect(getMemberRuntimeAdvisoryLabel(advisory, 'opencode')).toBe('OpenCode API error');
expect(getMemberRuntimeAdvisoryTone(advisory, 'opencode')).toBe('error');
const title = getMemberRuntimeAdvisoryTitle(advisory, 'opencode');
expect(title).toContain('OpenCode API error.');
expect(title).toContain(message);
}
);
const title = getMemberRuntimeAdvisoryTitle(advisory, 'opencode');
expect(title).toContain('OpenCode API error.');
expect(title).toContain(message);
});
it.each([
'resolved_behavior_changed:old->new/auth_unavailable',
@ -1212,21 +1209,24 @@ describe('memberHelpers spawn-aware presence', () => {
'OpenCode session is stale (resolved_behavior_changed:old->new); Key limit exceeded (total limit)',
'OpenCode session is stale (resolved_behavior_changed:old->new); 429 too many requests',
'OpenCode session is stale (resolved_behavior_changed:old->new); Free usage exceeded, subscribe to Go',
])('does not format stale refresh text with quota/rate failures as clean refresh: %s', (message) => {
const advisory = {
kind: 'api_error' as const,
observedAt: '2026-05-18T08:31:46.075Z',
reasonCode: 'backend_error' as const,
message,
};
])(
'does not format stale refresh text with quota/rate failures as clean refresh: %s',
(message) => {
const advisory = {
kind: 'api_error' as const,
observedAt: '2026-05-18T08:31:46.075Z',
reasonCode: 'backend_error' as const,
message,
};
expect(getMemberRuntimeAdvisoryLabel(advisory, 'opencode')).toBe('OpenCode API error');
expect(getMemberRuntimeAdvisoryTone(advisory, 'opencode')).toBe('error');
expect(getMemberRuntimeAdvisoryLabel(advisory, 'opencode')).toBe('OpenCode API error');
expect(getMemberRuntimeAdvisoryTone(advisory, 'opencode')).toBe('error');
const title = getMemberRuntimeAdvisoryTitle(advisory, 'opencode');
expect(title).toContain('OpenCode API error.');
expect(title).toContain(message);
});
const title = getMemberRuntimeAdvisoryTitle(advisory, 'opencode');
expect(title).toContain('OpenCode API error.');
expect(title).toContain(message);
}
);
it('does not format stale refresh text with unknown extra text as clean refresh', () => {
const message =
@ -1339,9 +1339,7 @@ describe('memberHelpers spawn-aware presence', () => {
'opencode'
);
expect(title).toContain(
'OpenCode created a reply without the required taskRefs metadata.'
);
expect(title).toContain('OpenCode created a reply without the required taskRefs metadata.');
expect(title).not.toContain('visible_reply_missing_task_refs');
});
@ -1410,6 +1408,43 @@ describe('memberHelpers spawn-aware presence', () => {
expect(presentation.dotClass).not.toContain('bg-red-400');
});
it('keeps recovered OpenCode App MCP connectivity advisory out of the terminal presentation', () => {
const presentation = buildMemberLaunchPresentation({
member: { ...member, providerId: 'opencode' },
spawnStatus: 'online',
spawnLaunchState: 'confirmed_alive',
spawnLivenessSource: 'heartbeat',
spawnRuntimeAlive: true,
spawnBootstrapConfirmed: true,
spawnAgentToolAccepted: true,
spawnHardFailure: false,
spawnLivenessKind: 'runtime_process',
runtimeEntry: {
memberName: 'alice',
providerId: 'opencode',
alive: true,
restartable: false,
livenessKind: 'runtime_process',
updatedAt: '2026-05-18T17:21:24.498Z',
},
runtimeAdvisory: {
kind: 'api_error',
observedAt: '2026-05-18T17:20:36.681Z',
reasonCode: 'network_error',
message:
'OpenCode app MCP was not connected before message delivery (status=attach_failed, connected=null). OpenCode app MCP readiness check failed: Unable to connect.',
},
isLaunchSettling: false,
isTeamAlive: true,
isTeamProvisioning: false,
});
expect(presentation.presenceLabel).not.toContain('OpenCode API error');
expect(presentation.runtimeAdvisoryLabel).toBeNull();
expect(presentation.runtimeAdvisoryTone).toBeNull();
expect(presentation.dotClass).not.toContain('bg-red-400');
});
it('falls back to the existing generic retry wording when no structured reason is present', () => {
expect(
getMemberRuntimeAdvisoryLabel(

View file

@ -220,13 +220,166 @@ describe('member launch diagnostics', () => {
);
});
it('does not surface recovered OpenCode App MCP connectivity advisory as card error', () => {
const appMcpMessage =
'OpenCode app MCP was not connected before message delivery (status=attach_failed, connected=null). OpenCode app MCP readiness check failed: Unable to connect. Is the computer able to access the url?';
const payload = buildMemberLaunchDiagnosticsPayload({
memberName: 'bob',
member: { name: 'bob', providerId: 'opencode' },
runtimeAdvisoryLabel: 'OpenCode API error',
runtimeAdvisoryTitle: `Network or connectivity error. ${appMcpMessage}`,
spawnEntry: {
status: 'online',
launchState: 'confirmed_alive',
runtimeAlive: true,
bootstrapConfirmed: true,
agentToolAccepted: true,
hardFailure: false,
livenessKind: 'runtime_process',
runtimeDiagnostic: 'OpenCode runtime process detected after bootstrap confirmation',
runtimeDiagnosticSeverity: 'info',
updatedAt: '2026-05-18T17:15:34.482Z',
},
runtimeEntry: {
memberName: 'bob',
providerId: 'opencode',
alive: true,
restartable: false,
livenessKind: 'runtime_process',
updatedAt: '2026-05-18T17:21:24.498Z',
},
runtimeAdvisory: {
kind: 'api_error',
observedAt: '2026-05-18T17:20:36.681Z',
reasonCode: 'network_error',
message: appMcpMessage,
},
});
expect(payload.memberCardError).toBeUndefined();
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(false);
expect(payload.runtimeAdvisoryReasonCode).toBe('network_error');
expect(payload.diagnostics).toContain(appMcpMessage);
});
it('keeps OpenCode App MCP connectivity advisory as error when health is not clean', () => {
const appMcpMessage =
'OpenCode app MCP was not connected before message delivery (status=attach_failed, connected=null). OpenCode app MCP readiness check failed: Unable to connect.';
for (const spawnEntry of [
{
status: 'online' as const,
launchState: 'confirmed_alive' as const,
runtimeAlive: true,
bootstrapConfirmed: true,
agentToolAccepted: true,
hardFailure: true,
updatedAt: '2026-05-18T17:15:34.482Z',
},
{
status: 'error' as const,
launchState: 'failed_to_start' as const,
runtimeAlive: true,
bootstrapConfirmed: true,
agentToolAccepted: true,
hardFailure: false,
updatedAt: '2026-05-18T17:15:34.482Z',
},
]) {
const payload = buildMemberLaunchDiagnosticsPayload({
memberName: 'bob',
member: { name: 'bob', providerId: 'opencode' },
runtimeAdvisoryLabel: 'OpenCode API error',
runtimeAdvisoryTitle: `Network or connectivity error. ${appMcpMessage}`,
spawnEntry,
runtimeAdvisory: {
kind: 'api_error',
observedAt: '2026-05-18T17:20:36.681Z',
reasonCode: 'network_error',
message: appMcpMessage,
},
});
expect(payload.memberCardError).toBe(`Network or connectivity error. ${appMcpMessage}`);
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
}
});
it.each([
[
'quota_exhausted' as const,
'OpenCode quota exhausted.',
'Free usage exceeded, subscribe to Go',
],
['auth_error' as const, 'OpenCode authentication issue.', 'authentication_failed'],
['rate_limited' as const, 'OpenCode rate limited the request.', '429 rate limited'],
])(
'keeps OpenCode %s advisory as card error on healthy members',
(reasonCode, title, message) => {
const payload = buildMemberLaunchDiagnosticsPayload({
memberName: 'bob',
member: { name: 'bob', providerId: 'opencode' },
runtimeAdvisoryLabel: 'OpenCode API error',
runtimeAdvisoryTitle: title,
spawnEntry: {
status: 'online',
launchState: 'confirmed_alive',
runtimeAlive: true,
bootstrapConfirmed: true,
agentToolAccepted: true,
hardFailure: false,
livenessKind: 'runtime_process',
updatedAt: '2026-05-18T17:15:34.482Z',
},
runtimeAdvisory: {
kind: 'api_error',
observedAt: '2026-05-18T17:20:36.681Z',
reasonCode,
message,
},
});
expect(payload.memberCardError).toBe(title);
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
}
);
it('does not suppress non-OpenCode App MCP connectivity advisory', () => {
const appMcpMessage =
'OpenCode app MCP was not connected before message delivery (status=attach_failed, connected=null). OpenCode app MCP readiness check failed: Unable to connect.';
const payload = buildMemberLaunchDiagnosticsPayload({
memberName: 'claude',
member: { name: 'claude', providerId: 'anthropic' },
runtimeAdvisoryLabel: 'Anthropic API error',
runtimeAdvisoryTitle: `Network or connectivity error. ${appMcpMessage}`,
spawnEntry: {
status: 'online',
launchState: 'confirmed_alive',
runtimeAlive: true,
bootstrapConfirmed: true,
agentToolAccepted: true,
hardFailure: false,
livenessKind: 'runtime_process',
updatedAt: '2026-05-18T17:15:34.482Z',
},
runtimeAdvisory: {
kind: 'api_error',
observedAt: '2026-05-18T17:20:36.681Z',
reasonCode: 'network_error',
message: appMcpMessage,
},
});
expect(payload.memberCardError).toBe(`Network or connectivity error. ${appMcpMessage}`);
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
});
it('does not surface recoverable OpenCode session refresh advisory as card error', () => {
const payload = buildMemberLaunchDiagnosticsPayload({
memberName: 'tom',
member: { name: 'tom', providerId: 'opencode' },
runtimeAdvisoryLabel: 'OpenCode session refresh',
runtimeAdvisoryTitle:
'OpenCode session changed; refreshing the session before retry.',
runtimeAdvisoryTitle: 'OpenCode session changed; refreshing the session before retry.',
spawnEntry: {
status: 'online',
launchState: 'confirmed_alive',
@ -322,7 +475,8 @@ describe('member launch diagnostics', () => {
kind: 'api_error',
observedAt: '2026-05-18T08:31:46.075Z',
reasonCode: 'backend_error',
message: 'OpenCode API error. opencode_prompt_delivery_session_refresh_scheduled permission denied',
message:
'OpenCode API error. opencode_prompt_delivery_session_refresh_scheduled permission denied',
},
});
@ -341,8 +495,7 @@ describe('member launch diagnostics', () => {
hardFailure: true,
error: 'OpenCode API error. resolved_behavior_changed:permission_blocked->pending',
hardFailureReason: 'OpenCode API error',
runtimeDiagnostic:
'resolved_behavior_changed:responded_non_visible_tool->pending',
runtimeDiagnostic: 'resolved_behavior_changed:responded_non_visible_tool->pending',
runtimeDiagnosticSeverity: 'error',
updatedAt: '2026-05-18T08:13:23.902Z',
},
@ -351,8 +504,7 @@ describe('member launch diagnostics', () => {
providerId: 'opencode',
alive: true,
restartable: false,
runtimeDiagnostic:
'resolved_behavior_changed:responded_non_visible_tool->pending',
runtimeDiagnostic: 'resolved_behavior_changed:responded_non_visible_tool->pending',
runtimeDiagnosticSeverity: 'error',
diagnostics: ['resolved_behavior_changed:tool_error->session_error'],
updatedAt: '2026-05-18T08:34:47.845Z',
@ -402,8 +554,7 @@ describe('member launch diagnostics', () => {
providerId: 'opencode',
alive: true,
restartable: false,
runtimeDiagnostic:
'opencode_session_refresh_scheduled_after_resolved_behavior_changed',
runtimeDiagnostic: 'opencode_session_refresh_scheduled_after_resolved_behavior_changed',
runtimeDiagnosticSeverity: 'error',
diagnostics: ['resolved_behavior_changed:old->new'],
updatedAt: '2026-05-18T08:34:47.845Z',
@ -454,9 +605,7 @@ describe('member launch diagnostics', () => {
},
});
expect(payload.memberCardError).toBe(
'OpenCode API errorresolved_behavior_changed:old->new'
);
expect(payload.memberCardError).toBe('OpenCode API errorresolved_behavior_changed:old->new');
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
});
@ -469,8 +618,7 @@ describe('member launch diagnostics', () => {
launchState: 'failed_to_start',
hardFailure: true,
error: 'OpenCode API error. resolved_behavior_changed:old->new',
hardFailureReason:
'opencode_session_refresh_scheduled_after_resolved_behavior_changed',
hardFailureReason: 'opencode_session_refresh_scheduled_after_resolved_behavior_changed',
runtimeDiagnostic: 'opencode_app_mcp_transport_changed:old->new',
runtimeDiagnosticSeverity: 'error',
updatedAt: '2026-05-18T08:13:23.902Z',
@ -478,9 +626,7 @@ describe('member launch diagnostics', () => {
});
expect(payload.memberCardError).toBeUndefined();
expect(payload.diagnostics).toContain(
'OpenCode API error. resolved_behavior_changed:old->new'
);
expect(payload.diagnostics).toContain('OpenCode API error. resolved_behavior_changed:old->new');
expect(payload.diagnosticHints).toBeUndefined();
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(false);
});
@ -510,9 +656,7 @@ describe('member launch diagnostics', () => {
expect(payload.memberCardError).toBeUndefined();
expect(payload.diagnostics).toContain('resolved_behavior_changed:old->new');
expect(payload.memberCardError).not.toBe(
'matched OpenCode runtime pid and process identity'
);
expect(payload.memberCardError).not.toBe('matched OpenCode runtime pid and process identity');
});
it('uses suppressed spawn runtime diagnostics as refresh evidence for generic OpenCode API errors', () => {
@ -679,7 +823,17 @@ describe('member launch diagnostics', () => {
expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true);
});
it.each(['permission_denied', 'error', 'failed', 'failure', 'aborted', 'canceled', 'cancelled', 'interrupted', 'enospc'])(
it.each([
'permission_denied',
'error',
'failed',
'failure',
'aborted',
'canceled',
'cancelled',
'interrupted',
'enospc',
])(
'keeps card error when refresh marker directly consumes failure-looking suffix _%s',
(suffix) => {
const error = `OpenCode API error. resolved_behavior_changed:old->new_${suffix}`;