Skip to content

Improve agent failure UX: surface structured errors (quota, auth, rate limit) instead of raw stderr tails #982

Description

@gabrielvaz

Summary

When an agent fails, Open Design renders a raw agent exited with code N message followed by the last 400 bytes of stderr. For most non-trivial failures this dumps an unparsed stack trace into the chat that is useless to non-developer users (and not great even for developers — the actual diagnostic message is often not in the last 400 bytes).

Repro (real example, today)

Triggered the Gemini CLI agent and hit Google's per-model capacity limit. The UI showed:

agent exited with code 1 nc file:///Users/gabriel.vaz/.npm-packages/lib/node_modules/@google/gemini-cli/bundle/gemini-U6FSFXFH.js:10811:26 at async main (file:///Users/gabriel.vaz/.npm-packages/lib/node_modules/@google/gemini-cli/bundle/gemini-U6FSFXFH.js:15885:5) {
  cause: {
    code: 429,
    message: 'You have exhausted your capacity on this model.',
    details: [ [Object], [Object] ]
  },
  retryDelayMs: 10000
}

The signal in there — code: 429, 'You have exhausted your capacity on this model.', retryDelayMs: 10000 — is exactly what the user needs to see, but it's buried in a JS stack trace because Open Design just slices the tail of stderr.

Source

apps/web/src/providers/daemon.ts:327-333

if (endStatus === 'failed' || exitSignal || (exitCode !== null && exitCode !== 0)) {
  const tail = stderrBuf.trim().slice(-400);
  handlers.onError(
    new Error(`agent exited with ${exitSignal ? `signal ${exitSignal}` : `code ${exitCode}`}${tail ? `\n${tail}` : ''}`),
  );
  return;
}

There are two problems:

  1. The error is constructed as a single, lossy string. Anything structured the daemon already knows (which agent, which command, which model, what the exit code means for this CLI) is collapsed.
  2. The 400-char stderr tail is a heuristic that frequently truncates the actual error message and keeps only the trailing stack frames — which is exactly what we see in the example above.

The daemon already has clear classification for HTTP errors elsewhere (server.ts:3987 maps 429 → RATE_LIMITED, etc.), but this same intelligence is not applied to agent CLI exits.

Requested change

1) Parse and classify common failure modes

Before falling back to the generic message, run stderr through a small set of per-agent matchers and surface a typed error to the UI:

Pattern (any agent) UI category Example user-facing message
code: 429, RESOURCE_EXHAUSTED, rate limit, capacity on this model quota_exhausted "Gemini hit a per-model capacity limit. Try again in ~10s, switch models, or use your own API key."
401, 403, Not logged in, auth, Please run /login auth_required "Claude Code needs to log in. Run /login in a terminal."
ENAMETOOLONG, prompt exceeds the safe size prompt_too_large "The composed prompt is too large for this CLI's argv limit."
Unknown arguments: ... cli_version_mismatch "Your installed Gemini CLI is too old (or too new) for the flags Open Design passes. See #978."
ENOENT, spawn ... ENOENT binary_not_found "Could not find claude on PATH."
Any other non-zero exit agent_crashed Show the full stderr in a collapsible panel, not a 400-char tail.

These categories are stable contracts the UI can branch on (icons, retry buttons, links to docs).

2) Don't truncate the diagnostic

Keep the full stderr buffer per run (it is already accumulated as stderrBuf). When showing it in chat, render it inside a collapsible "Show details" disclosure — short message on top, full output one click away. The 400-byte slice is hostile to debugging.

3) Honor retryDelayMs when present

Gemini CLI emits retryDelayMs for transient failures. When the classifier detects a retryable error (429, 500, 503), Open Design could:

  • show a "Retry in 10s" countdown button in the failed message, and/or
  • automatically retry once with backoff for transient categories (with a setting toggle).

4) Cross-link other failure-mode issues

Several existing bugs are symptoms of the same root cause (no structured error reporting):

A single error-classifier surface would let each of these become a one-click "fix this" UI hint instead of a raw trace.

Out of scope (but worth considering)

  • A debug button that opens the full run log file (the daemon already writes one) so users don't have to copy-paste the stack trace into a bug report.
  • An "Open in terminal" button that re-runs the same agent invocation in the user's shell so they can debug live.

Acceptance criteria

  • A 429 from Gemini renders as a one-line user-facing message ("capacity exhausted, try again in 10s") with the full stack trace tucked under a disclosure.
  • A missing-binary failure renders as "Could not find X — install instructions" instead of spawn ENOENT.
  • The chat history no longer shows truncated mid-stack-frame strings.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions