Keep OpenCode app MCP transport evidence durable, refresh stale sessions without consuming normal delivery attempts, and keep recoverable runtime diagnostics out of member card errors. Cover stable MCP restart/fallback, forced session refresh, resolved_behavior_changed recovery, and renderer diagnostics with regression and safe e2e tests.
116 lines
4.4 KiB
TypeScript
116 lines
4.4 KiB
TypeScript
import {
|
|
classifyRuntimeDiagnostic,
|
|
selectRuntimeDiagnosticClassification,
|
|
} from '../../runtime/RuntimeDiagnosticClassifier';
|
|
|
|
import type { OpenCodePromptDeliveryLedgerRecord } from './OpenCodePromptDeliveryLedger';
|
|
|
|
export function normalizeOpenCodeRuntimeDeliveryDiagnostic(
|
|
message: string | null | undefined
|
|
): string | null {
|
|
return classifyRuntimeDiagnostic(message).normalizedMessage;
|
|
}
|
|
|
|
export function isGenericOpenCodeRuntimeDeliveryDiagnostic(message: string): boolean {
|
|
return classifyRuntimeDiagnostic(message).generic;
|
|
}
|
|
|
|
export function selectOpenCodeRuntimeDeliveryReason(
|
|
record: OpenCodePromptDeliveryLedgerRecord
|
|
): string | null {
|
|
const candidates = [...record.diagnostics.slice().reverse(), record.lastReason].filter(
|
|
(diagnostic) => !isInformationalOpenCodeRuntimeDeliveryDiagnostic(diagnostic)
|
|
);
|
|
const selected = selectRuntimeDiagnosticClassification(candidates);
|
|
|
|
if (selected && !selected.generic && selected.normalizedMessage) {
|
|
return boundOpenCodeRuntimeDeliveryReason(selected.normalizedMessage);
|
|
}
|
|
|
|
const fallback = getOpenCodeRuntimeDeliveryStateFallback(record);
|
|
if (fallback) {
|
|
return fallback;
|
|
}
|
|
|
|
return selected ? 'OpenCode runtime delivery did not complete.' : null;
|
|
}
|
|
|
|
function isInformationalOpenCodeRuntimeDeliveryDiagnostic(
|
|
message: string | null | undefined
|
|
): boolean {
|
|
const normalized = message?.trim().toLowerCase();
|
|
return (
|
|
normalized === 'opencode app mcp is connected for message delivery.' ||
|
|
normalized ===
|
|
'opencode prompt_async accepted; response observation will continue through durable app-side ledger reconciliation.' ||
|
|
normalized === 'opencode session status busy' ||
|
|
normalized === 'opencode_delivery_response_pending' ||
|
|
normalized === 'opencode_session_refresh_scheduled_after_resolved_behavior_changed' ||
|
|
Boolean(
|
|
normalized?.startsWith('resolved_behavior_changed:') ||
|
|
normalized?.includes('opencode_app_mcp_transport_changed:')
|
|
)
|
|
);
|
|
}
|
|
|
|
export function isActionRequiredOpenCodeRuntimeDeliveryReason(
|
|
message: string | null | undefined
|
|
): boolean {
|
|
return classifyRuntimeDiagnostic(message).actionRequired;
|
|
}
|
|
|
|
function getOpenCodeRuntimeDeliveryStateFallback(
|
|
record: OpenCodePromptDeliveryLedgerRecord
|
|
): string | null {
|
|
const state = record.responseState?.trim();
|
|
const reason = record.lastReason?.trim();
|
|
const normalizedReason = reason?.toLowerCase();
|
|
const diagnostics = record.diagnostics.map((diagnostic) => diagnostic.trim().toLowerCase());
|
|
if (state === 'empty_assistant_turn' || normalizedReason === 'empty_assistant_turn') {
|
|
return 'OpenCode returned an empty assistant turn.';
|
|
}
|
|
if (
|
|
normalizedReason === 'visible_reply_missing_task_refs' ||
|
|
diagnostics.includes('visible_reply_missing_task_refs') ||
|
|
diagnostics.includes('visible_reply_missing_task_refs_after_merge')
|
|
) {
|
|
return 'OpenCode created a reply without the required taskRefs metadata.';
|
|
}
|
|
if (diagnostics.includes('visible_reply_task_refs_merge_failed')) {
|
|
return 'OpenCode created a reply without the required taskRefs metadata, and the app could not attach it automatically.';
|
|
}
|
|
if (
|
|
normalizedReason === 'visible_reply_still_required' ||
|
|
normalizedReason === 'visible_reply_ack_only_still_requires_answer' ||
|
|
normalizedReason === 'plain_text_ack_only_still_requires_answer'
|
|
) {
|
|
return 'OpenCode responded, but did not create a visible message_send reply.';
|
|
}
|
|
if (
|
|
state === 'prompt_delivered_no_assistant_message' ||
|
|
normalizedReason === 'prompt_delivered_no_assistant_message'
|
|
) {
|
|
return 'OpenCode accepted the prompt, but no assistant turn was recorded.';
|
|
}
|
|
if (
|
|
state === 'session_stale' ||
|
|
normalizedReason?.startsWith('resolved_behavior_changed:') ||
|
|
normalizedReason?.startsWith('opencode_app_mcp_transport_changed:')
|
|
) {
|
|
return 'OpenCode session changed; refreshing the session before retry.';
|
|
}
|
|
if (
|
|
normalizedReason === 'visible_reply_destination_not_found_yet' ||
|
|
normalizedReason === 'visible_reply_missing_relayofmessageid'
|
|
) {
|
|
return 'OpenCode created a reply without the required relayOfMessageId correlation.';
|
|
}
|
|
if (normalizedReason === 'non_visible_tool_without_task_progress') {
|
|
return 'OpenCode used tools, but did not create a visible reply or task progress proof.';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function boundOpenCodeRuntimeDeliveryReason(reason: string): string {
|
|
return reason.length > 500 ? `${reason.slice(0, 497).trimEnd()}...` : reason;
|
|
}
|