@crewhaus/recovery-engine 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,10 @@
5
5
  * the chosen action (sleeping, recompacting, injecting messages).
6
6
  *
7
7
  * Taxonomy:
8
+ * billing (402 / credit balance / insufficient_quota)
9
+ * → halt (terminal — retrying an empty account is futile)
10
+ * auth (401/403) → halt (terminal — retrying bad credentials is futile)
11
+ * rate_limit (429) → retry (honors Retry-After; exhaustion → halt, exit 32)
8
12
  * prompt_too_long → compact (max once per turn → fail)
9
13
  * max_output_tokens → continue (max 3 per turn → fail)
10
14
  * overloaded / 5xx → retry (exponential backoff, max 5 → fail)
@@ -23,7 +27,7 @@
23
27
  * References: claude-code/query.ts recovery branches; agent-framework
24
28
  * _runner.py retry; AI-Harness-Systems §recovery.
25
29
  */
26
- import { CrewhausError } from "@crewhaus/errors";
30
+ import { CrewhausError, type FailureClass, type FailureReport } from "@crewhaus/errors";
27
31
  export type RecoveryAction = {
28
32
  readonly kind: "compact";
29
33
  } | {
@@ -40,8 +44,11 @@ export type RecoveryAction = {
40
44
  } | {
41
45
  readonly kind: "fail";
42
46
  readonly reason: string;
47
+ } | {
48
+ readonly kind: "halt";
49
+ readonly report: FailureReport;
43
50
  };
44
- export type RecoveryErrorClass = "prompt_too_long" | "max_output_tokens" | "overloaded_or_5xx" | "invalid_request" | "user_aborted" | "unknown";
51
+ export type RecoveryErrorClass = "billing" | "auth" | "rate_limit" | "prompt_too_long" | "max_output_tokens" | "overloaded_or_5xx" | "invalid_request" | "user_aborted" | "unknown";
45
52
  export type RecoveryState = {
46
53
  /** Number of `retry` actions already chosen in this turn. */
47
54
  readonly retryCount: number;
@@ -64,6 +71,17 @@ export declare class RecoveryEngineError extends CrewhausError {
64
71
  * 4 s, ..., capped at 30 s, plus 0–250 ms of jitter.
65
72
  */
66
73
  export declare function backoffMs(attempt: number, jitterFn?: () => number): number;
74
+ /**
75
+ * v0.3.0 Goal 6 — extract a provider Retry-After delay from an error object,
76
+ * in milliseconds, or undefined when absent. Duck-typed against the shapes
77
+ * the SDKs / adapters surface:
78
+ * · `retryAfterMs` (number, milliseconds) — adapter-normalized field;
79
+ * · `retryAfter` (number, seconds) — SDK convenience field;
80
+ * · `headers` — a Headers instance or plain object carrying `retry-after`
81
+ * as delta-seconds or an HTTP-date.
82
+ * Results are clamped into [0, RETRY_AFTER_CAP_MS].
83
+ */
84
+ export declare function retryAfterMs(error: unknown, now?: () => number): number | undefined;
67
85
  /**
68
86
  * Classify an unknown error value into a recovery taxonomy bucket. Duck-typed
69
87
  * against the Anthropic SDK error shape: `.status`, `.error.type`, `.message`,
@@ -74,6 +92,60 @@ export declare function backoffMs(attempt: number, jitterFn?: () => number): num
74
92
  * that shape after seeing `stop_reason: "max_tokens"`).
75
93
  */
76
94
  export declare function classify(error: unknown): RecoveryErrorClass;
95
+ /**
96
+ * v0.3.0 Goal 6 — built-in named failure classes. Report metadata (title /
97
+ * remediation / exit code) for the terminal classes the engine can halt
98
+ * with. Consulted AFTER user `failure_taxonomy` entries (user overrides
99
+ * win) and BEFORE the generic classify buckets. `mcp_boot_failure` and
100
+ * `crewhaus_budget` are not produced by `classify()` — they exist so the
101
+ * runtime's MCP-boot and budget-cap paths build their reports from the
102
+ * same table (wired in the follow-up runtime PR).
103
+ */
104
+ export type BuiltinFailureClass = {
105
+ readonly class: FailureClass;
106
+ readonly title: string;
107
+ readonly remediation: string;
108
+ readonly exitCode: number;
109
+ readonly docsUrl?: string;
110
+ };
111
+ export declare const BUILTIN_FAILURE_CLASSES: {
112
+ readonly billing_exhausted: {
113
+ readonly class: "billing";
114
+ readonly title: "provider account out of funding";
115
+ readonly remediation: "add credits to your provider account, then rerun.";
116
+ readonly exitCode: 31;
117
+ };
118
+ readonly auth_invalid: {
119
+ readonly class: "auth";
120
+ readonly title: "provider rejected the credentials";
121
+ readonly remediation: "check the provider API key env var (see .env.example), then rerun.";
122
+ readonly exitCode: 30;
123
+ };
124
+ readonly rate_limited: {
125
+ readonly class: "rate_limit";
126
+ readonly title: "provider rate limit still exceeded after retries";
127
+ readonly remediation: "wait for the rate-limit window to reset (or lower the request rate), then rerun.";
128
+ readonly exitCode: 32;
129
+ };
130
+ readonly mcp_boot_failure: {
131
+ readonly class: "mcp_boot";
132
+ readonly title: "an MCP server failed to boot";
133
+ readonly remediation: "check the MCP server command, URL, and credentials in the spec, then rerun.";
134
+ readonly exitCode: 40;
135
+ };
136
+ readonly crewhaus_budget: {
137
+ readonly class: "crewhaus_budget";
138
+ readonly title: "run stopped by the configured budget cap";
139
+ readonly remediation: "raise the spec's budget cap (or rerun with a higher cap).";
140
+ readonly exitCode: 33;
141
+ };
142
+ };
143
+ /**
144
+ * Build a `FailureReport` for a built-in class from the offending error.
145
+ * AdapterError wrappers carry `providerId`, which drives the "…said:"
146
+ * attribution in `detail` and the provider-specific remediation line.
147
+ */
148
+ export declare function buildFailureReport(key: keyof typeof BUILTIN_FAILURE_CLASSES, error: unknown): FailureReport;
77
149
  /**
78
150
  * Section 55 (Track A) — a single entry from a spec's `failure_taxonomy`,
79
151
  * carried verbatim from the IR. The recovery engine consults the taxonomy
@@ -117,4 +189,5 @@ export declare const BUDGETS: {
117
189
  readonly BASE_BACKOFF_MS: 1000;
118
190
  readonly MAX_BACKOFF_MS: 30000;
119
191
  readonly MAX_JITTER_MS: 250;
192
+ readonly RETRY_AFTER_CAP_MS: 60000;
120
193
  };
package/dist/index.js CHANGED
@@ -5,6 +5,10 @@
5
5
  * the chosen action (sleeping, recompacting, injecting messages).
6
6
  *
7
7
  * Taxonomy:
8
+ * billing (402 / credit balance / insufficient_quota)
9
+ * → halt (terminal — retrying an empty account is futile)
10
+ * auth (401/403) → halt (terminal — retrying bad credentials is futile)
11
+ * rate_limit (429) → retry (honors Retry-After; exhaustion → halt, exit 32)
8
12
  * prompt_too_long → compact (max once per turn → fail)
9
13
  * max_output_tokens → continue (max 3 per turn → fail)
10
14
  * overloaded / 5xx → retry (exponential backoff, max 5 → fail)
@@ -23,7 +27,7 @@
23
27
  * References: claude-code/query.ts recovery branches; agent-framework
24
28
  * _runner.py retry; AI-Harness-Systems §recovery.
25
29
  */
26
- import { CrewhausError } from "@crewhaus/errors";
30
+ import { CrewhausError, EXIT_CODES, isRunFailedError, } from "@crewhaus/errors";
27
31
  export const initialRecoveryState = {
28
32
  retryCount: 0,
29
33
  compactCount: 0,
@@ -59,6 +63,51 @@ export function backoffMs(attempt, jitterFn = Math.random) {
59
63
  const jitter = Math.min(Math.floor(jitterFn() * (MAX_JITTER_MS + 1)), MAX_JITTER_MS);
60
64
  return exp + jitter;
61
65
  }
66
+ // Honor a provider Retry-After up to this cap so a pathological header
67
+ // (e.g. an HTTP-date hours away) can't stall the run indefinitely; past the
68
+ // cap the normal capped exponential backoff applies.
69
+ const RETRY_AFTER_CAP_MS = 60_000;
70
+ /**
71
+ * v0.3.0 Goal 6 — extract a provider Retry-After delay from an error object,
72
+ * in milliseconds, or undefined when absent. Duck-typed against the shapes
73
+ * the SDKs / adapters surface:
74
+ * · `retryAfterMs` (number, milliseconds) — adapter-normalized field;
75
+ * · `retryAfter` (number, seconds) — SDK convenience field;
76
+ * · `headers` — a Headers instance or plain object carrying `retry-after`
77
+ * as delta-seconds or an HTTP-date.
78
+ * Results are clamped into [0, RETRY_AFTER_CAP_MS].
79
+ */
80
+ export function retryAfterMs(error, now = Date.now) {
81
+ if (error === null || typeof error !== "object")
82
+ return undefined;
83
+ const errObj = error;
84
+ const clamp = (ms) => Number.isFinite(ms) ? Math.min(Math.max(Math.round(ms), 0), RETRY_AFTER_CAP_MS) : undefined;
85
+ if (typeof errObj.retryAfterMs === "number")
86
+ return clamp(errObj.retryAfterMs);
87
+ if (typeof errObj.retryAfter === "number")
88
+ return clamp(errObj.retryAfter * 1_000);
89
+ const headers = errObj.headers;
90
+ let raw;
91
+ if (headers !== null && typeof headers === "object") {
92
+ const maybeGet = headers.get;
93
+ if (typeof maybeGet === "function") {
94
+ raw = maybeGet.call(headers, "retry-after");
95
+ }
96
+ else {
97
+ const record = headers;
98
+ raw = record["retry-after"] ?? record["Retry-After"];
99
+ }
100
+ }
101
+ if (typeof raw !== "string" || raw.length === 0)
102
+ return undefined;
103
+ // Delta-seconds form ("30") first, then the HTTP-date form.
104
+ if (/^\d+$/.test(raw.trim()))
105
+ return clamp(Number(raw.trim()) * 1_000);
106
+ const at = Date.parse(raw);
107
+ if (Number.isNaN(at))
108
+ return undefined;
109
+ return clamp(at - now());
110
+ }
62
111
  /**
63
112
  * Classify an unknown error value into a recovery taxonomy bucket. Duck-typed
64
113
  * against the Anthropic SDK error shape: `.status`, `.error.type`, `.message`,
@@ -79,8 +128,44 @@ export function classify(error) {
79
128
  }
80
129
  }
81
130
  const innerType = typeof errObj.error?.type === "string" ? errObj.error.type : undefined;
131
+ const innerMessage = typeof errObj.error?.message === "string" ? errObj.error.message : "";
82
132
  const message = typeof errObj.message === "string" ? errObj.message : "";
83
133
  const status = typeof errObj.status === "number" ? errObj.status : undefined;
134
+ // BOTH code slots are consulted independently (PR 2): a raw SDK error
135
+ // carries the provider code top-level, but adapter WRAPPERS keep
136
+ // CrewhausError's ErrorCode there ("adapter") and surface the provider
137
+ // code on the copied error envelope — a top-level string must not shadow
138
+ // the envelope's discriminator.
139
+ const code = typeof errObj.code === "string" ? errObj.code : undefined;
140
+ const innerCode = typeof errObj.error?.code === "string" ? errObj.error.code : undefined;
141
+ // v0.3.0 Goal 6 — billing / auth / rate_limit come BEFORE the pre-existing
142
+ // buckets so an out-of-funds 400 is not misrouted through a tombstone and
143
+ // an insufficient_quota 429 is not burned through backoff retries.
144
+ //
145
+ // billing — the provider account itself is out of funding:
146
+ // · HTTP 402 anywhere;
147
+ // · Anthropic: 400 invalid_request_error + "credit balance … too low";
148
+ // · OpenAI: 429 + code "insufficient_quota" (distinct from rate limits);
149
+ // · Bedrock: ServiceQuotaExceededException by name (a hard account
150
+ // quota — retrying won't help; raise the quota instead).
151
+ if (status === 402)
152
+ return "billing";
153
+ if (code === "insufficient_quota" || innerCode === "insufficient_quota")
154
+ return "billing";
155
+ if (status === 400 && /credit balance/i.test(innerMessage.length > 0 ? innerMessage : message)) {
156
+ return "billing";
157
+ }
158
+ if (typeof errObj.name === "string" && /ServiceQuotaExceeded/.test(errObj.name)) {
159
+ return "billing";
160
+ }
161
+ // auth — runtime 401/403 (distinct from boot-time ProviderAuthError, which
162
+ // fires before any request when credentials are entirely missing).
163
+ if (status === 401 || status === 403)
164
+ return "auth";
165
+ // rate_limit — a genuine 429 (NOT insufficient_quota, handled above).
166
+ // Retried with Retry-After honored; budget exhaustion halts with exit 32.
167
+ if (status === 429 || innerType === "rate_limit_error")
168
+ return "rate_limit";
84
169
  if (innerType === "max_output_tokens")
85
170
  return "max_output_tokens";
86
171
  // Anthropic returns "Prompt is too long" inside invalid_request_error responses;
@@ -98,6 +183,100 @@ export function classify(error) {
98
183
  return "invalid_request";
99
184
  return "unknown";
100
185
  }
186
+ export const BUILTIN_FAILURE_CLASSES = {
187
+ billing_exhausted: {
188
+ class: "billing",
189
+ title: "provider account out of funding",
190
+ remediation: "add credits to your provider account, then rerun.",
191
+ exitCode: EXIT_CODES.billing,
192
+ },
193
+ auth_invalid: {
194
+ class: "auth",
195
+ title: "provider rejected the credentials",
196
+ remediation: "check the provider API key env var (see .env.example), then rerun.",
197
+ exitCode: EXIT_CODES.auth,
198
+ },
199
+ rate_limited: {
200
+ class: "rate_limit",
201
+ title: "provider rate limit still exceeded after retries",
202
+ remediation: "wait for the rate-limit window to reset (or lower the request rate), then rerun.",
203
+ exitCode: EXIT_CODES.rate_limit,
204
+ },
205
+ mcp_boot_failure: {
206
+ class: "mcp_boot",
207
+ title: "an MCP server failed to boot",
208
+ remediation: "check the MCP server command, URL, and credentials in the spec, then rerun.",
209
+ exitCode: EXIT_CODES.tool,
210
+ },
211
+ crewhaus_budget: {
212
+ class: "crewhaus_budget",
213
+ title: "run stopped by the configured budget cap",
214
+ remediation: "raise the spec's budget cap (or rerun with a higher cap).",
215
+ exitCode: EXIT_CODES.crewhaus_budget,
216
+ },
217
+ };
218
+ /** Provider display names for `detail` attribution ("Anthropic said: …"). */
219
+ const PROVIDER_DISPLAY_NAMES = {
220
+ anthropic: "Anthropic",
221
+ openai: "OpenAI",
222
+ gemini: "Gemini",
223
+ bedrock: "Bedrock",
224
+ };
225
+ /** Per-provider remediation overrides for the billing / auth classes. */
226
+ const PROVIDER_REMEDIATIONS = {
227
+ anthropic: {
228
+ billing: "add credits at https://console.anthropic.com/settings/billing, then rerun.",
229
+ auth: "check ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY (see .env.example), then rerun.",
230
+ },
231
+ openai: {
232
+ billing: "add credits at https://platform.openai.com/settings/organization/billing, then rerun.",
233
+ auth: "check OPENAI_API_KEY (see .env.example), then rerun.",
234
+ },
235
+ gemini: {
236
+ billing: "check your plan and billing at https://aistudio.google.com/, then rerun.",
237
+ auth: "check GEMINI_API_KEY / GOOGLE_API_KEY (see .env.example), then rerun.",
238
+ },
239
+ bedrock: {
240
+ billing: "request a service-quota increase in the AWS console, then rerun.",
241
+ auth: "check your AWS credentials and Bedrock model access, then rerun.",
242
+ },
243
+ };
244
+ /** Best raw provider text: the API envelope message, else the wrapper message. */
245
+ function rawProviderText(error) {
246
+ const errObj = error;
247
+ const inner = typeof errObj?.error?.message === "string" ? errObj.error.message : "";
248
+ if (inner.length > 0)
249
+ return inner;
250
+ const outer = typeof errObj?.message === "string" ? errObj.message : "";
251
+ if (outer.length > 0)
252
+ return outer;
253
+ return String(error);
254
+ }
255
+ /**
256
+ * Build a `FailureReport` for a built-in class from the offending error.
257
+ * AdapterError wrappers carry `providerId`, which drives the "…said:"
258
+ * attribution in `detail` and the provider-specific remediation line.
259
+ */
260
+ export function buildFailureReport(key, error) {
261
+ const builtin = BUILTIN_FAILURE_CLASSES[key];
262
+ const providerId = typeof error?.providerId === "string"
263
+ ? error.providerId
264
+ : undefined;
265
+ const raw = rawProviderText(error);
266
+ const detail = providerId !== undefined
267
+ ? `${PROVIDER_DISPLAY_NAMES[providerId] ?? providerId} said: ${JSON.stringify(raw)}`
268
+ : raw;
269
+ const remediation = (providerId !== undefined ? PROVIDER_REMEDIATIONS[providerId]?.[builtin.class] : undefined) ??
270
+ builtin.remediation;
271
+ return {
272
+ class: builtin.class,
273
+ title: builtin.title,
274
+ detail,
275
+ remediation,
276
+ exitCode: builtin.exitCode,
277
+ ...(builtin.docsUrl !== undefined ? { docsUrl: builtin.docsUrl } : {}),
278
+ };
279
+ }
101
280
  /**
102
281
  * Match an unknown error against a taxonomy entry's `pattern`. The pattern
103
282
  * is either a `/regex/flags` literal or a case-insensitive substring of
@@ -142,16 +321,44 @@ export function matchNamedFailure(error, taxonomy) {
142
321
  * built-in classes). Unmatched errors fall through to the built-in flow.
143
322
  */
144
323
  export function recover(error, state, taxonomy) {
324
+ // v0.3.0 §7.1 — an error that ALREADY carries a classified terminal report
325
+ // (a child run's RunFailedError escalated through the sub-agent spawner)
326
+ // halts with that report verbatim: no re-classification, no retry ladder,
327
+ // and the user taxonomy cannot downgrade an already-terminal verdict —
328
+ // "a billing failure anywhere ends the run with the billing message".
329
+ if (isRunFailedError(error)) {
330
+ return { kind: "halt", report: error.report };
331
+ }
145
332
  if (taxonomy !== undefined && taxonomy.length > 0) {
146
333
  const named = matchNamedFailure(error, taxonomy);
147
334
  if (named !== undefined) {
148
- return recoverNamed(named, state);
335
+ return recoverNamed(named, state, error);
149
336
  }
150
337
  }
151
338
  const klass = classify(error);
152
339
  const message = error?.message;
153
340
  const reasonStr = typeof message === "string" && message.length > 0 ? message : klass;
154
341
  switch (klass) {
342
+ // v0.3.0 Goal 6 — terminal classes halt immediately: no tombstone
343
+ // detour, no futile backoff retries against an empty account or a
344
+ // rejected key.
345
+ case "billing":
346
+ return { kind: "halt", report: buildFailureReport("billing_exhausted", error) };
347
+ case "auth":
348
+ return { kind: "halt", report: buildFailureReport("auth_invalid", error) };
349
+ case "rate_limit": {
350
+ // A genuine rate limit is transient: keep retrying, but honor the
351
+ // provider's Retry-After for the delay when one is present. Budget
352
+ // exhaustion is a classified halt (exit 32), not a generic fail.
353
+ if (state.retryCount >= MAX_RETRIES) {
354
+ return { kind: "halt", report: buildFailureReport("rate_limited", error) };
355
+ }
356
+ return {
357
+ kind: "retry",
358
+ delayMs: retryAfterMs(error) ?? backoffMs(state.retryCount),
359
+ attempt: state.retryCount + 1,
360
+ };
361
+ }
155
362
  case "prompt_too_long":
156
363
  if (state.compactCount >= MAX_COMPACTS) {
157
364
  return { kind: "fail", reason: `compact budget exhausted: ${reasonStr}` };
@@ -185,15 +392,35 @@ export function recover(error, state, taxonomy) {
185
392
  /**
186
393
  * Convert a matched named-failure entry into the corresponding
187
394
  * RecoveryAction, respecting the same per-turn budgets as built-in
188
- * classes. Budget exhaustion always returns `fail` so the user can't
189
- * declare an infinite retry loop via taxonomy.
395
+ * classes. Budget exhaustion always returns a terminal action so the
396
+ * user can't declare an infinite retry loop via taxonomy.
397
+ *
398
+ * v0.3.0 Goal 6 — the entry's `hint` field (declared since §55, never
399
+ * consumed until now) finally reaches the user: when a matched entry
400
+ * resolves terminally (declared `fail` or budget exhaustion) AND carries a
401
+ * `hint`, the result is a `halt` whose report carries the hint as the
402
+ * remediation line. Entries without a hint keep the pre-0.3.0 `fail`.
190
403
  */
191
- function recoverNamed(named, state) {
404
+ function recoverNamed(named, state, error) {
192
405
  const reason = `failure_taxonomy: ${named.class}`;
406
+ const terminal = (fullReason) => {
407
+ if (named.hint === undefined)
408
+ return { kind: "fail", reason: fullReason };
409
+ return {
410
+ kind: "halt",
411
+ report: {
412
+ class: "unknown",
413
+ title: fullReason,
414
+ detail: error === undefined ? "" : rawProviderText(error),
415
+ remediation: named.hint,
416
+ exitCode: EXIT_CODES.generic,
417
+ },
418
+ };
419
+ };
193
420
  switch (named.recovery) {
194
421
  case "retry":
195
422
  if (state.retryCount >= MAX_RETRIES) {
196
- return { kind: "fail", reason: `retry budget exhausted: ${reason}` };
423
+ return terminal(`retry budget exhausted: ${reason}`);
197
424
  }
198
425
  return {
199
426
  kind: "retry",
@@ -202,26 +429,26 @@ function recoverNamed(named, state) {
202
429
  };
203
430
  case "compact":
204
431
  if (state.compactCount >= MAX_COMPACTS) {
205
- return { kind: "fail", reason: `compact budget exhausted: ${reason}` };
432
+ return terminal(`compact budget exhausted: ${reason}`);
206
433
  }
207
434
  return { kind: "compact" };
208
435
  case "continue":
209
436
  if (state.continueCount >= MAX_CONTINUES) {
210
- return { kind: "fail", reason: `continue budget exhausted: ${reason}` };
437
+ return terminal(`continue budget exhausted: ${reason}`);
211
438
  }
212
439
  return { kind: "continue" };
213
440
  case "tombstone":
214
441
  if (state.tombstoneCount >= MAX_TOMBSTONES) {
215
- return { kind: "fail", reason: `tombstone budget exhausted: ${reason}` };
442
+ return terminal(`tombstone budget exhausted: ${reason}`);
216
443
  }
217
444
  return { kind: "tombstone" };
218
445
  case "switch-model":
219
446
  if (state.switchModelCount >= MAX_SWITCH_MODELS) {
220
- return { kind: "fail", reason: `switch-model budget exhausted: ${reason}` };
447
+ return terminal(`switch-model budget exhausted: ${reason}`);
221
448
  }
222
449
  return { kind: "switch-model" };
223
450
  case "fail":
224
- return { kind: "fail", reason };
451
+ return terminal(reason);
225
452
  }
226
453
  }
227
454
  /**
@@ -241,6 +468,7 @@ export function advanceState(state, action) {
241
468
  case "switch-model":
242
469
  return { ...state, switchModelCount: state.switchModelCount + 1 };
243
470
  case "fail":
471
+ case "halt":
244
472
  return state;
245
473
  }
246
474
  }
@@ -254,4 +482,5 @@ export const BUDGETS = {
254
482
  BASE_BACKOFF_MS,
255
483
  MAX_BACKOFF_MS,
256
484
  MAX_JITTER_MS,
485
+ RETRY_AFTER_CAP_MS,
257
486
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/recovery-engine",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Pure recovery taxonomy: classify Anthropic API errors into a RecoveryAction (compact/retry/continue/tombstone/fail)",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/errors": "0.2.4"
18
+ "@crewhaus/errors": "0.3.1"
19
19
  },
20
20
  "license": "Apache-2.0",
21
21
  "author": {