982 lines
37 KiB
TypeScript
982 lines
37 KiB
TypeScript
import {
|
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence,
|
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext,
|
|
isBootstrapConfirmedProvisionedButNotAliveFailure,
|
|
} from '@shared/utils/teamLaunchFailureReason';
|
|
|
|
import { isHealthyOpenCodeAppMcpConnectivityAdvisory } from './openCodeAdvisoryHealth';
|
|
|
|
import type {
|
|
MemberLaunchState,
|
|
MemberRuntimeAdvisory,
|
|
MemberSpawnLivenessSource,
|
|
MemberSpawnStatus,
|
|
MemberSpawnStatusEntry,
|
|
TeamAgentRuntimeDiagnosticSeverity,
|
|
TeamAgentRuntimeEntry,
|
|
TeamAgentRuntimeLivenessKind,
|
|
TeamAgentRuntimePidSource,
|
|
} from '@shared/types';
|
|
|
|
export interface MemberLaunchDiagnosticsPayload {
|
|
teamName?: string;
|
|
runId?: string;
|
|
memberName: string;
|
|
providerId?: string;
|
|
providerBackendId?: string;
|
|
model?: string;
|
|
runtimeModel?: string;
|
|
agentType?: string;
|
|
laneId?: string;
|
|
laneKind?: 'primary' | 'secondary';
|
|
laneOwnerProviderId?: string;
|
|
removedAt?: number;
|
|
memberCardError?: string;
|
|
launchState?: MemberLaunchState;
|
|
spawnStatus?: MemberSpawnStatus;
|
|
backendType?: string;
|
|
alive?: boolean;
|
|
restartable?: boolean;
|
|
runtimeAlive?: boolean;
|
|
bootstrapConfirmed?: boolean;
|
|
agentToolAccepted?: boolean;
|
|
hardFailure?: boolean;
|
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
|
livenessSource?: MemberSpawnLivenessSource;
|
|
pid?: number;
|
|
pidSource?: TeamAgentRuntimePidSource;
|
|
paneId?: string;
|
|
panePid?: number;
|
|
paneCurrentCommand?: string;
|
|
processCommand?: string;
|
|
runtimePid?: number;
|
|
runtimeSessionId?: string;
|
|
runtimeLeaseExpiresAt?: string;
|
|
runtimeLastSeenAt?: string;
|
|
historicalBootstrapConfirmed?: boolean;
|
|
cwd?: string;
|
|
rssBytes?: number;
|
|
runtimeDiagnostic?: string;
|
|
runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
|
runtimeAdvisoryKind?: MemberRuntimeAdvisory['kind'];
|
|
runtimeAdvisoryReasonCode?: MemberRuntimeAdvisory['reasonCode'];
|
|
runtimeAdvisoryObservedAt?: string;
|
|
runtimeAdvisoryRetryUntil?: string;
|
|
runtimeAdvisoryRetryDelayMs?: number;
|
|
bootstrapStalled?: boolean;
|
|
pendingPermissionRequestIds?: string[];
|
|
firstSpawnAcceptedAt?: string;
|
|
lastHeartbeatAt?: string;
|
|
livenessLastCheckedAt?: string;
|
|
probableCause?: string;
|
|
diagnosticHints?: string[];
|
|
diagnostics?: string[];
|
|
spawnUpdatedAt?: string;
|
|
runtimeUpdatedAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
const MAX_DIAGNOSTIC_STRING_LENGTH = 500;
|
|
const MAX_DIAGNOSTIC_ITEMS = 20;
|
|
const MAX_PERMISSION_REQUEST_IDS = 10;
|
|
const SECRET_FLAG_PATTERN =
|
|
/(--(?:api-key|token|password|secret|authorization|auth-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
|
|
const SECRET_VALUE_PATTERNS: [RegExp, string][] = [
|
|
[/\bsk-\S{12,}\b/gi, '[redacted]'],
|
|
[/\b[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g, '[redacted]'],
|
|
];
|
|
const SECRET_ENV_KEY_PARTS = [
|
|
'API_KEY',
|
|
'AUTH_TOKEN',
|
|
'TOKEN',
|
|
'SECRET',
|
|
'PASSWORD',
|
|
'AUTHORIZATION',
|
|
];
|
|
|
|
function hasStoppedRuntimeLivenessKind(livenessKind: TeamAgentRuntimeLivenessKind | undefined) {
|
|
return (
|
|
livenessKind === 'not_found' ||
|
|
livenessKind === 'registered_only' ||
|
|
livenessKind === 'shell_only' ||
|
|
livenessKind === 'stale_metadata'
|
|
);
|
|
}
|
|
const OPENCODE_SESSION_REFRESH_REASON_MARKERS = [
|
|
'resolved_behavior_changed',
|
|
'opencode_app_mcp_transport_changed',
|
|
] as const;
|
|
const OPENCODE_SESSION_REFRESH_REASON_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789._~/=->';
|
|
const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN =
|
|
// eslint-disable-next-line sonarjs/regex-complexity -- Keyword taxonomy is kept literal to preserve diagnostic behavior.
|
|
/(?:^|[_\s:;./()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;./(),-])/i;
|
|
const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN =
|
|
/\b(?:not_observed|pending|prompt_not_indexed|responded_tool_call|responded_visible_message|responded_non_visible_tool|responded_plain_text|permission_blocked|tool_error|empty_assistant_turn|prompt_delivered_no_assistant_message|session_stale|session_error|reconcile_failed)\b/g;
|
|
|
|
type MemberSpawnStatusCollection =
|
|
| Record<string, MemberSpawnStatusEntry>
|
|
| Map<string, MemberSpawnStatusEntry>
|
|
| undefined;
|
|
|
|
interface MemberDiagnosticsMemberLike {
|
|
name: string;
|
|
providerId?: string;
|
|
providerBackendId?: string;
|
|
model?: string;
|
|
agentType?: string;
|
|
laneId?: string;
|
|
laneKind?: 'primary' | 'secondary';
|
|
laneOwnerProviderId?: string;
|
|
removedAt?: number;
|
|
}
|
|
|
|
function boundedString(
|
|
value: string | undefined,
|
|
maxLength = MAX_DIAGNOSTIC_STRING_LENGTH
|
|
): string | undefined {
|
|
const trimmed = value?.replace(/\s+/g, ' ').trim();
|
|
if (!trimmed) return undefined;
|
|
const redacted = redactDiagnosticEnvAssignments(
|
|
SECRET_VALUE_PATTERNS.reduce(
|
|
(current, [pattern, replacement]) => current.replace(pattern, replacement),
|
|
trimmed.replace(SECRET_FLAG_PATTERN, '$1[redacted]')
|
|
)
|
|
);
|
|
return redacted.length > maxLength
|
|
? `${redacted.slice(0, Math.max(0, maxLength - 3))}...`
|
|
: redacted;
|
|
}
|
|
|
|
function redactDiagnosticEnvAssignments(value: string): string {
|
|
return value.replace(/\b[A-Z0-9_]+\s*=\s*("[^"]*"|'[^']*'|\S+)/gi, (assignment) => {
|
|
const separatorIndex = assignment.indexOf('=');
|
|
const key = assignment.slice(0, separatorIndex).trim().toUpperCase();
|
|
return SECRET_ENV_KEY_PARTS.some((part) => key.includes(part)) ? '[redacted]' : assignment;
|
|
});
|
|
}
|
|
|
|
function boundedNumber(value: number | undefined): number | undefined {
|
|
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
? Math.trunc(value)
|
|
: undefined;
|
|
}
|
|
|
|
function boundedStringArray(
|
|
values: readonly string[] | undefined,
|
|
limit = MAX_PERMISSION_REQUEST_IDS
|
|
): string[] | undefined {
|
|
const result = values
|
|
?.map((value) => boundedString(value, 160))
|
|
.filter((value): value is string => Boolean(value))
|
|
.slice(0, limit);
|
|
return result && result.length > 0 ? result : undefined;
|
|
}
|
|
|
|
function maybeString(value: string | undefined): string | undefined {
|
|
return boundedString(value, 240);
|
|
}
|
|
|
|
function isRuntimeDiagnosticCardError(params: {
|
|
runtimeDiagnostic?: string;
|
|
runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
|
|
launchState?: MemberLaunchState;
|
|
spawnStatus?: MemberSpawnStatus;
|
|
hardFailure?: boolean;
|
|
providerId?: string;
|
|
}): boolean {
|
|
if (!params.runtimeDiagnostic) {
|
|
return false;
|
|
}
|
|
if (params.runtimeDiagnosticSeverity === 'info') {
|
|
return false;
|
|
}
|
|
if (
|
|
params.providerId === 'opencode' &&
|
|
isRecoverableOpenCodeSessionRefreshText(params.runtimeDiagnostic)
|
|
) {
|
|
return false;
|
|
}
|
|
return (
|
|
params.runtimeDiagnosticSeverity === 'error' ||
|
|
params.launchState === 'failed_to_start' ||
|
|
params.spawnStatus === 'error' ||
|
|
params.hardFailure === true
|
|
);
|
|
}
|
|
|
|
function isRecoverableOpenCodeSessionRefreshText(value: string | undefined): boolean {
|
|
const normalized = value?.trim().toLowerCase() ?? '';
|
|
const refreshText = stripOpenCodeGenericApiErrorPrefix(normalized);
|
|
const refreshMarkerText = refreshText.replace(/[.:\s-]+$/, '');
|
|
if (
|
|
refreshMarkerText === 'session_stale' ||
|
|
refreshMarkerText === 'opencode session refresh' ||
|
|
refreshMarkerText === 'opencode session changed; refreshing the session before retry' ||
|
|
refreshMarkerText === 'opencode session refresh scheduled after resolved behavior changed' ||
|
|
refreshMarkerText === 'opencode_prompt_delivery_session_refresh_scheduled' ||
|
|
refreshMarkerText === 'opencode_session_refresh_scheduled_after_resolved_behavior_changed'
|
|
) {
|
|
return true;
|
|
}
|
|
const reasonRanges = findOpenCodeSessionRefreshReasonRanges(refreshText);
|
|
if (reasonRanges.length === 0) {
|
|
return false;
|
|
}
|
|
const markerText = refreshText;
|
|
if (hasOpenCodeSessionRefreshFailureConflict(markerText)) {
|
|
return false;
|
|
}
|
|
const rawRemainder = removeOpenCodeSessionRefreshReasonRanges(markerText, reasonRanges);
|
|
const remainder = rawRemainder.replace(/[().,;:\s-]+/g, '');
|
|
if (remainder.length === 0) {
|
|
return true;
|
|
}
|
|
const staleLogProjectionContext =
|
|
normalized.includes('session is stale') ||
|
|
normalized.includes('stored session is stale') ||
|
|
normalized.includes('session reconcile skipped');
|
|
return staleLogProjectionContext && isBenignOpenCodeSessionRefreshRemainder(rawRemainder);
|
|
}
|
|
|
|
function stripOpenCodeGenericApiErrorPrefix(message: string): string {
|
|
return message.replace(/^opencode api error(?:[.:\s-]+|$)/i, '');
|
|
}
|
|
|
|
function findOpenCodeSessionRefreshReasonRanges(text: string): Array<[number, number]> {
|
|
const ranges: Array<[number, number]> = [];
|
|
for (const marker of OPENCODE_SESSION_REFRESH_REASON_MARKERS) {
|
|
const prefix = `${marker}:`;
|
|
let searchFrom = 0;
|
|
while (searchFrom < text.length) {
|
|
const markerStart = text.indexOf(prefix, searchFrom);
|
|
if (markerStart < 0) {
|
|
break;
|
|
}
|
|
const tokenStart = markerStart + prefix.length;
|
|
const tokenEnd = findOpenCodeSessionRefreshReasonTokenEnd(text, tokenStart);
|
|
if (tokenEnd !== null) {
|
|
ranges.push([markerStart, tokenEnd]);
|
|
}
|
|
searchFrom = Math.max(tokenStart + 1, tokenEnd ?? tokenStart);
|
|
}
|
|
}
|
|
return ranges.sort(([left], [right]) => left - right);
|
|
}
|
|
|
|
function findOpenCodeSessionRefreshReasonTokenEnd(text: string, start: number): number | null {
|
|
let end = start;
|
|
while (end < text.length && OPENCODE_SESSION_REFRESH_REASON_CHARS.includes(text[end] ?? '')) {
|
|
end += 1;
|
|
}
|
|
|
|
const token = text.slice(start, end);
|
|
const arrowIndex = token.indexOf('->');
|
|
if (arrowIndex <= 0 || arrowIndex >= token.length - 2) {
|
|
return null;
|
|
}
|
|
return end;
|
|
}
|
|
|
|
function removeOpenCodeSessionRefreshReasonRanges(
|
|
text: string,
|
|
ranges: ReadonlyArray<[number, number]>
|
|
): string {
|
|
let result = text;
|
|
for (const [start, end] of [...ranges].sort(([left], [right]) => right - left)) {
|
|
result = `${result.slice(0, start)}${result.slice(end)}`;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function isBenignOpenCodeSessionRefreshRemainder(rawRemainder: string): boolean {
|
|
if (OPENCODE_SESSION_REFRESH_FAILURE_PATTERN.test(rawRemainder)) {
|
|
return false;
|
|
}
|
|
const normalized = rawRemainder.replace(/[().,;:\s-]+/g, ' ').trim();
|
|
return (
|
|
normalized === 'opencode session is stale' ||
|
|
normalized ===
|
|
'opencode session is stale reading historical messages for log projection only' ||
|
|
normalized === 'opencode session reconcile skipped because the stored session is stale' ||
|
|
normalized === 'stored session is stale'
|
|
);
|
|
}
|
|
|
|
function hasOpenCodeSessionRefreshFailureConflict(value: string): boolean {
|
|
return OPENCODE_SESSION_REFRESH_FAILURE_PATTERN.test(
|
|
value.replace(OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN, 'state')
|
|
);
|
|
}
|
|
|
|
function isGenericOpenCodeApiErrorText(value: string | undefined): boolean {
|
|
const normalized =
|
|
value
|
|
?.trim()
|
|
.toLowerCase()
|
|
.replace(/[.:\s-]+$/, '') ?? '';
|
|
return normalized === 'opencode api error';
|
|
}
|
|
|
|
function isBenignOpenCodeRefreshContextText(value: string | undefined): boolean {
|
|
const normalized =
|
|
value
|
|
?.trim()
|
|
.toLowerCase()
|
|
.replace(/[.:\s-]+$/, '') ?? '';
|
|
return (
|
|
!normalized ||
|
|
isRecoverableOpenCodeSessionRefreshText(normalized) ||
|
|
isGenericOpenCodeApiErrorText(normalized) ||
|
|
normalized === 'matched opencode runtime pid and process identity' ||
|
|
normalized === 'bootstrap confirmed' ||
|
|
normalized === 'opencode runtime process detected after bootstrap confirmation'
|
|
);
|
|
}
|
|
|
|
function hasCleanRecoverableOpenCodeRefreshContext(
|
|
values: readonly (string | undefined)[]
|
|
): boolean {
|
|
const normalizedValues = values
|
|
.map((value) => value?.trim())
|
|
.filter((value): value is string => Boolean(value));
|
|
return (
|
|
normalizedValues.some(isRecoverableOpenCodeSessionRefreshText) &&
|
|
normalizedValues.every(isBenignOpenCodeRefreshContextText)
|
|
);
|
|
}
|
|
|
|
function isRuntimeAdvisoryCardError(
|
|
runtimeAdvisory: MemberRuntimeAdvisory | undefined,
|
|
providerId: string | undefined
|
|
): boolean {
|
|
if (providerId === 'opencode' && isRecoverableOpenCodeSessionRefreshAdvisory(runtimeAdvisory)) {
|
|
return false;
|
|
}
|
|
return (
|
|
runtimeAdvisory?.kind === 'api_error' && runtimeAdvisory.reasonCode !== 'protocol_proof_missing'
|
|
);
|
|
}
|
|
|
|
function isRecoverableOpenCodeSessionRefreshAdvisory(
|
|
runtimeAdvisory: MemberRuntimeAdvisory | undefined
|
|
): boolean {
|
|
return (
|
|
Boolean(runtimeAdvisory) &&
|
|
(runtimeAdvisory?.reasonCode == null ||
|
|
runtimeAdvisory.reasonCode === 'backend_error' ||
|
|
runtimeAdvisory.reasonCode === 'unknown') &&
|
|
isRecoverableOpenCodeSessionRefreshText(runtimeAdvisory?.message)
|
|
);
|
|
}
|
|
|
|
export function normalizeMemberLaunchFailureReason(value: string | undefined): string | null {
|
|
const normalized = value
|
|
?.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.replace(/^Latest assistant message\s+\S+\s+failed with APIError\s*[-:]\s*/i, '')
|
|
.replace(/^APIError\s*[-:]\s*/i, '');
|
|
return normalized && normalized.length > 0 ? normalized : null;
|
|
}
|
|
|
|
function firstMemberCardFailureReason(input: {
|
|
candidates: (string | undefined)[];
|
|
evidence?: readonly (string | undefined)[];
|
|
providerId?: string;
|
|
}): string | undefined {
|
|
const hasCleanRecoverableOpenCodeRefresh =
|
|
input.providerId === 'opencode' &&
|
|
hasCleanRecoverableOpenCodeRefreshContext([...input.candidates, ...(input.evidence ?? [])]);
|
|
for (const value of input.candidates) {
|
|
const normalized = normalizeMemberLaunchFailureReason(value);
|
|
if (
|
|
!normalized ||
|
|
(hasCleanRecoverableOpenCodeRefresh &&
|
|
input.providerId === 'opencode' &&
|
|
isRecoverableOpenCodeSessionRefreshText(normalized)) ||
|
|
(hasCleanRecoverableOpenCodeRefresh && isGenericOpenCodeApiErrorText(normalized))
|
|
) {
|
|
continue;
|
|
}
|
|
return boundedString(normalized);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function uniqueDiagnostics(
|
|
...groups: (readonly (string | undefined)[] | undefined)[]
|
|
): string[] | undefined {
|
|
const seen = new Set<string>();
|
|
const diagnostics: string[] = [];
|
|
for (const group of groups) {
|
|
for (const item of group ?? []) {
|
|
const normalized = boundedString(item);
|
|
if (!normalized || seen.has(normalized)) continue;
|
|
seen.add(normalized);
|
|
diagnostics.push(normalized);
|
|
if (diagnostics.length >= MAX_DIAGNOSTIC_ITEMS) {
|
|
return diagnostics;
|
|
}
|
|
}
|
|
}
|
|
return diagnostics.length > 0 ? diagnostics : undefined;
|
|
}
|
|
|
|
function textIncludesAny(text: string, needles: readonly string[]): boolean {
|
|
return needles.some((needle) => text.includes(needle));
|
|
}
|
|
|
|
function buildDiagnosticHints(input: {
|
|
memberCardError?: string;
|
|
runtimeDiagnostic?: string;
|
|
diagnostics?: readonly string[];
|
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
|
launchState?: MemberLaunchState;
|
|
spawnStatus?: MemberSpawnStatus;
|
|
providerId?: string;
|
|
}): string[] | undefined {
|
|
const text = [input.memberCardError, input.runtimeDiagnostic, ...(input.diagnostics ?? [])]
|
|
.filter((item): item is string => Boolean(item))
|
|
.join('\n')
|
|
.toLowerCase();
|
|
const openCodeRefreshEvidenceContext = [input.runtimeDiagnostic, ...(input.diagnostics ?? [])];
|
|
const hasCleanRecoverableOpenCodeRefreshEvidence =
|
|
input.providerId === 'opencode' &&
|
|
hasCleanRecoverableOpenCodeRefreshContext(openCodeRefreshEvidenceContext);
|
|
const hints: string[] = [];
|
|
|
|
if (textIncludesAny(text, ['reason=query_active', 'queryguardstatus=running'])) {
|
|
hints.push(
|
|
'Bootstrap submit was rejected because the teammate REPL already had a running query.'
|
|
);
|
|
}
|
|
if (textIncludesAny(text, ['queryguardstatus=dispatching'])) {
|
|
hints.push(
|
|
'Bootstrap submit collided with a queued prompt dispatch before the model turn started.'
|
|
);
|
|
}
|
|
if (
|
|
textIncludesAny(text, [
|
|
'reason=command_queue_busy',
|
|
'commandqueuemodes=prompt',
|
|
'commandqueuemodes=bash',
|
|
])
|
|
) {
|
|
hints.push(
|
|
'Bootstrap submit was rejected because local prompt/bash command queue was not empty.'
|
|
);
|
|
}
|
|
if (
|
|
textIncludesAny(text, ['bootstrap_submit_rejected', 'submit rejected by local prompt handler'])
|
|
) {
|
|
hints.push(
|
|
'The teammate process observed bootstrap mail, but local prompt submission did not accept the bootstrap turn.'
|
|
);
|
|
}
|
|
if (
|
|
textIncludesAny(text, [
|
|
'did not bootstrap-confirm',
|
|
'bootstrap-confirm before timeout',
|
|
'bootstrap was not confirmed',
|
|
'last transport stage: bootstrap_submitted',
|
|
])
|
|
) {
|
|
hints.push(
|
|
'Bootstrap prompt was submitted, but teammate did not bootstrap-confirm before timeout.'
|
|
);
|
|
}
|
|
if (
|
|
textIncludesAny(text, [
|
|
'did not submit bootstrap prompt',
|
|
'timed out waiting for bootstrap_submitted',
|
|
])
|
|
) {
|
|
hints.push('Parent process timed out waiting for durable bootstrap_submitted evidence.');
|
|
}
|
|
if (textIncludesAny(text, ['no stdin data received in 3s'])) {
|
|
hints.push(
|
|
'CLI read empty stdin before bootstrap submit; verify headless teammate runtime flag/env and startup input handling.'
|
|
);
|
|
}
|
|
if (
|
|
input.livenessKind === 'stale_metadata' ||
|
|
textIncludesAny(text, ['persisted runtime pid is not alive'])
|
|
) {
|
|
hints.push(
|
|
'Persisted runtime pid is dead; this is post-failure liveness, not the original root cause.'
|
|
);
|
|
}
|
|
if (
|
|
(input.launchState === 'failed_to_start' || input.spawnStatus === 'error') &&
|
|
!(hasCleanRecoverableOpenCodeRefreshEvidence && !input.memberCardError)
|
|
) {
|
|
hints.push(
|
|
'Launch state is terminal for this run; restart/relaunch is required after fixing the cause.'
|
|
);
|
|
}
|
|
|
|
return hints.length > 0 ? [...new Set(hints)].slice(0, 8) : undefined;
|
|
}
|
|
|
|
function buildProbableCause(hints: readonly string[] | undefined): string | undefined {
|
|
return hints?.[0];
|
|
}
|
|
|
|
export function buildMemberLaunchDiagnosticsPayload(params: {
|
|
teamName?: string | null;
|
|
runId?: string | null;
|
|
memberName: string;
|
|
member?: MemberDiagnosticsMemberLike;
|
|
spawnStatus?: MemberSpawnStatus;
|
|
launchState?: MemberLaunchState;
|
|
livenessSource?: MemberSpawnLivenessSource;
|
|
spawnEntry?: MemberSpawnStatusEntry;
|
|
runtimeEntry?: TeamAgentRuntimeEntry;
|
|
runtimeAdvisory?: MemberRuntimeAdvisory;
|
|
runtimeAdvisoryLabel?: string | null;
|
|
runtimeAdvisoryTitle?: string;
|
|
}): MemberLaunchDiagnosticsPayload {
|
|
const spawnEntry = params.spawnEntry;
|
|
const runtimeEntry = params.runtimeEntry;
|
|
const runtimeAdvisory = params.runtimeAdvisory;
|
|
const providerId = runtimeEntry?.providerId ?? params.member?.providerId;
|
|
const providerBackendId = runtimeEntry?.providerBackendId ?? params.member?.providerBackendId;
|
|
const laneId = runtimeEntry?.laneId ?? params.member?.laneId;
|
|
const laneKind = runtimeEntry?.laneKind ?? params.member?.laneKind;
|
|
const livenessKind = hasStoppedRuntimeLivenessKind(runtimeEntry?.livenessKind)
|
|
? runtimeEntry?.livenessKind
|
|
: (spawnEntry?.livenessKind ?? runtimeEntry?.livenessKind);
|
|
const bootstrapConfirmedProvisionedButNotAlive =
|
|
isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry);
|
|
const hasUnsafeSpawnProvisionedButNotAliveEvidence =
|
|
bootstrapConfirmedProvisionedButNotAlive &&
|
|
hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawnEntry);
|
|
const hasUnsafeRuntimeProvisionedButNotAliveEvidence =
|
|
bootstrapConfirmedProvisionedButNotAlive &&
|
|
!hasUnsafeSpawnProvisionedButNotAliveEvidence &&
|
|
hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry);
|
|
const hasUnsafeProvisionedButNotAliveEvidence =
|
|
bootstrapConfirmedProvisionedButNotAlive &&
|
|
(hasUnsafeSpawnProvisionedButNotAliveEvidence ||
|
|
hasUnsafeRuntimeProvisionedButNotAliveEvidence);
|
|
const useBootstrapConfirmedVisualState =
|
|
bootstrapConfirmedProvisionedButNotAlive &&
|
|
spawnEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
|
runtimeEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
|
!hasUnsafeProvisionedButNotAliveEvidence;
|
|
const useBootstrapConfirmedRuntimeAlive =
|
|
useBootstrapConfirmedVisualState &&
|
|
runtimeEntry?.runtimeDiagnosticSeverity !== 'error' &&
|
|
spawnEntry?.runtimeDiagnosticSeverity !== 'error';
|
|
const runtimeEntryDiagnostic = boundedString(runtimeEntry?.runtimeDiagnostic);
|
|
const hasRuntimeDiagnosticEvidence =
|
|
runtimeEntryDiagnostic != null || runtimeEntry?.runtimeDiagnosticSeverity != null;
|
|
const useSpawnDiagnosticsForHealedEntry =
|
|
bootstrapConfirmedProvisionedButNotAlive && !hasRuntimeDiagnosticEvidence;
|
|
const keepSpawnFailureDiagnostics =
|
|
useSpawnDiagnosticsForHealedEntry ||
|
|
hasUnsafeSpawnProvisionedButNotAliveEvidence ||
|
|
spawnEntry?.runtimeDiagnosticSeverity === 'error';
|
|
const launchState = useBootstrapConfirmedVisualState
|
|
? 'confirmed_alive'
|
|
: (spawnEntry?.launchState ?? params.launchState);
|
|
const spawnStatus = useBootstrapConfirmedVisualState
|
|
? 'online'
|
|
: (spawnEntry?.status ?? params.spawnStatus);
|
|
const spawnRuntimeAlive = useBootstrapConfirmedRuntimeAlive ? true : spawnEntry?.runtimeAlive;
|
|
const spawnHardFailure = useBootstrapConfirmedVisualState ? false : spawnEntry?.hardFailure;
|
|
const runtimeAdvisoryTitle = boundedString(params.runtimeAdvisoryTitle);
|
|
const runtimeAdvisoryLabel = boundedString(params.runtimeAdvisoryLabel ?? undefined);
|
|
const runtimeAdvisoryMessage = boundedString(runtimeAdvisory?.message);
|
|
const suppressOpenCodeAppMcpAdvisory = isHealthyOpenCodeAppMcpConnectivityAdvisory({
|
|
providerId,
|
|
runtimeAdvisory,
|
|
runtimeAdvisoryLabel,
|
|
runtimeAdvisoryTitle,
|
|
runtimeAdvisoryMessage,
|
|
spawnStatus,
|
|
launchState,
|
|
runtimeAlive: spawnRuntimeAlive,
|
|
bootstrapConfirmed: spawnEntry?.bootstrapConfirmed,
|
|
agentToolAccepted: spawnEntry?.agentToolAccepted,
|
|
hardFailure: spawnHardFailure,
|
|
livenessKind,
|
|
runtimeEntry,
|
|
});
|
|
const runtimeAdvisoryCardError =
|
|
!suppressOpenCodeAppMcpAdvisory && isRuntimeAdvisoryCardError(runtimeAdvisory, providerId)
|
|
? (runtimeAdvisoryTitle ?? runtimeAdvisoryLabel ?? runtimeAdvisoryMessage)
|
|
: undefined;
|
|
const runtimeDiagnosticSeverity =
|
|
spawnEntry?.runtimeDiagnosticSeverity === 'error'
|
|
? spawnEntry.runtimeDiagnosticSeverity
|
|
: bootstrapConfirmedProvisionedButNotAlive
|
|
? (runtimeEntry?.runtimeDiagnosticSeverity ??
|
|
(useSpawnDiagnosticsForHealedEntry ? spawnEntry?.runtimeDiagnosticSeverity : undefined))
|
|
: (spawnEntry?.runtimeDiagnosticSeverity ?? runtimeEntry?.runtimeDiagnosticSeverity);
|
|
const spawnRuntimeDiagnosticCardError = isRuntimeDiagnosticCardError({
|
|
runtimeDiagnostic:
|
|
bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics
|
|
? undefined
|
|
: spawnEntry?.runtimeDiagnostic,
|
|
runtimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity,
|
|
launchState: spawnEntry?.launchState,
|
|
spawnStatus: spawnEntry?.status,
|
|
hardFailure: spawnEntry?.hardFailure,
|
|
providerId,
|
|
})
|
|
? spawnEntry?.runtimeDiagnostic
|
|
: undefined;
|
|
const healedSpawnFailureCardError =
|
|
keepSpawnFailureDiagnostics && spawnEntry?.runtimeDiagnosticSeverity === 'error'
|
|
? (spawnRuntimeDiagnosticCardError ?? spawnEntry?.error ?? spawnEntry?.hardFailureReason)
|
|
: undefined;
|
|
const runtimeEntryDiagnosticCardError = isRuntimeDiagnosticCardError({
|
|
runtimeDiagnostic: runtimeEntry?.runtimeDiagnostic,
|
|
runtimeDiagnosticSeverity: runtimeEntry?.runtimeDiagnosticSeverity,
|
|
providerId,
|
|
})
|
|
? runtimeEntry?.runtimeDiagnostic
|
|
: undefined;
|
|
const runtimeDiagnostic =
|
|
(bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics
|
|
? undefined
|
|
: boundedString(spawnEntry?.runtimeDiagnostic)) ??
|
|
runtimeEntryDiagnostic ??
|
|
(bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics
|
|
? undefined
|
|
: (boundedString(spawnEntry?.hardFailureReason) ?? boundedString(spawnEntry?.error))) ??
|
|
runtimeAdvisoryMessage;
|
|
const memberCardError = firstMemberCardFailureReason({
|
|
candidates: bootstrapConfirmedProvisionedButNotAlive
|
|
? [healedSpawnFailureCardError, runtimeEntryDiagnosticCardError, runtimeAdvisoryCardError]
|
|
: [
|
|
spawnEntry?.error,
|
|
spawnEntry?.hardFailureReason,
|
|
spawnRuntimeDiagnosticCardError,
|
|
runtimeEntryDiagnosticCardError,
|
|
runtimeAdvisoryCardError,
|
|
],
|
|
evidence: [
|
|
spawnEntry?.runtimeDiagnostic,
|
|
runtimeEntry?.runtimeDiagnostic,
|
|
runtimeAdvisoryTitle,
|
|
runtimeAdvisoryLabel,
|
|
runtimeAdvisoryMessage,
|
|
...(runtimeEntry?.diagnostics ?? []),
|
|
],
|
|
providerId,
|
|
});
|
|
const diagnostics = uniqueDiagnostics(
|
|
memberCardError ? [memberCardError] : undefined,
|
|
runtimeDiagnostic ? [runtimeDiagnostic] : undefined,
|
|
runtimeAdvisoryTitle ? [runtimeAdvisoryTitle] : undefined,
|
|
runtimeAdvisoryLabel ? [runtimeAdvisoryLabel] : undefined,
|
|
runtimeAdvisoryMessage ? [runtimeAdvisoryMessage] : undefined,
|
|
(!bootstrapConfirmedProvisionedButNotAlive || keepSpawnFailureDiagnostics) &&
|
|
spawnEntry?.hardFailureReason
|
|
? [spawnEntry.hardFailureReason]
|
|
: undefined,
|
|
(!bootstrapConfirmedProvisionedButNotAlive || keepSpawnFailureDiagnostics) && spawnEntry?.error
|
|
? [spawnEntry.error]
|
|
: undefined,
|
|
runtimeEntry?.diagnostics
|
|
);
|
|
const runId = boundedString(params.runId ?? undefined);
|
|
const runtimeUpdatedAt = maybeString(runtimeEntry?.updatedAt);
|
|
const spawnUpdatedAt = maybeString(spawnEntry?.updatedAt);
|
|
const diagnosticHints = buildDiagnosticHints({
|
|
memberCardError,
|
|
runtimeDiagnostic,
|
|
diagnostics,
|
|
livenessKind,
|
|
launchState,
|
|
spawnStatus,
|
|
providerId,
|
|
});
|
|
const probableCause = buildProbableCause(diagnosticHints);
|
|
|
|
return {
|
|
...(params.teamName ? { teamName: params.teamName } : {}),
|
|
...(runId ? { runId } : {}),
|
|
memberName: params.memberName,
|
|
...(providerId ? { providerId } : {}),
|
|
...(providerBackendId ? { providerBackendId } : {}),
|
|
...(maybeString(params.member?.model) ? { model: maybeString(params.member?.model) } : {}),
|
|
...(maybeString(runtimeEntry?.runtimeModel ?? spawnEntry?.runtimeModel)
|
|
? { runtimeModel: maybeString(runtimeEntry?.runtimeModel ?? spawnEntry?.runtimeModel) }
|
|
: {}),
|
|
...(maybeString(params.member?.agentType)
|
|
? { agentType: maybeString(params.member?.agentType) }
|
|
: {}),
|
|
...(maybeString(laneId) ? { laneId: maybeString(laneId) } : {}),
|
|
...(laneKind ? { laneKind } : {}),
|
|
...(params.member?.laneOwnerProviderId
|
|
? { laneOwnerProviderId: params.member.laneOwnerProviderId }
|
|
: {}),
|
|
...(boundedNumber(params.member?.removedAt)
|
|
? { removedAt: boundedNumber(params.member?.removedAt) }
|
|
: {}),
|
|
...(memberCardError ? { memberCardError } : {}),
|
|
...(launchState ? { launchState } : {}),
|
|
...(spawnStatus ? { spawnStatus } : {}),
|
|
...(runtimeEntry?.backendType ? { backendType: runtimeEntry.backendType } : {}),
|
|
...(typeof runtimeEntry?.alive === 'boolean' ? { alive: runtimeEntry.alive } : {}),
|
|
...(typeof runtimeEntry?.restartable === 'boolean'
|
|
? { restartable: runtimeEntry.restartable }
|
|
: {}),
|
|
...(typeof spawnRuntimeAlive === 'boolean' ? { runtimeAlive: spawnRuntimeAlive } : {}),
|
|
...(typeof spawnEntry?.bootstrapConfirmed === 'boolean'
|
|
? { bootstrapConfirmed: spawnEntry.bootstrapConfirmed }
|
|
: {}),
|
|
...(typeof spawnEntry?.agentToolAccepted === 'boolean'
|
|
? { agentToolAccepted: spawnEntry.agentToolAccepted }
|
|
: {}),
|
|
...(typeof spawnHardFailure === 'boolean' ? { hardFailure: spawnHardFailure } : {}),
|
|
...(livenessKind ? { livenessKind } : {}),
|
|
...((spawnEntry?.livenessSource ?? params.livenessSource)
|
|
? { livenessSource: spawnEntry?.livenessSource ?? params.livenessSource }
|
|
: {}),
|
|
...(boundedNumber(runtimeEntry?.pid) ? { pid: boundedNumber(runtimeEntry?.pid) } : {}),
|
|
...(runtimeEntry?.pidSource ? { pidSource: runtimeEntry.pidSource } : {}),
|
|
...(boundedString(runtimeEntry?.paneId) ? { paneId: boundedString(runtimeEntry?.paneId) } : {}),
|
|
...(boundedNumber(runtimeEntry?.panePid)
|
|
? { panePid: boundedNumber(runtimeEntry?.panePid) }
|
|
: {}),
|
|
...(boundedString(runtimeEntry?.paneCurrentCommand)
|
|
? { paneCurrentCommand: boundedString(runtimeEntry?.paneCurrentCommand) }
|
|
: {}),
|
|
...(boundedString(runtimeEntry?.processCommand)
|
|
? { processCommand: boundedString(runtimeEntry?.processCommand) }
|
|
: {}),
|
|
...(boundedNumber(runtimeEntry?.runtimePid)
|
|
? { runtimePid: boundedNumber(runtimeEntry?.runtimePid) }
|
|
: {}),
|
|
...(boundedString(runtimeEntry?.runtimeSessionId)
|
|
? { runtimeSessionId: boundedString(runtimeEntry?.runtimeSessionId) }
|
|
: {}),
|
|
...(maybeString(runtimeEntry?.runtimeLeaseExpiresAt)
|
|
? { runtimeLeaseExpiresAt: maybeString(runtimeEntry?.runtimeLeaseExpiresAt) }
|
|
: {}),
|
|
...(maybeString(runtimeEntry?.runtimeLastSeenAt ?? spawnEntry?.lastHeartbeatAt)
|
|
? {
|
|
runtimeLastSeenAt: maybeString(
|
|
runtimeEntry?.runtimeLastSeenAt ?? spawnEntry?.lastHeartbeatAt
|
|
),
|
|
}
|
|
: {}),
|
|
...(typeof runtimeEntry?.historicalBootstrapConfirmed === 'boolean'
|
|
? { historicalBootstrapConfirmed: runtimeEntry.historicalBootstrapConfirmed }
|
|
: {}),
|
|
...(maybeString(runtimeEntry?.cwd) ? { cwd: maybeString(runtimeEntry?.cwd) } : {}),
|
|
...(boundedNumber(runtimeEntry?.rssBytes)
|
|
? { rssBytes: boundedNumber(runtimeEntry?.rssBytes) }
|
|
: {}),
|
|
...(runtimeDiagnostic ? { runtimeDiagnostic } : {}),
|
|
...(runtimeDiagnosticSeverity
|
|
? {
|
|
runtimeDiagnosticSeverity,
|
|
}
|
|
: {}),
|
|
...(runtimeAdvisory?.kind ? { runtimeAdvisoryKind: runtimeAdvisory.kind } : {}),
|
|
...(runtimeAdvisory?.reasonCode
|
|
? { runtimeAdvisoryReasonCode: runtimeAdvisory.reasonCode }
|
|
: {}),
|
|
...(maybeString(runtimeAdvisory?.observedAt)
|
|
? { runtimeAdvisoryObservedAt: maybeString(runtimeAdvisory?.observedAt) }
|
|
: {}),
|
|
...(maybeString(runtimeAdvisory?.retryUntil)
|
|
? { runtimeAdvisoryRetryUntil: maybeString(runtimeAdvisory?.retryUntil) }
|
|
: {}),
|
|
...(boundedNumber(runtimeAdvisory?.retryDelayMs)
|
|
? { runtimeAdvisoryRetryDelayMs: boundedNumber(runtimeAdvisory?.retryDelayMs) }
|
|
: {}),
|
|
...(spawnEntry?.bootstrapStalled === true ? { bootstrapStalled: true } : {}),
|
|
...(boundedStringArray(spawnEntry?.pendingPermissionRequestIds)
|
|
? { pendingPermissionRequestIds: boundedStringArray(spawnEntry?.pendingPermissionRequestIds) }
|
|
: {}),
|
|
...(maybeString(spawnEntry?.firstSpawnAcceptedAt)
|
|
? { firstSpawnAcceptedAt: maybeString(spawnEntry?.firstSpawnAcceptedAt) }
|
|
: {}),
|
|
...(maybeString(spawnEntry?.lastHeartbeatAt)
|
|
? { lastHeartbeatAt: maybeString(spawnEntry?.lastHeartbeatAt) }
|
|
: {}),
|
|
...(maybeString(spawnEntry?.livenessLastCheckedAt)
|
|
? { livenessLastCheckedAt: maybeString(spawnEntry?.livenessLastCheckedAt) }
|
|
: {}),
|
|
...(probableCause ? { probableCause } : {}),
|
|
...(diagnosticHints ? { diagnosticHints } : {}),
|
|
...(diagnostics ? { diagnostics } : {}),
|
|
...(spawnUpdatedAt ? { spawnUpdatedAt } : {}),
|
|
...(runtimeUpdatedAt ? { runtimeUpdatedAt } : {}),
|
|
...(boundedString(spawnEntry?.updatedAt ?? runtimeEntry?.updatedAt)
|
|
? { updatedAt: boundedString(spawnEntry?.updatedAt ?? runtimeEntry?.updatedAt) }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
function parseStatusUpdatedAtMs(value: string | undefined): number | null {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const parsed = Date.parse(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean {
|
|
if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) {
|
|
return hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry);
|
|
}
|
|
return entry?.launchState === 'failed_to_start' || entry?.status === 'error';
|
|
}
|
|
|
|
function shouldPreferSnapshotEntryOverLive(params: {
|
|
liveEntry: MemberSpawnStatusEntry | undefined;
|
|
snapshotEntry: MemberSpawnStatusEntry | undefined;
|
|
snapshotUpdatedAt?: string;
|
|
}): boolean {
|
|
const { liveEntry, snapshotEntry, snapshotUpdatedAt } = params;
|
|
if (!liveEntry || !snapshotEntry) {
|
|
return false;
|
|
}
|
|
if (!isFailedSpawnEntry(liveEntry) || isFailedSpawnEntry(snapshotEntry)) {
|
|
return false;
|
|
}
|
|
|
|
const liveUpdatedAtMs = parseStatusUpdatedAtMs(liveEntry.updatedAt);
|
|
const snapshotUpdatedAtMs =
|
|
parseStatusUpdatedAtMs(snapshotEntry.updatedAt) ?? parseStatusUpdatedAtMs(snapshotUpdatedAt);
|
|
return (
|
|
snapshotUpdatedAtMs != null &&
|
|
(liveUpdatedAtMs == null || snapshotUpdatedAtMs >= liveUpdatedAtMs)
|
|
);
|
|
}
|
|
|
|
function getPreferredSpawnEntry(params: {
|
|
liveEntry: MemberSpawnStatusEntry | undefined;
|
|
snapshotEntry: MemberSpawnStatusEntry | undefined;
|
|
snapshotUpdatedAt?: string;
|
|
}): MemberSpawnStatusEntry | undefined {
|
|
return shouldPreferSnapshotEntryOverLive(params)
|
|
? params.snapshotEntry
|
|
: (params.liveEntry ?? params.snapshotEntry);
|
|
}
|
|
|
|
function getSpawnEntry(
|
|
collection: MemberSpawnStatusCollection,
|
|
name: string
|
|
): MemberSpawnStatusEntry | undefined {
|
|
return collection instanceof Map ? collection.get(name) : collection?.[name];
|
|
}
|
|
|
|
export function buildTeamMemberLaunchDiagnosticsPayloads(params: {
|
|
teamName?: string | null;
|
|
runId?: string | null;
|
|
members?: readonly MemberDiagnosticsMemberLike[];
|
|
memberSpawnStatuses?: MemberSpawnStatusCollection;
|
|
memberSpawnSnapshot?: {
|
|
statuses?: Record<string, MemberSpawnStatusEntry>;
|
|
updatedAt?: string;
|
|
};
|
|
runtimeEntries?: Record<string, TeamAgentRuntimeEntry> | null;
|
|
}): MemberLaunchDiagnosticsPayload[] {
|
|
const membersByName = new Map(
|
|
(params.members ?? [])
|
|
.map((member) => [member.name.trim(), member] as const)
|
|
.filter(([name]) => name.length > 0)
|
|
);
|
|
const names = new Set<string>(membersByName.keys());
|
|
if (params.memberSpawnStatuses instanceof Map) {
|
|
for (const name of params.memberSpawnStatuses.keys()) {
|
|
names.add(name);
|
|
}
|
|
} else {
|
|
for (const name of Object.keys(params.memberSpawnStatuses ?? {})) {
|
|
names.add(name);
|
|
}
|
|
}
|
|
for (const name of Object.keys(params.memberSpawnSnapshot?.statuses ?? {})) {
|
|
names.add(name);
|
|
}
|
|
for (const name of Object.keys(params.runtimeEntries ?? {})) {
|
|
names.add(name);
|
|
}
|
|
|
|
return [...names]
|
|
.sort((left, right) => left.localeCompare(right))
|
|
.map((name) => {
|
|
const liveEntry = getSpawnEntry(params.memberSpawnStatuses, name);
|
|
const snapshotEntry = params.memberSpawnSnapshot?.statuses?.[name];
|
|
return buildMemberLaunchDiagnosticsPayload({
|
|
teamName: params.teamName,
|
|
runId: params.runId,
|
|
memberName: name,
|
|
member: membersByName.get(name),
|
|
spawnEntry: getPreferredSpawnEntry({
|
|
liveEntry,
|
|
snapshotEntry,
|
|
snapshotUpdatedAt: params.memberSpawnSnapshot?.updatedAt,
|
|
}),
|
|
runtimeEntry: params.runtimeEntries?.[name],
|
|
});
|
|
});
|
|
}
|
|
|
|
export function hasMemberLaunchDiagnosticsDetails(
|
|
payload: MemberLaunchDiagnosticsPayload
|
|
): boolean {
|
|
const weakLiveness =
|
|
payload.livenessKind === 'runtime_process_candidate' ||
|
|
payload.livenessKind === 'permission_blocked' ||
|
|
payload.livenessKind === 'shell_only' ||
|
|
payload.livenessKind === 'registered_only' ||
|
|
payload.livenessKind === 'stale_metadata' ||
|
|
payload.livenessKind === 'not_found';
|
|
return Boolean(
|
|
(payload.launchState && payload.launchState !== 'confirmed_alive') ||
|
|
(payload.spawnStatus && payload.spawnStatus !== 'online') ||
|
|
payload.memberCardError ||
|
|
payload.bootstrapStalled === true ||
|
|
weakLiveness ||
|
|
payload.runtimeDiagnostic ||
|
|
payload.diagnostics?.length
|
|
);
|
|
}
|
|
|
|
export function hasMemberLaunchDiagnosticsError(payload: MemberLaunchDiagnosticsPayload): boolean {
|
|
if (
|
|
payload.providerId === 'opencode' &&
|
|
!payload.memberCardError &&
|
|
hasCleanRecoverableOpenCodeRefreshContext([
|
|
payload.runtimeDiagnostic,
|
|
...(payload.diagnostics ?? []),
|
|
])
|
|
) {
|
|
return false;
|
|
}
|
|
return Boolean(
|
|
payload.memberCardError ||
|
|
payload.spawnStatus === 'error' ||
|
|
payload.launchState === 'failed_to_start' ||
|
|
payload.runtimeDiagnosticSeverity === 'error'
|
|
);
|
|
}
|
|
|
|
export function getMemberLaunchDiagnosticsErrorMessage(
|
|
payload: MemberLaunchDiagnosticsPayload
|
|
): string | undefined {
|
|
if (!hasMemberLaunchDiagnosticsError(payload)) {
|
|
return undefined;
|
|
}
|
|
return (
|
|
payload.memberCardError ??
|
|
payload.runtimeDiagnostic ??
|
|
payload.diagnostics?.[0] ??
|
|
'Launch failed'
|
|
);
|
|
}
|
|
|
|
export function formatMemberLaunchDiagnosticsPayload(
|
|
payload: MemberLaunchDiagnosticsPayload
|
|
): string {
|
|
return JSON.stringify(payload, null, 2);
|
|
}
|