Skip to content

fix(eve): classify gateway stream-assembler desync as terminal, not recoverable (#1227) - #1439

Open
iroiro147 wants to merge 1 commit into
vercel:mainfrom
iroiro147:fix/gateway-desync-recoverable-1227
Open

fix(eve): classify gateway stream-assembler desync as terminal, not recoverable (#1227)#1439
iroiro147 wants to merge 1 commit into
vercel:mainfrom
iroiro147:fix/gateway-desync-recoverable-1227

Conversation

@iroiro147

Copy link
Copy Markdown
Contributor

Fixes #1227.

Problem

When a task-mode (declared) subagent streams through the AI Gateway, an interleaved provider stream (observed with deepseek/deepseek-v4-flash reasoning parts) intermittently desyncs the AI SDK stream assembler, throwing Error: text part <id> not found from consumeStreamContent. That error carried no gateway signals, no status code, and no SDK isRetryable flag, so it fell through classifyModelCallError's final branch and was classified recoverable.

In task mode eve cannot park for user-driven recovery, so recoverable model-call failures are rethrown for Workflow's durable step retry — which replays the tool loop from the last committed snapshot. Because the desync is deterministic (the provider emits the same malformed frame sequence on replay), the retry re-ran the entire turn repeatedly: production measured single coder turns as 15–70 minute silent replay loops and parent runs of 50–69M replayed cache-read tokens (~5M for clean runs).

Change

classifyModelCallError now detects the stream-assembler desync via a dedicated isStreamAssemblerDesyncError matcher and returns "terminal" so the step fails fast instead of parking for an unbounded durable replay.

  • The matcher anchors on the fixed text part <hex-id> not found message shape (the SDK throws a plain Error here; the part id is a per-stream hex token, so we match the message prefix) and walks the cause chain, mirroring the existing isNoOutputGeneratedError predicate.
  • It runs before the isRetryable duck-typing and the transient/catalog branches, so a desync that the SDK also marks retryable still resolves terminal.

Why this is the right classification

Replaying cannot fix a broken stream frame and there is no partial model state to preserve — a desynced assembler has already lost the parts it needs. The failure is neither transient (nothing to wait out) nor a fixable configuration mistake in the usual sense; it is a deterministic malformed response. Failing the step surfaces the real error immediately and lets the operator switch models (the issue's own workaround: moving subagents off deepseek/deepseek-v4-flash), rather than silently burning a turn's compute on a loop that cannot converge.

Non-goals (deliberately out of scope, tracked separately):

  • Fixing/tolerating the reasoning-part desync itself is in the AI SDK / gateway assembler path (vercel/ai), not eve's tool loop; eve can only classify.
  • A blanket cap on durable-step retries for all recoverable failures is a separate recovery-semantics change that touches the documented "durable step retry" contract for legitimate recoverable errors. This PR removes the misclassified deterministic trigger that produced the reported loops; a general durable-retry budget deserves its own design discussion.

Docs

docs/subagents.mdx → the retry/recovery paragraph now states that a malformed-provider stream desync is terminal, not recoverable, so eve fails the step instead of replaying the whole tool loop.

Validation

  • Targeted tests in model-call-error.test.ts: desync classified terminal even when marked isRetryable; a bare desync (no gateway/status/retryable signals — the exact production shape) is terminal, not the recoverable catch-all; isStreamAssemblerDesyncError matches direct + cause-chain shapes and rejects lookalikes. 40/40 unit pass.
  • tsc -p tsconfig.json --noEmit clean; oxlint clean on both source files; oxfmt --check clean.
  • Changeset: packages/eve/.changeset/gateway-stream-desync-terminal-1227.md (patch).

…ecoverable (vercel#1227)

The AI stream assembler's desync (`text part <id> not found`, raised from
consumeStreamContent when an interleaved provider stream references a part
the assembler no longer tracks — observed with deepseek/deepseek-v4-flash
reasoning parts via the AI Gateway) fell through classifyModelCallError's
recoverable catch-all. eve then parked the session for a durable step retry
that re-ran the entire tool loop from the start: production measured 15-70
minute silent replay loops and 50-69M replayed cache-read tokens per run.

A malformed stream frame is deterministic — replaying the turn re-emits the
same broken sequence, and there is no partial model state to recover. Add a
dedicated isStreamAssemblerDesyncError matcher (message-prefix + cause-chain
walk) and return "terminal" so the step fails fast instead of replaying.

Signed-off-by: iroiro147 <sarthak.singh@juspay.in>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@iroiro147 is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gateway stream desync (text part <id> not found) retried as recoverable task error replays entire durable turns (15-70 min loops)

1 participant