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

This commit is contained in:
YeonGyu-Kim
2026-05-27 07:34:57 +09:00
parent d9844cfe8d
commit 16c1117af6
2 changed files with 48 additions and 1 deletions

View File

@@ -7727,3 +7727,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
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.
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.
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.

View File

@@ -222,7 +222,9 @@ fn main() {
// fields and add the stable status/error_kind/action contract used
// by non-interactive command guards.
let kind = classify_error_kind(&message);
let (short_reason, hint) = split_error_hint(&message);
let (short_reason, inline_hint) = split_error_hint(&message);
// #781: fall back to a kind-derived hint when the message has no \n-delimited hint
let hint = inline_hint.or_else(|| fallback_hint_for_error_kind(kind).map(String::from));
eprintln!(
"{}",
serde_json::json!({
@@ -301,6 +303,20 @@ fn classify_error_kind(message: &str) -> &'static str {
"unsupported_resumed_command"
} else if message.contains("confirmation required") {
"confirmation_required"
} else if (message.contains("api failed") || message.contains("api returned"))
&& (message.contains("401")
|| message.contains("Unauthorized")
|| message.contains("authentication_error"))
{
// #781: sub-classify auth failures so wrappers can distinguish from rate-limit / server errors
"api_auth_error"
} else if (message.contains("api failed") || message.contains("api returned"))
&& (message.contains("429")
|| message.contains("rate_limit")
|| message.contains("rate limit"))
{
// #781: sub-classify rate-limit failures
"api_rate_limit_error"
} else if message.contains("api failed") || message.contains("api returned") {
"api_http_error"
} else if message.contains("mcpServers") {
@@ -365,6 +381,24 @@ fn split_error_hint(message: &str) -> (String, Option<String>) {
}
}
/// #781: derive a stable fallback hint from a classified error kind when the error
/// message itself has no `\n`-delimited hint. Returns `None` for kinds where the
/// message is self-explanatory or no canonical remediation exists.
fn fallback_hint_for_error_kind(kind: &str) -> Option<&'static str> {
match kind {
"api_auth_error" => {
Some("Check that ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN is set and valid.")
}
"api_rate_limit_error" => {
Some("You have hit the API rate limit. Wait and retry, or reduce request frequency.")
}
"missing_credentials" => {
Some("Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN before running claw.")
}
_ => None,
}
}
/// Read piped stdin content when stdin is not a terminal.
///
/// Returns `None` when stdin is attached to a terminal (interactive REPL use),
@@ -13034,8 +13068,19 @@ mod tests {
classify_error_kind("confirmation required before running destructive operation"),
"confirmation_required"
);
// #781: 429 and 401 now sub-classify; generic 5xx/other still api_http_error
assert_eq!(
classify_error_kind("api returned unexpected status 429"),
"api_rate_limit_error"
);
assert_eq!(
classify_error_kind(
"api returned 401 Unauthorized (authentication_error): invalid x-api-key"
),
"api_auth_error"
);
assert_eq!(
classify_error_kind("api returned 500 Internal Server Error"),
"api_http_error"
);
assert_eq!(