Why
Part of the reliability epic #3408. The daemon now emits a canonical failure classification on run_finished (failure_category / failure_stage / retryable / user_action, via run-failure-classification.ts from #4431), and §3 safe-retry already consumes retryable. But the web error card still maps its CTA off the raw error_code (apps/web/src/runtime/amr-guidance.ts resolveRunFailureUi()), so the canonical user_action the daemon already decided is ignored. New classes (prompt_too_large -> reduce_context, model_unavailable -> switch_model, rate_limit -> retry-vs-switch, upstream_unavailable -> retry) do not drive a matching CTA. This is the last open proposal piece (§5) under #3408; §3 is confirmed done.
What users will see
Failed-run error cards show a CTA that matches the actual failure reason — e.g. "reduce context" for an over-large prompt, "switch model" for an unavailable model, "recharge" for AMR balance — instead of a generic retry/authorize derived from a coarse error code.
Proposal
resolveRunFailureUi() prefers the structured user_action from run_finished when present and maps it to the CTA at one choke point (login / recharge / retry / switch_model / reduce_context / install_cli / none), reusing the existing AMR promote path. CTA is driven by user_action, not re-derived from failure_category (trust the daemon decision; avoid the two layers diverging). failure_category is used for display only.
- Fallback chain: when
user_action is absent or none, fall through to the existing error_code map so older/unclassified runs — and partially-classified runs (category present, action unresolved) — still render a real CTA rather than a blank one.
- Tests: resolver covered against every
failure_category / user_action (mirroring the daemon classifier test as fixture source-of-truth); a classified run beats the raw-code map; an unclassified run still resolves via fallback; and the partial-classification edge (category set, user_action = none / absent -> reaches the error_code map, no blank CTA).
Scope is UI-side only; daemon classification and retry layers are untouched.
Relates to #3408.
Why
Part of the reliability epic #3408. The daemon now emits a canonical failure classification on
run_finished(failure_category/failure_stage/retryable/user_action, viarun-failure-classification.tsfrom #4431), and §3 safe-retry already consumesretryable. But the web error card still maps its CTA off the rawerror_code(apps/web/src/runtime/amr-guidance.tsresolveRunFailureUi()), so the canonicaluser_actionthe daemon already decided is ignored. New classes (prompt_too_large -> reduce_context,model_unavailable -> switch_model,rate_limit -> retry-vs-switch,upstream_unavailable -> retry) do not drive a matching CTA. This is the last open proposal piece (§5) under #3408; §3 is confirmed done.What users will see
Failed-run error cards show a CTA that matches the actual failure reason — e.g. "reduce context" for an over-large prompt, "switch model" for an unavailable model, "recharge" for AMR balance — instead of a generic retry/authorize derived from a coarse error code.
Proposal
resolveRunFailureUi()prefers the structureduser_actionfromrun_finishedwhen present and maps it to the CTA at one choke point (login/recharge/retry/switch_model/reduce_context/install_cli/none), reusing the existing AMR promote path. CTA is driven byuser_action, not re-derived fromfailure_category(trust the daemon decision; avoid the two layers diverging).failure_categoryis used for display only.user_actionis absent ornone, fall through to the existingerror_codemap so older/unclassified runs — and partially-classified runs (category present, action unresolved) — still render a real CTA rather than a blank one.failure_category/user_action(mirroring the daemon classifier test as fixture source-of-truth); a classified run beats the raw-code map; an unclassified run still resolves via fallback; and the partial-classification edge (category set,user_action=none/ absent -> reaches theerror_codemap, no blank CTA).Scope is UI-side only; daemon classification and retry layers are untouched.
Relates to #3408.