fix(opencode): suppress recovered app mcp advisory
This commit is contained in:
parent
fed152bd25
commit
3bb8e18982
11 changed files with 408 additions and 78 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
70
src/renderer/utils/openCodeAdvisoryHealth.ts
Normal file
70
src/renderer/utils/openCodeAdvisoryHealth.ts
Normal 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))
|
||||
);
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue