Error Recovery Strategy
When something fails, the default human reaction is "retry." That is often the wrong
default. Retrying a permission-denied file write burns the same error three times in a row.
Retrying a network timeout that won't resolve in 30 seconds burns three minutes.
This Skill codifies the decision: categorize the error first, then pick one of five
recovery actions, then commit to it explicitly.
When to use
Activate when any of these is true:
- A tool call returns a non-success result (non-zero exit, HTTP 4xx/5xx, exception,
error message).
- A sub-agent reports
status: closed-failed in the family file.
- An exception escapes from any of your own code or a library you called.
- A timeout fires on a long-running operation.
- A "weird" result comes back that might be a partial success (e.g. command exited 0
but produced no output where you expected output).
When NOT to use
- The operation succeeded. Do not second-guess success.
- The error is in user input (bad prompt, missing file the user should provide). That is
not a recovery case; it is a clarification case.
- The error is part of expected flow (e.g. a
grep returning 0 matches is an exit-1, but
it is not a failure for the search use case).
Process
Stop. Do not retry yet. Even if the obvious answer is "retry," run this Skill.
Categorize the error into one of four buckets:
| Bucket |
Signals |
Examples |
| transient |
Will probably succeed if tried again soon |
Network timeout, HTTP 429/503, "ECONNRESET", "temporarily unavailable", rate limit |
| deterministic |
Will fail every time the same way |
Permission denied, file not found, "command not found", invalid syntax, wrong credentials |
| stale |
Was true once, not anymore |
"Index out of date", "stale lock", "file modified since read" |
| unknown |
Cannot classify from the message alone |
Generic exception, empty error, "something went wrong" |
Pick one of five recovery actions based on the bucket:
| Bucket |
Default action |
When to deviate |
| transient |
retry-with-backoff (see next skill) |
Never skip the backoff. If retries exceed the budget, escalate to ask-user. |
| deterministic |
switch (try an alternative tool) or fallback (degrade gracefully) |
If no alternative exists, ask-user. |
| stale |
refresh-then-retry (re-read state, then retry once) |
If still failing after one refresh, escalate to ask-user. |
| unknown |
ask-user (or skip-with-warning if user signal allowed it) |
Never silently retry an unknown error. |
Execute the chosen action explicitly — do not bundle it with other tool calls.
The user must be able to see "I retried" / "I switched to X" / "I asked you" as a discrete
step in the response.
Record the decision in the response so the user can audit:
Recovery decision: <action>
Reason: <one-line reason, citing the bucket>
After the action, re-evaluate. If the action also failed, re-categorize (the bucket
may have changed — e.g. a "transient" error that persists for 5 retries is now a
"deterministic" error in practice) and pick a new action.
Output contract
The user sees, in this order:
- One-line error summary (what went wrong, with the tool / file / op named).
- Bucket classification:
transient / deterministic / stale / unknown.
- Chosen action:
retry / switch / fallback / refresh-then-retry / ask-user / skip.
- One-line reason citing the bucket.
- (If retry) the backoff plan: "N attempts, M seconds between, exponential with jitter."
- (If ask) the user-facing question with concrete options.
- (If fallback / skip) what degraded behaviour the user is now getting.
Example
> bash(command="cat /etc/hosts")
Error: cat: /etc/hosts: Permission denied
Recovery decision: switch
Reason: bucket=deterministic (permission denied won't change on retry);
switching to `read` with explicit `path=/etc/hosts` (read may have different
permission semantics than shell cat) before asking the user.
> bash(command="curl https://internal-api.example.com/health")
Error: connect ECONNREFUSED 10.0.0.5:443
Recovery decision: retry
Reason: bucket=transient (network refused); will retry 3 times with 2s/4s/8s backoff
and 500ms jitter; if all fail, escalate to ask-user.
> task(agent_name="explore", prompt="...")
Status: closed-failed. Sub-agent error: "context window exceeded".
Recovery decision: ask-user
Reason: bucket=unknown (sub-agent did not return a clear error class); do not silently
retry with a smaller brief; surface to user with options:
(a) reduce the sub-task scope
(b) switch to a model with larger context
(c) skip this sub-task
Common pitfalls
- Do not default to retry. Retry is only correct for
transient (and a few stale).
For deterministic and unknown, retry is the most expensive wrong answer.
- Do not bundle the recovery with other tool calls. A retry hidden inside a larger
batch of work is invisible. Always surface the recovery as a discrete step.
- Do not re-categorize silently. If you categorize as
transient, retry 3 times,
and it still fails, the bucket is now deterministic or unknown — say so out loud.
- Do not ask the user a vague question. "What should I do?" is not an option. Give
the user 2-4 concrete options based on the bucket.
- Do not skip-with-warning without permission. The user did not pre-authorize
silent skips. If the work is optional, the user should have said so at the start.
- Do not blame the tool. The tool did what it was told. Categorize the error
honestly, not defensively.
- Do not loop on retry forever. Always have a max-attempt budget; on exhaustion,
escalate to
ask-user.
Verification checklist
1---2name: error-recovery-strategy3description: Classify error into 4 buckets (transient / deterministic / stale / unknown) and pick one of 5 actions (retry / switch / fallback / refresh-then-retry / ask-user / skip). USE WHEN: tool returns non-success, sub-agent `status: closed-failed`, exception escapes, timeout fires, weird partial-success result, ECONNREFUSED / 5xx / 429 / timeout / permission denied / "command not found" / "fail" / "error" / "出错了" / "挂" / "失败". TRIGGER PHRASES: "出错了", "failed", "挂", "error", "失败", "fail", "permission denied", "command not found", "ECONNREFUSED", "timeout", "挂了", "再试一次", "retry", "这不行", "没用", "fallback", "退路", "不行", "跑不通", "broken". SKIP WHEN: operation succeeded, error is in user input (clarification case), error is part of expected flow (grep 0 matches).4license: Apache-2.05---67# Error Recovery Strategy89When something fails, the default human reaction is "retry." That is often the **wrong**10default. Retrying a permission-denied file write burns the same error three times in a row.11Retrying a network timeout that won't resolve in 30 seconds burns three minutes.1213This Skill codifies the decision: **categorize the error first, then pick one of five14recovery actions, then commit to it explicitly.**1516## When to use1718Activate when **any** of these is true:1920- A tool call returns a non-success result (non-zero exit, HTTP 4xx/5xx, exception,21 error message).22- A sub-agent reports `status: closed-failed` in the family file.23- An exception escapes from any of your own code or a library you called.24- A timeout fires on a long-running operation.25- A "weird" result comes back that might be a partial success (e.g. command exited 026 but produced no output where you expected output).2728## When NOT to use2930- The operation succeeded. Do not second-guess success.31- The error is in user input (bad prompt, missing file the user should provide). That is32 not a recovery case; it is a clarification case.33- The error is part of expected flow (e.g. a `grep` returning 0 matches is an exit-1, but34 it is not a failure for the search use case).3536## Process37381. **Stop. Do not retry yet.** Even if the obvious answer is "retry," run this Skill.392. **Categorize the error** into one of four buckets:4041 | Bucket | Signals | Examples |42 |---|---|---|43 | **transient** | Will probably succeed if tried again soon | Network timeout, HTTP 429/503, "ECONNRESET", "temporarily unavailable", rate limit |44 | **deterministic** | Will fail every time the same way | Permission denied, file not found, "command not found", invalid syntax, wrong credentials |45 | **stale** | Was true once, not anymore | "Index out of date", "stale lock", "file modified since read" |46 | **unknown** | Cannot classify from the message alone | Generic exception, empty error, "something went wrong" |47483. **Pick one of five recovery actions** based on the bucket:4950 | Bucket | Default action | When to deviate |51 |---|---|---|52 | **transient** | `retry-with-backoff` (see next skill) | Never skip the backoff. If retries exceed the budget, escalate to `ask-user`. |53 | **deterministic** | `switch` (try an alternative tool) or `fallback` (degrade gracefully) | If no alternative exists, `ask-user`. |54 | **stale** | `refresh-then-retry` (re-read state, then retry once) | If still failing after one refresh, escalate to `ask-user`. |55 | **unknown** | `ask-user` (or `skip-with-warning` if user signal allowed it) | Never silently retry an unknown error. |56574. **Execute the chosen action explicitly** — do not bundle it with other tool calls.58 The user must be able to see "I retried" / "I switched to X" / "I asked you" as a discrete59 step in the response.60615. **Record the decision in the response** so the user can audit:6263 ```text64 Recovery decision: <action>65 Reason: <one-line reason, citing the bucket>66 ```67686. **After the action**, re-evaluate. If the action also failed, re-categorize (the bucket69 may have changed — e.g. a "transient" error that persists for 5 retries is now a70 "deterministic" error in practice) and pick a new action.7172## Output contract7374The user sees, in this order:7576- One-line error summary (what went wrong, with the tool / file / op named).77- Bucket classification: `transient` / `deterministic` / `stale` / `unknown`.78- Chosen action: `retry` / `switch` / `fallback` / `refresh-then-retry` / `ask-user` / `skip`.79- One-line reason citing the bucket.80- (If retry) the backoff plan: "N attempts, M seconds between, exponential with jitter."81- (If ask) the user-facing question with concrete options.82- (If fallback / skip) what degraded behaviour the user is now getting.8384## Example8586```text87> bash(command="cat /etc/hosts")8889Error: cat: /etc/hosts: Permission denied90Recovery decision: switch91Reason: bucket=deterministic (permission denied won't change on retry);92 switching to `read` with explicit `path=/etc/hosts` (read may have different93 permission semantics than shell cat) before asking the user.94```9596```text97> bash(command="curl https://internal-api.example.com/health")9899Error: connect ECONNREFUSED 10.0.0.5:443100Recovery decision: retry101Reason: bucket=transient (network refused); will retry 3 times with 2s/4s/8s backoff102 and 500ms jitter; if all fail, escalate to ask-user.103```104105```text106> task(agent_name="explore", prompt="...")107108Status: closed-failed. Sub-agent error: "context window exceeded".109Recovery decision: ask-user110Reason: bucket=unknown (sub-agent did not return a clear error class); do not silently111 retry with a smaller brief; surface to user with options:112 (a) reduce the sub-task scope113 (b) switch to a model with larger context114 (c) skip this sub-task115```116117## Common pitfalls118119- **Do not default to retry.** Retry is only correct for `transient` (and a few `stale`).120 For `deterministic` and `unknown`, retry is the most expensive wrong answer.121- **Do not bundle the recovery with other tool calls.** A retry hidden inside a larger122 batch of work is invisible. Always surface the recovery as a discrete step.123- **Do not re-categorize silently.** If you categorize as `transient`, retry 3 times,124 and it still fails, the bucket is now `deterministic` or `unknown` — say so out loud.125- **Do not ask the user a vague question.** "What should I do?" is not an option. Give126 the user 2-4 concrete options based on the bucket.127- **Do not skip-with-warning without permission.** The user did not pre-authorize128 silent skips. If the work is optional, the user should have said so at the start.129- **Do not blame the tool.** The tool did what it was told. Categorize the error130 honestly, not defensively.131- **Do not loop on retry forever.** Always have a max-attempt budget; on exhaustion,132 escalate to `ask-user`.133134## Verification checklist135136- [ ] Did you categorize the error into one of four buckets before picking an action?137- [ ] Did you pick one of five actions based on the bucket (not the default)?138- [ ] Did you state the recovery decision in the response, with the bucket and reason?139- [ ] (Retry) Did you specify the backoff plan (attempts, intervals, jitter)?140- [ ] (Ask) Did you give 2-4 concrete options, not "what should I do?"141- [ ] (Switch / Fallback) Did you name the alternative tool / the degraded behaviour?142- [ ] (Skip) Did you confirm the user pre-authorized this work as optional?143- [ ] Did you re-evaluate after the action and re-categorize if it failed?144- [ ] Is the recovery step a discrete line in the response (not bundled)?