Skip to content

fix: preserve provider failure taxonomy through retries and Desktop presentation #1216

Description

@xuruiray

Summary

Local end-to-end validation of #1214 found two separate boundary defects in the provider-failure path:

  1. Retryable provider failures lose their taxonomy after Vercel AI SDK wraps the final APICallError in AI_RetryError.
  2. Desktop receives a safe, structured ErrorEvent.reason, but reparses only event.message, producing generic or unknown UI copy.

These are existing behavioral gaps, not regressions introduced by #1214. The extraction in #1214 remains byte-for-byte behavior-neutral and should stay separate from these fixes.

Reproduction evidence

Reproduced on 2026-07-19 from commit 44baea50 with:

  • the real Electron Desktop renderer and IPC bridge
  • SessionManager -> AiSdkBackend -> ModelAdapter -> RuntimeEvent -> TurnRecord
  • the real ai@7.0.31 OpenAI-compatible adapter
  • an isolated loopback provider and isolated Desktop user-data directory
  • no real provider credentials

1. Retry wrapper loses 429 / 503 classification

The loopback provider returned one response per request. AI SDK made three attempts for each retryable failure:

Scenario Provider requests Expected final reason Actual final reason / TurnRecord
HTTP 429 3 rate_limit no reason / unknown
HTTP 503 3 provider_unavailable no reason / unknown

A direct runtime probe showed:

429 outer class: AI_RetryError
429 lastError class: RateLimit

503 outer class: AI_RetryError
503 lastError class: ProviderUnavailable

The outer error exposes no top-level provider status. AI SDK keeps the final AI_APICallError in its public lastError field, but Maka currently classifies only the outer Error.

2. Desktop discards the structured reason

The runtime correctly emitted and persisted:

reason: context_overflow
message: Context window exceeded
TurnRecord.errorClass: context_overflow

The visible Desktop toast was still the generic Chinese fallback:

对话运行失败,请稍后重试。

Desktop context-overflow event rendered as a generic conversation error

The same pure presentation probe currently produces:

Stable reason Toast Failed-turn label
context_overflow generic failure unknown error
timeout generic failure request timeout
provider_billing generic failure unknown error
provider_unavailable generic failure model service error
auth / rate_limit / network categorized categorized

Current failure path

flowchart LR
  subgraph R["Runtime classification gap"]
    R1["HTTP 429 / 503"] --> R2["AI_APICallError<br/>statusCode available"]
    R2 --> R3["AI_RetryError<br/>lastError available"]
    R3 --> R4["Classify outer error"]
    R4 --> R5["unknown"]
    R3 -. "Unwrap official retry error" .-> R6["rate_limit / provider_unavailable"]
  end

  subgraph D["Desktop presentation gap"]
    D1["ErrorEvent<br/>reason + safe message"] --> D2["Drop reason"]
    D2 --> D3["Reparse safe English message"]
    D3 --> D4["generic toast / unknown label"]
    D1 -. "Allowlisted reason mapping" .-> D5["localized actionable copy"]
  end
Loading

Root cause

Runtime

The normalizer reads statusCode, code, provider data, and response bodies from the error it receives. It does not recognize the official AI SDK retry wrapper or inspect lastError.

Both terminal event projection and telemetry use that classification, so the loss propagates into:

  • ErrorEvent.reason
  • TurnRecord / AgentRun failure taxonomy
  • Runtime Event Log and traces
  • headless provider-infrastructure accounting
  • Desktop presentation

This does not cause an extra retry today: AI SDK has already exhausted its retry policy, and Maka's additional transport retry remains limited to the existing Network path.

Desktop

ErrorEvent.reason is explicitly the stable machine-readable UI/telemetry routing field. sessionEventErrorMessage() ignores it and passes only new Error(event.message) into the generic redaction classifier.

That safe message is already a projection. Parsing it again is lossy: for example, Request timed out does not contain the current timeout keyword, and Provider returned an error no longer contains a 5xx status.

The failed-turn label has a separate allowlist, but it currently omits at least context_overflow and provider_billing.

Related work

Non-goals

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions