import { isMediaOverflowError } from './media-overflow' import { isUnsupportedCodexAcpVersionError } from './codex-runtime' import { CLAUDE_CLI_INCOMPATIBLE_MESSAGE } from './claude-runtime' export { CLAUDE_CLI_INCOMPATIBLE_MESSAGE, isClaudeCliCompatibilityError } from './claude-runtime' // Classifies a failed run into "expected" (keep the message, no report button) vs "unknown/reportable" // (an opaque or internal failure worth a GitHub issue). The primary signal is STRUCTURAL, not textual: // a model/provider failure is tagged `providerError` on the error event at the ACP layer (runtime.ts, // via isProviderPromptError) and persisted as `session.errorReportable = false`. Arbitrary provider // wording is not guessed from text — that was fragile and repeatedly swallowed genuine app errors. // // This module owns only the SECONDARY, text-based tier: recognizing the app's OWN crafted reminder // strings (which we author, so an exact-match set is reliable) so their report button is hidden even // on the paths that don't carry the structural flag (a persisted pre-flag session, or a renderer-side // failRun call). Two fixed Claude Code transport wrappers are recognized here (`API Error: Unable to // connect to API` and `API Error: Connection closed mid-response`) so historical sessions and // createSession failRun hide Report without rewriting stored records. Arbitrary provider text is still // not guessed. It is a pure, dependency-light leaf module (like media-overflow.ts) usable from both // processes. // App-crafted resume-failure messages (useWorkspaceAgentRuntime.getResumeFailureMessage). Each is the // actionable text the app writes when it recognizes a specific resume cause. The generic // "Agent session resume failed: …" fallback is deliberately NOT here — an unrecognized resume cause // stays reportable. export const RESUME_WORKSPACE_MISSING_MESSAGE = 'Session workspace is missing; start a new conversation.' export const RESUME_TIMED_OUT_MESSAGE = 'Agent session resume timed out; click Resume to try again.' export const RESUME_UNSUPPORTED_MESSAGE = 'This agent build cannot resume sessions; start a new conversation.' export const RESUME_RECONNECT_FAILED_MESSAGE = 'Could not reconnect to the agent; check it is installed, then click Resume to retry.' export const RESUME_MODEL_INCOMPATIBLE_MESSAGE = "The active model isn't compatible with this agent framework. Open Settings → Model to pick a compatible model or switch frameworks." // A conversation that needs image replay on a text-only model (useWorkspaceAgentRuntime). export const IMAGE_REPLAY_UNSUPPORTED_MESSAGE = 'This conversation needs image replay, but the selected model does not support image input.' // App-owned Vision relay failures cross Electron's invoke boundary as Error text, so public error // identity is kept in shared constants instead of depending on non-enumerable Error properties. export const VISION_MODEL_NOT_CONFIGURED_MESSAGE = "The selected model doesn't support images. Configure a Vision model in Settings > Model to enable image support." const LEGACY_VISION_MODEL_NOT_CONFIGURED_MESSAGE = 'Configure a Vision model in Settings > Model before sending images to this model.' export const VISION_IMAGE_TOO_LARGE_MESSAGE = 'The attached image is too large to prepare for the Vision model.' export const VISION_IMAGE_INVALID_MESSAGE = 'The attached image is invalid.' export const VISION_IMAGE_BUDGET_MESSAGE = 'The current images exceed the Vision evidence request budget.' export const VISION_EVIDENCE_BUDGET_MESSAGE = 'The current Vision evidence exceeds the request budget.' export const VISION_EVIDENCE_INVALID_MESSAGE = 'The Vision model returned invalid image evidence.' const VISION_RUN_FAILURE_MESSAGES = [ VISION_MODEL_NOT_CONFIGURED_MESSAGE, VISION_IMAGE_TOO_LARGE_MESSAGE, VISION_IMAGE_INVALID_MESSAGE, VISION_IMAGE_BUDGET_MESSAGE, VISION_EVIDENCE_BUDGET_MESSAGE, VISION_EVIDENCE_INVALID_MESSAGE ] as const export type VisionRunFailureMessage = (typeof VISION_RUN_FAILURE_MESSAGES)[number] export const visionRunFailureMessage = ( error: string | null | undefined ): VisionRunFailureMessage | undefined => { const message = error?.trim() if (!message) return undefined if ( message === LEGACY_VISION_MODEL_NOT_CONFIGURED_MESSAGE || message.endsWith(`Error: ${LEGACY_VISION_MODEL_NOT_CONFIGURED_MESSAGE}`) ) { return VISION_MODEL_NOT_CONFIGURED_MESSAGE } return VISION_RUN_FAILURE_MESSAGES.find( (candidate) => message === candidate || message.endsWith(`Error: ${candidate}`) ) } // App-authored agent-setup guidance thrown by settings/service.ts:resolveActiveAgentBackend at spawn // time — surfaced when a conversation FAILS TO START (createSession), which does not route through the // resume-path softener. All three are wrong-config the user fixes in Settings → Model, not app bugs, so // they must hide the report button. service.ts builds its throws from these SAME constants/builder so // the text can never drift from what the classifier recognizes. export const NO_ACTIVE_PROVIDER_MESSAGE = 'No active model provider is configured. Configure one in settings.' export const CLAUDE_EXECUTABLE_MISSING_MESSAGE = 'Claude executable is not configured. Complete onboarding in settings.' export const CODEX_BRIDGE_UNSUPPORTED_MESSAGE = 'The active model is not supported over the Codex Chat Completions bridge. Pick another model in Settings → Model.' export const CONFIGURED_MODEL_UNAVAILABLE_PREFIX = 'The configured model is no longer available from provider' export const buildConfiguredModelUnavailableMessage = ( model: string, providerName: string ): string => `${CONFIGURED_MODEL_UNAVAILABLE_PREFIX} "${providerName}": "${model}". Pick another model in Settings → Model.` // The model↔framework mismatch message interpolates the framework name, so the classifier matches on // this leading, framework-independent phrase. It also covers the resume-path RESUME_MODEL_INCOMPATIBLE // wording (both begin here), so either surfacing is recognized. export const ACTIVE_MODEL_INCOMPATIBLE_PREFIX = "The active model isn't compatible with" export const buildActiveModelIncompatibleMessage = (frameworkDisplayName: string): string => `${ACTIVE_MODEL_INCOMPATIBLE_PREFIX} ${frameworkDisplayName}. Open Settings → Model to pick a compatible model or switch the agent framework.` // Stable prefix of the provider "resource not found" message produced by main's describePromptError. // That message interpolates the model name and the provider's own response, so the classifier matches // on this leading, model-independent phrase. prompt-error.ts builds its message from the SAME constant // so the two can never drift (a drift-guard test feeds describePromptError's real output through the // classifier). export const PROVIDER_RESOURCE_NOT_FOUND_PREFIX = 'The model provider could not find the requested resource' export const PROVIDER_CONNECTION_FAILED_PREFIX = 'Could not connect to the model provider' // Claude Code's fixed unreachable-API wrapper. Recognized here so createSession failRun and persisted // pre-flag sessions hide Report without a stored-schema migration. Match the distinctive // `API Error: Unable to connect to API` phrase only — not a generic "connection" or "connect" word. const CLAUDE_API_CONNECTION_FAILURE_PATTERN = /(?:^|:\s*)api error:\s*unable to connect to api\b/i const CLAUDE_API_RESPONSE_INTERRUPTION_PATTERN = /(?:^|:\s*)api error:\s*connection closed mid-response\b/i export const isClaudeApiConnectionFailure = (error: string | null | undefined): boolean => { const message = error?.trim() if (!message) return false return CLAUDE_API_CONNECTION_FAILURE_PATTERN.test(message) } // Claude Code's fixed wrapper for a response stream that ended after partial output. Keep this // separate from connect-time failures: the latter gets base-URL/proxy guidance, while this one is // projected as an interrupted turn that can use the existing Resume flow. export const isClaudeApiResponseInterruption = (error: string | null | undefined): boolean => { const message = error?.trim() if (!message) return false return CLAUDE_API_RESPONSE_INTERRUPTION_PATTERN.test(message) } // The exact app-crafted messages an equality check recognizes as expected. const EXPECTED_RUN_FAILURE_MESSAGES = new Set([ RESUME_WORKSPACE_MISSING_MESSAGE, RESUME_TIMED_OUT_MESSAGE, RESUME_UNSUPPORTED_MESSAGE, RESUME_RECONNECT_FAILED_MESSAGE, RESUME_MODEL_INCOMPATIBLE_MESSAGE, CLAUDE_CLI_INCOMPATIBLE_MESSAGE, IMAGE_REPLAY_UNSUPPORTED_MESSAGE, NO_ACTIVE_PROVIDER_MESSAGE, CLAUDE_EXECUTABLE_MISSING_MESSAGE, CODEX_BRIDGE_UNSUPPORTED_MESSAGE ]) // Whether a run failure is one the app itself already surfaced with a purpose — an app-crafted // actionable reminder, the reworded provider not-found, Claude Code's fixed unreachable-API wrapper, // or a request-size overflow the app auto-recovers — so the report button is hidden even without the // structural `providerError` flag (an old persisted session, or a renderer-side failRun). Recognition // is by EXACT crafted string / known prefix only; it deliberately does NOT try to recognize arbitrary // provider error text (that is the structural flag's job), so an unknown/opaque failure it doesn't // author stays reportable. export const isExpectedRunFailure = (error: string | null | undefined): boolean => { const message = error?.trim() // An empty message is itself an unknown failure (a run failed with nothing to explain it). if (!message) return false if (EXPECTED_RUN_FAILURE_MESSAGES.has(message)) return true if (visionRunFailureMessage(message)) return true // An outdated app-managed adapter is an actionable Settings problem, not a reportable app bug. if (isUnsupportedCodexAcpVersionError(message)) return true // The reworded provider not-found (a model-config problem the user fixes in Settings, not a bug). if (message.startsWith(PROVIDER_RESOURCE_NOT_FOUND_PREFIX)) return true // The actionable provider connection reminder produced for Claude Code connection failures. if (message.startsWith(PROVIDER_CONNECTION_FAILED_PREFIX)) return true if (message.startsWith(CONFIGURED_MODEL_UNAVAILABLE_PREFIX)) return true // Model↔framework incompatibility raised at spawn/createSession. The main-side message names the // framework (`…compatible with Codex.`) while the resume path rewords it to a generic form; both // share this leading phrase, so one prefix covers the createSession path (which is not reworded) and // any framework name. It is app-authored setup guidance ("Open Settings → Model"), not a bug. if (message.startsWith(ACTIVE_MODEL_INCOMPATIBLE_PREFIX)) return true // Claude Code's fixed transport wrappers (createSession / persisted pre-flag sessions). if (isClaudeApiConnectionFailure(message) || isClaudeApiResponseInterruption(message)) return true // A request-size overflow the app auto-recovers from — never a reportable bug. return isMediaOverflowError(message) } // Whether a run failure should offer the "Report error → open a GitHub issue" affordance. True only for // unknown/opaque failures; recognized (expected) ones return false so they are not reported as bugs. export const isReportableRunFailure = (error: string | null | undefined): boolean => !isExpectedRunFailure(error)