Skip to content

Commit 16c1117

Browse files
committed
fix(#781): sub-classify api_auth_error/api_rate_limit_error from api_http_error; add fallback_hint_for_error_kind for hint-less API errors
1 parent d9844cf commit 16c1117

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7727,3 +7727,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
77277727
779. **Resumed `/skills <skill>` invocation returned bare prose → `error_kind:"unknown"` + `hint:null` after #776** — dogfooded 2026-05-27 on `fded4f6b` (pinpoint by Gaebal-gajae). Sibling of #777: the `/skills` invoke-dispatch guard emitted a single-line prose error identical in structure to the pre-#777 plugins mutation guard. After #776's classify/split it fell to `unknown+null` because no `interactive_only:` prefix was present. Fix: replaced with `interactive_only: /skills {skill_name} invocation requires a live session.\n...hint...` format. Integration test `resume_skills_invocation_is_typed_interactive_only_779` added. 40 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `fded4f6b`, 2026-05-27.
77287728

77297729
780. **`classify_error_kind` arm ordering bug: `"failed to restore session: legacy session is missing workspace binding: ..."` classified as `session_load_failed` instead of `legacy_session_no_workspace_binding`** — dogfooded 2026-05-27 on `364e7909`. The full error message from `resume_session` prepends `"failed to restore session: "` before `"legacy session is missing workspace binding: ..."`. The `contains("failed to restore session")` arm at line 278 matched first, returning `session_load_failed`; the more specific `legacy_session_no_workspace_binding` arm at line 282 was never reached. Same shadowing existed for `no_managed_sessions`. Fix: reordered the three arms — specific cases (`no_managed_sessions`, `legacy_session_no_workspace_binding`) before the generic `session_load_failed` catch-all. Unit test updated to assert corrected discriminants, plus new assertion covering the full prefixed message that exposed the bug. 40 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori classifier-ordering probe on `364e7909`, 2026-05-27.
7730+
7731+
781. **`api_http_error` was a single bucket for all HTTP errors; 401 auth and 429 rate-limit returned `hint:null` with no distinction** — dogfooded 2026-05-27 on `d9844cfe`. `classify_error_kind` had a single `api_http_error` arm for all API failures. 401 Unauthorized and 429 rate-limit errors emitted `error_kind:"api_http_error"` + `hint:null`, making it impossible for automation to distinguish auth misconfiguration from transient rate-limiting. Fixes: (1) added `api_auth_error` sub-classifier arm for 401/Unauthorized/authentication_error messages; (2) added `api_rate_limit_error` arm for 429/rate_limit messages; (3) added `fallback_hint_for_error_kind()` that derives a stable hint from the error kind when `split_error_hint` returns `None` (API layer never emits `\n`-delimited hints); (4) main JSON error emission path now calls `fallback_hint_for_error_kind` as fallback. Auth errors now return `api_auth_error` + env-var hint; rate-limit returns `api_rate_limit_error` + retry hint. Unit tests updated. 40 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori API error opacity probe on `d9844cfe`, 2026-05-27.

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ fn main() {
222222
// fields and add the stable status/error_kind/action contract used
223223
// by non-interactive command guards.
224224
let kind = classify_error_kind(&message);
225-
let (short_reason, hint) = split_error_hint(&message);
225+
let (short_reason, inline_hint) = split_error_hint(&message);
226+
// #781: fall back to a kind-derived hint when the message has no \n-delimited hint
227+
let hint = inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
226228
eprintln!(
227229
"{}",
228230
serde_json::json!({
@@ -301,6 +303,20 @@ fn classify_error_kind(message: &str) -> &'static str {
301303
"unsupported_resumed_command"
302304
} else if message.contains("confirmation required") {
303305
"confirmation_required"
306+
} else if (message.contains("api failed") || message.contains("api returned"))
307+
&& (message.contains("401")
308+
|| message.contains("Unauthorized")
309+
|| message.contains("authentication_error"))
310+
{
311+
// #781: sub-classify auth failures so wrappers can distinguish from rate-limit / server errors
312+
"api_auth_error"
313+
} else if (message.contains("api failed") || message.contains("api returned"))
314+
&& (message.contains("429")
315+
|| message.contains("rate_limit")
316+
|| message.contains("rate limit"))
317+
{
318+
// #781: sub-classify rate-limit failures
319+
"api_rate_limit_error"
304320
} else if message.contains("api failed") || message.contains("api returned") {
305321
"api_http_error"
306322
} else if message.contains("mcpServers") {
@@ -365,6 +381,24 @@ fn split_error_hint(message: &str) -> (String, Option<String>) {
365381
}
366382
}
367383

384+
/// #781: derive a stable fallback hint from a classified error kind when the error
385+
/// message itself has no `\n`-delimited hint. Returns `None` for kinds where the
386+
/// message is self-explanatory or no canonical remediation exists.
387+
fn fallback_hint_for_error_kind(kind: &str) -> Option<&'static str> {
388+
match kind {
389+
"api_auth_error" => {
390+
Some("Check that ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN is set and valid.")
391+
}
392+
"api_rate_limit_error" => {
393+
Some("You have hit the API rate limit. Wait and retry, or reduce request frequency.")
394+
}
395+
"missing_credentials" => {
396+
Some("Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN before running claw.")
397+
}
398+
_ => None,
399+
}
400+
}
401+
368402
/// Read piped stdin content when stdin is not a terminal.
369403
///
370404
/// Returns `None` when stdin is attached to a terminal (interactive REPL use),
@@ -13034,8 +13068,19 @@ mod tests {
1303413068
classify_error_kind("confirmation required before running destructive operation"),
1303513069
"confirmation_required"
1303613070
);
13071+
// #781: 429 and 401 now sub-classify; generic 5xx/other still api_http_error
1303713072
assert_eq!(
1303813073
classify_error_kind("api returned unexpected status 429"),
13074+
"api_rate_limit_error"
13075+
);
13076+
assert_eq!(
13077+
classify_error_kind(
13078+
"api returned 401 Unauthorized (authentication_error): invalid x-api-key"
13079+
),
13080+
"api_auth_error"
13081+
);
13082+
assert_eq!(
13083+
classify_error_kind("api returned 500 Internal Server Error"),
1303913084
"api_http_error"
1304013085
);
1304113086
assert_eq!(

0 commit comments

Comments
 (0)