Skip to content

fix(native-chat): decide provider failures by kind, never by searching the payload - #29092

Merged
brennanb2025 merged 5 commits into
mainfrom
brennanb2025/provider-frame-errors-by-kind
Oct 16, 2026
Merged

brennanb2025 merged 5 commits into
mainfrom
brennanb2025/provider-frame-errors-by-kind

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​56 $\color{#cf222e}{\Huge{\mathbf{−}}}$​196 $\color{#cf222e}{\Huge{\mathbf{−}}}$​140
Prod 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​46 $\color{#cf222e}{\Huge{\mathbf{−}}}$​215 $\color{#cf222e}{\Huge{\mathbf{−}}}$​169

Author: @BrennanKB5

ELI5

If one of your MCP servers can't connect, every Claude chat opened with a red error card titled claude · message:system:init. Nothing had actually gone wrong with the chat. Orca was reading a list inside Claude's "session started" message, saw the word failed next to one server, and decided the whole message was an error. Orca no longer hunts through a message for failure-looking words. A message is an error only when its type is an error, or when the field that message type uses to report its own outcome says it failed.

What Changed

What users see

  • Before: with one MCP server down (expired login, removed endpoint, missing binary), every Claude chat started with a red claude · message:system:init card. Its Details held the raw startup message. Users in [Bug]: Agent chat shows error card "claude · message:system:init" instead of session init message #25477 saw it on every session.
  • After: the startup message writes nothing. The chat starts clean.
  • Codex chats used to show a red "MCP server X failed to start" row for the same underlying reason. That row is gone too, so Claude and Codex now match: a failed MCP server shows no chat row (claude mcp list / codex mcp list show why).
  • Real failures still show: a failed hook (with its name and output), a Claude sign-in status error, a failed turn, and every message type that is itself an error (Codex error, warning, Claude permission_denied, …).

How it works

  • classifyProviderFrame used to run hasProviderError first. That function walked every nested field of a message looking for status: "failed", error: … or is_error: true, and its answer overrode the per-type table. That function is deleted. Classification now comes only from the message type, which is the common pattern.
  • Known message types report their own outcome in declared fields, which the type table now reads directly: Claude hook_response.outcome === 'error', Claude auth_status.error when it has a message, and Codex hook/completed run.status === 'failed'.
  • Whether a Claude turn result counts as a failure depends on the turn: a turn the user stopped is not a failure. The result journaling already makes that call (claudeResultFailure), so it now passes its verdict to the fallback row as reportsFailure instead of relying on the search to agree.
  • With nothing left to override the type table, the workaround built for the previous bug of this kind goes too: the list of "covered" types, its coveredByTypedTranslator option, and the sentence helper for background-task messages that name no task. That previous bug was the failed background task that showed as claude · message:system:task_notification (fix(native-chat): give a failed Claude background task a typed row instead of an opcode #20519).

Why

This is the second bug with the same cause. The first was the failed background-task card. It was fixed by exempting those message types from the search, and this one would have been fixed by exempting system:init (#28857). The next message that carries a list of things with their own statuses would hit it again. Removing the search removes the whole class.

Dropped alternatives:

Behavior removed on purpose:

  • Codex's red MCP-startup row.
  • The fallback for a malformed background-task message that names no task.
  • Plugin install failures reported in Claude startup progress. The common pattern treats these progress frames as internal status.
  • Red rows for message types nobody has catalogued that happen to contain failure-looking fields. They are now ordinary fallback rows, drawn only when they carry a sentence.

Linked Issue

Tracked in #25477

An alternative to #28857.

Visual Proof

Same Claude chat, same broken MCP server (one user server whose command does not exist), built from source on a separate Mac with a throwaway home. Both versions also show a genuine "Claude isn't signed in" card, because the test used a deliberately invalid API key.

Before (main ef563bc) After (this branch c8080f7)
before: red claude · message:system:init card above the sign-in card after: new chat shows only the sign-in card

The startup message the before card came from carried only mcp_servers: [{"name":"broken","status":"failed","source":"user"}]. Reopening the old chat in the fixed build still shows its stored card (history is not rewritten); its stored row was byte-for-byte unchanged.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • provider-frame-disposition.test.ts: the startup message with the reporter's real mcp_servers roster writes no row. Nested failure-looking values (task notification, Codex MCP startup, an uncatalogued type) never become error rows. Hook failures and a Claude sign-in status error still surface from their declared outcome fields.

  • Tests that pinned the old search (failed startup variants, failed delta-shaped messages, covered-type options) are removed or rewritten to the new rule.

  • Electron, macOS: reproduced on main and verified on this branch in a real Claude native chat, driven only through the DevTools protocol on a hidden window, with an isolated home. A new chat on the fix writes no startup card; the reopened old chat keeps its stored one.

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH / mixed versions: the host decides rows when it writes the journal, so an updated host stops writing these rows for any client version. No wire change.
  • Existing history: sessions that already stored a red startup card keep it in their history. New sessions don't write one.
  • All agents: the Grok/OpenCode path had no type table, so it only ever got red rows through the search. Those messages now stay ordinary fallback rows.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Local Electron QA: Claude auth_status error rendered as a red provider row

M4Air screenshot comparison

Hidden DevTools screenshots of the two built apps, using the same workspace, the same real Claude CLI, and the same prompt. One configured MCP server command was missing, and that server failed to start on both. Full access stayed on, and no MCP approval dialog appeared.

Current main 93095c2f67447474c1894e477f6695cdf5e98c16 This branch a9f7ed99ff132c44ea369c9376b554d6289910a9
main: red SessionStart hook row and red claude · message:system:init row, then the reply pong branch: red SessionStart hook row and the reply pong, with no system:init row

On current main the chat shows the real SessionStart hook failure and a red claude · message:system:init card. On this branch the hook failure and the reply pong remain, and the message:system:init card is absent. The auth_status image and the plugin-install note above are unchanged.

Earlier M4Air Electron QA

Built and compared earlier PR head 7ce7f9c0689fae9250fb8890db44c4a039fcd733 with current main 79b916395150eeee2954f03c30cc1744d95f6bcf in separate fresh profiles, using the same fixture repository, setup script, default tabs, failed MCP server, deliberately failing SessionStart hook, and signed-in real Claude CLI. Hidden whole-window Playwright screenshots show that both builds run setup, open Notes → Idle → Setup → Claude Chat with Claude active, and return pong; only main adds the false red claude · message:system:init row. The genuine hook error remains visible on both. Real agent configuration hashes were unchanged and the test processes were stopped.

Current main: real hook error and false startup row Final PR head: real hook error, no false startup row
Current main whole-window Claude chat showing a real hook error, false startup error, and pong reply Final PR head whole-window Claude chat showing the real hook error and pong reply without a false startup error
Setup and default-tab proof Current main Final PR head
Notes and tab order Current main whole-window Notes tab and default-tab order Final PR head whole-window Notes tab and default-tab order
Idle output Current main whole-window Idle terminal output Final PR head whole-window Idle terminal output

The two named real-Claude test files ran on that earlier head with the single no-hooks test excluded because the signed-in agent's real home was accessible. Ten tests passed; three assertions failed because they expect older hardcoded Claude model IDs (claude-haiku-4-5-20251001 and claude-sonnet-5) while the real CLI returned claude-haiku-5-5 and claude-sonnet-5-5. Those test files are unchanged by this PR and identical on the main revision used for QA; the full suites are not green.

Exact-head M4Air Electron QA after the main lint fix

Fresh builds compared PR head 14937ee770790baabb0191331f5795b93c47eac7 with main f8089c787b04b85b5d796e94c330858b5c2c0496 using separate profiles, the same fixture repository, setup and default-tab commands, a failed MCP server, a failing SessionStart hook, and real signed-in Claude. Both opened Notes → Idle → Setup → Claude Chat with Claude active and replied pong. Only main showed the false red claude · message:system:init row; both preserved the genuine hook error. Whole-window hidden-renderer screenshots were captured through Playwright CDP. All 50 recorded test processes exited, the QA ports are free, and real agent configuration hashes match their pretest values.

Current main: real hook error and false startup row Exact PR head: real hook error, no false startup row
Current main whole-window Claude chat with real hook error, false startup error, and pong reply Exact PR head whole-window Claude chat with real hook error and pong reply, without false startup error
Setup and default-tab proof Current main Exact PR head
Notes and tab order Current main whole-window Notes output and default-tab order Exact PR head whole-window Notes output and default-tab order
Idle output Current main whole-window Idle output Exact PR head whole-window Idle output

…g the payload

A Claude chat opened with a red "claude · message:system:init" card whenever one of the
user's MCP servers failed to connect. The classifier searched every nested field of a
frame for failure-looking values and let that outrank the per-kind catalogue, so the
session-start roster's `status: "failed"` turned the whole frame into an error row. The
same search caused the earlier failed-background-task opcode cards.

A frame is now a failure only because its kind is one, or because a field its kind
declares as its outcome says so (Claude `hook_response.outcome`, Codex `hook/completed`
`run.status`). A Claude turn result is a failure when the result journaling says so,
since that depends on whether the user stopped the turn. With nothing left to override,
the typed-translator coverage list, its option and the task-frame sentence seam go.

Failed MCP servers write no row for Claude or Codex.
@brennanb2025

brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

A failed MCP server made new Claude chats show a red claude · message:system:init card even though the chat worked. Orca treated a failed item in Claude's startup inventory as a failure of the whole message. This PR classifies the frame itself, so new chats stay clear of that false error while genuine failed hooks, sign-in errors, and failed turns still appear.

The two-round review found and fixed one regression introduced by the change: a Claude sign-in status error had lost its row. The final code reads that frame's declared error field, and its focused regression test passed. Deferred concerns are future uncatalogued frames with undeclared errors, malformed background-task frames without task identity, and a pre-existing Codex blocked/stopped hook visibility difference. Plugin-install progress remains quiet by design.

Exact-head QA passed on M4Air. Fresh hidden Electron builds compared PR head 14937ee770790baabb0191331f5795b93c47eac7 with main f8089c787b04b85b5d796e94c330858b5c2c0496, using fresh profiles, the same setup/default-tab fixture, a failed MCP server, a deliberately failing hook, and real signed-in Claude. Both opened Notes → Idle → Setup → Claude Chat with Claude active and returned pong. Only main rendered the false startup row; both preserved the genuine hook error. Six whole-window screenshots are in the PR body's Exact-head M4Air Electron QA after the main lint fix section. All 50 recorded test processes exited, ports are free, and real agent configuration hashes were unchanged.

Current-head CI passed. The full PR Checks workflow run 38980299454 passed on this exact commit, including lint, typecheck, the test shards, package jobs, and its aggregate PR checks passed; Mobile verify passed too. The PR is out of draft, has no unresolved review threads, and has a clean merge state. Earlier supplementary real-Claude suites had 10 passing tests, three failures on hardcoded older model names in unchanged tests, and one no-hooks test deliberately excluded to protect the signed-in home; those suites were not rerun on this head and are not claimed green.

For Brennan's review: Compare the main and PR chat screenshots, then the Notes and Idle screenshots in the PR body. The main judgment point is the intentional silence for MCP startup failures in frame classification; Claude sign-in errors and stopped-versus-failed turn handling show the preserved failure paths. No merge was performed.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 16, 2026 23:13
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f2890870-e1fa-403c-824a-c8bec26e8655
📥 Commits

Reviewing files that changed from the base of the PR and between f8089c7 and 14937ee.

📒 Files selected for processing: 13
  • src/main/claude/claude-api-retry-row.test.ts
  • src/main/claude/claude-background-task-frames.ts
  • src/main/claude/claude-result-journaling.ts
  • src/main/claude/claude-structured-journal-translation-background-tasks.test.ts
  • src/main/claude/claude-structured-journal-translation.ts
  • src/main/claude/claude-structured-provider-fallback.ts
  • src/main/codex/codex-structured-journal-generic-frames.ts
  • src/main/codex/codex-structured-journal-translation-streams.test.ts
  • src/main/codex/codex-structured-journal-translation.ts
  • src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts
  • src/main/native-chat/agent-session-wire/provider-frame-disposition.ts
  • src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts
  • src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts
💤 Files with no reviewable changes: 1
  • src/main/claude/claude-background-task-frames.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

📝 Walkthrough

Provider-frame classification no longer treats arbitrary nested error-like payload values as failures. Claude and Codex frames use frame kinds and declared outcome fields, while journal creation can also receive an explicit reportsFailure flag. Claude background-task fallback sentence generation and typed-translator metadata were removed from translation. Codex generic-frame appending now uses the helper’s default options.

Priority: ⬇️ Low

Severity of issue fixed: Low

Merge Risk: ⚪ Minimal · up to 14937

No code defect identified here prevents merging. The reported main sync and full workflow checks remain to be completed before release.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 12 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed No active directly linked issue remains. Linked issue #25514 is closed and provides historical context only. Therefore, this pull request has no active linked-issue coding requirements to fail.
Out of Scope Changes check Passed The changes remain within the pull request scope. They remove payload-wide failure promotion, add declared-outcome handling, pass Claude result failure decisions explicitly, and update Claude and Code…
Title check Passed The title clearly and concisely describes the main change: provider failures are classified by message kind instead of arbitrary payload contents.
Description check Passed The description is detailed and covers the user impact, implementation, rationale, linked issue reference, visual proof, testing, compatibility notes, and checklist. The required sections are present,…
Full details: Out of Scope Changes check

Explanation

The changes remain within the pull request scope. They remove payload-wide failure promotion, add declared-outcome handling, pass Claude result failure decisions explicitly, and update Claude and Codex tests and fallback paths. The Codex MCP-startup behavior and background-task fallback changes are stated objectives, not unrelated changes.

Full details: Description check

Explanation

The description is detailed and covers the user impact, implementation, rationale, linked issue reference, visual proof, testing, compatibility notes, and checklist. The required sections are present, although the linked issue is referenced as #25477 rather than provided as an explicit issue link.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brennanb2025
brennanb2025 merged commit fcfe319 into main Oct 16, 2026
59 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/provider-frame-errors-by-kind branch October 16, 2026 23:42
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.

1 participant