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:
- 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.
- 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.
Summary
When an agent fails, Open Design renders a raw
agent exited with code Nmessage 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:
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-333There are two problems:
The daemon already has clear classification for HTTP errors elsewhere (
server.ts:3987maps 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:
code: 429,RESOURCE_EXHAUSTED,rate limit,capacity on this modelquota_exhausted401,403,Not logged in,auth,Please run /loginauth_required/loginin a terminal."ENAMETOOLONG,prompt exceeds the safe sizeprompt_too_largeUnknown arguments: ...cli_version_mismatchENOENT,spawn ... ENOENTbinary_not_foundclaudeon PATH."agent_crashedThese 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
retryDelayMswhen presentGemini CLI emits
retryDelayMsfor transient failures. When the classifier detects a retryable error (429,500,503), Open Design could:4) Cross-link other failure-mode issues
Several existing bugs are symptoms of the same root cause (no structured error reporting):
Unknown arguments: output-format, outputFormat#978 —Unknown arguments: output-format, outputFormat(Gemini CLI version mismatch). Could be auto-classified.Not logged in / Please run /login.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)
Acceptance criteria
spawn ENOENT.