- Add missing retry_after: None field to ApiError::Api construction
in main.rs test. This field was introduced by the Retry-After
header support but was not added to the test's error initializer,
causing a compile error under CI's strict mode.
- Remove duplicate #[must_use] attribute on retry_after() method
in error.rs (lines 134+138 both had it; kept the outer one
above the doc comment per convention).
- Cargo fmt --all run.
- Reviewer question "Are defaults preserved?" — answered yes:
ApiTimeoutConfig defaults to 30s connect / 300s request / 8 retries.
with_retry_policy() is opt-in. No behavior change without explicit
configuration.
Cherry-picked from PR #2816 onto current upstream/main, resolving
conflicts from PR #3015's merge (which added retry_after to ApiError
but some construction sites were missing it).
Commits preserved:
- ade85398: API timeout config, Retry-After header, configurable retry
- TimeoutConfig in HTTP client builder (connect 30s, request 5min)
- CLAW_API_CONNECT_TIMEOUT and CLAW_API_REQUEST_TIMEOUT env vars
- Retry-After header parsing on 429 responses
- ApiTimeoutConfig in runtime config (settings.json)
- 8a883430: retry 400 responses with transient gateway error bodies
- Detects known gateway phrases in 400 response bodies
- Marks them as retryable instead of hard-failing
- ed91a61e: add 'no parseable body' to CONTEXT_WINDOW_ERROR_MARKERS
- Some providers return 400 with 'no parseable body' for oversized
requests instead of a proper context_length_exceeded error
Commits skipped (already in upstream via PR #3015):
- 453ab642: optional id field (already merged)
- baa8d1ba: HTML detection in streaming (already merged)
- 33d2f789: JSON error detection in streaming (already merged)
8 files changed, 299 insertions, 80 deletions
Some OpenAI-compat backends (e.g. glm-5.1-fast) return 400 with
"no parseable body" when the request payload is too large to parse,
rather than a proper context_length_exceeded error. Without this marker,
is_context_window_error() returns false and the auto-compact retry
loop never triggers — the user just sees an opaque 400 error.
💘 Generated with Crush
Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
Some providers/proxies return HTTP 400 with bodies like "no parseable
body" or "connection reset" during transient network blips. These are
not real bad requests — they're gateway errors wearing a 400 mask.
Detect known gateway error phrases in 400 response bodies and mark
them as retryable so the existing exponential backoff handles them.
- Add TimeoutConfig to HTTP client builder with connect_timeout (30s)
and request_timeout (5min) defaults, configurable via
CLAW_API_CONNECT_TIMEOUT and CLAW_API_REQUEST_TIMEOUT env vars
- Add with_timeout() builder to both AnthropicClient and
OpenAiCompatClient for per-client timeout configuration
- Parse Retry-After header on 429 responses and use it to override
exponential backoff delay when present
- Add ApiTimeoutConfig to runtime config with apiTimeout settings
in ~/.claw/settings.json (connectTimeout, requestTimeout, maxRetries)
- Add retry_after field to ApiError::Api for propagating rate limit
backoff hints through the retry pipeline
Commands like /commit, /pr, /issue, /bughunter, /ultraplan are
interactive-only and NOT resume-safe. Previously the generic
interactive_only error always suggested 'claw --resume SESSION.jsonl
/commit', which would just re-trigger interactive_only.
Fix: check commands::resume_supported_slash_commands() in the
SlashCommand::Ok(Some(cmd)) arm. Resume-safe commands get the full
--resume suggestion; non-resume-safe commands only say 'Start claw'.
Also update two existing unit tests whose assertions checked for the old
'interactive-only' substring (now 'interactive_only:' prefix).
Two new integration tests:
- non_resume_safe_interactive_only_hint_omits_resume_suggestion
- resume_safe_interactive_only_hint_includes_resume_suggestion
572 tests pass, 1 pre-existing worker_boot failure unrelated.
/approve, /yes, /deny, /no (and /y, /n) are valid REPL-only slash
commands. Outside the REPL they were falling through to
format_unknown_direct_slash_command -> error_kind:unknown_slash_command.
Fix: intercept them in the SlashCommand::Unknown arm and emit
interactive_only: prefix so classify_error_kind returns the correct kind.
One new test: approve_deny_outside_repl_emits_interactive_only (covers
/approve, /yes, /deny, /no)
572 tests pass, 1 pre-existing worker_boot failure unrelated.
Single-word all-alpha/dash tokens that don't match any known subcommand
now always emit command_not_found (with or without fuzzy suggestions).
Multi-word cases fall through to CliAction::Prompt (natural language
prompt passthrough like 'claw explain this' must still work). The
multi-word gap is documented as ROADMAP #826 (known limitation).
Tests:
- unknown_subcommand_json_emits_command_not_found (new)
- unknown_subcommand_text_emits_command_not_found_on_stderr (new)
- unknown_subcommand_typo_with_suggestions_json_emits_command_not_found (new)
- multi_word_unknown_subcommand_falls_through_to_prompt_826 (documents gap)
572 tests pass, 1 pre-existing worker_boot failure unrelated.