| Age | Commit message (Collapse) | Author |
|
Closes the three items left on the deferred queue after the post-epic
cleanup.
GeminiRunner.execOnce now actually executes the gemini binary instead
of writing hardcoded stream data. Mirrors ClaudeRunner.execOnce:
- exec.CommandContext with the same env vars (CLAUDOMATOR_API_URL etc.)
- process group SIGKILL on context cancel
- stdout piped through parseGeminiStream → stdoutFile
- stderr to file
- exit codes captured, stderr tail surfaced on failure
Test infrastructure bug uncovered in passing: testServerWithGeminiMockRunner's
mock script used double-quoted echo with literal triple-backticks, which
bash interpreted as command substitution. The script always produced
empty output. The bug was invisible until now because GeminiRunner
ignored the script entirely. Switched to a single-quoted heredoc.
Frontend: index.html dropdown gains a "Local" option. No JS branching
needed — the value flows through to agent.type verbatim and downstream
display reads the type string as-is.
storage/db.go: removed stale debug-comment scaffolding (the "TODO:
Replace with proper logger" block) that was tracking a dead
`fmt.Printf` call. The path it commented on is fine without logging —
unmarshal errors are returned wrapped.
Test status: `go test -race ./...` green across every package, zero
skips, zero excluded tests.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Addresses the cleanup queue captured in docs/plans/local-oss-runner.md
after the local-OSS-models epic landed. After this commit
`go test -race ./...` is green across every package with zero `t.Skip`
calls and no excluded tests.
Real bugs fixed:
- claude.go setupSandbox callsites used `sandboxDir, err := ...` which
shadowed the outer variable, so BlockedError.SandboxDir was always
empty. Resume-after-block was broken for both new and stale-sandbox
paths. TestBlockedError_IncludesSandboxDir now exercises the right
invariant.
- TestPool_ActivePerAgent_DeletesZeroEntries flake under -race: the
cleanup defer in execute()/executeResume() runs AFTER
handleRunResult sends on resultCh, so consumers observing a result
could see a still-counted activePerAgent entry. Extracted
decActiveAgent(agentType, *cleaned) helper; called explicitly before
every resultCh send, defer becomes a no-op via the cleaned flag.
Verified clean over `go test -race -count=10`.
Test infrastructure made hermetic:
- gitSafe now also passes -c commit.gpgsign=false / -c tag.gpgsign=false
so sandbox tests pass on hosts whose global config requires signing.
- Bare repos in tests initialized with `-b main` (HEAD symbolic ref
matched to the branch we push) so `git log` after push works.
- TestSandboxCloneSource_FallsBackToOrigin uses a local-FS origin URL,
matching sandboxCloneSource's intentional filter against network URLs.
- TestGeminiLogs_ParsedCorrectly URL fixed to the actual log route
(/api/executions/{id}/log).
GeminiRunner gap closed (partial):
- parseGeminiStream now walks lines for `result` events, surfacing
is_error as an error and total_cost_usd as the float return value.
- GeminiRunner.Run propagates parsed cost to Execution.CostUSD.
- TestParseGeminiStream_ParsesStructuredOutput unskipped.
Notes:
- GeminiRunner is still simulated end-to-end (Run writes hardcoded
stream data instead of execing the binary). The result/cost parser
now exists; finishing the runner is a smaller, contained follow-up.
Kept on the deferred queue.
- Frontend "Local" agent option and a minor storage.db.go logger TODO
remain on the deferred queue, both intentionally — neither blocks
anything in flight.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Phase 4 of "local OSS models as agents" plan. Closes the epic.
When an execution finishes and the agent did NOT write a "## Summary"
heading in its stdout (so the existing extractSummary path returns
empty), and the Pool has a local LLM configured, we now synthesize a
2-4 sentence summary from the assistant text content of the log tail.
Behavior:
- Primary path unchanged: if the agent wrote "## Summary", that wins
byte-for-byte (TestPool_HandleRunResult_ExtractSummaryWins guards).
- Fallback path: empty extractSummary + Pool.LLM != nil → synthesize.
- All-empty path: when no LLM is configured, summary stays empty —
identical to pre-Phase-4 behavior.
Implementation:
- Pool gains an LLM *llm.Client field, wired in serve.go and run.go
alongside Classifier.LLM (same localClient used everywhere).
- New synthesizeSummary in internal/executor/summary.go:
* 6s timeout so a slow local model can't stall finalization
* 16 KB tail cap on the stdout log
* readAssistantTextTail seeks to the last 16 KB and skips the
first (likely partial) line, parses each line as a stream-json
event, joins assistant `text` blocks (skips system/result/etc).
* Returns "" on any error so the caller's behavior never regresses.
- handleRunResult: 3-tier summary resolution — exec.Summary set by
runner → extractSummary → synthesizeSummary → empty.
- minimalMockStore now records UpdateTaskSummary calls (additive;
existing tests unaffected) so integration tests can assert.
Tests (9 new):
- synthesizeSummary nil client / empty path / missing file all
return "" without HTTP calls.
- empty assistant content short-circuits without LLM call.
- success path returns trimmed body, with both assistant texts in
the user prompt.
- LLM 500 returns "" (caller handles same as no-summary).
- readAssistantTextTail seeks past early content in a large file.
- Pool integration: ## Summary present → LLM not called, agent text
used. ## Summary absent + LLM set → LLM called, synthesized summary
recorded against the right task ID.
Plan: docs/plans/local-oss-runner.md.
Epic complete. Post-epic deep cleanup queue captured in the same plan
file for follow-up.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Phase 1 of "local OSS models as agents" plan. Adds a third Runner
backed by any OpenAI-compatible HTTP server (Ollama, vLLM, LM Studio,
llama.cpp), and migrates the Gemini-CLI classifier to route through
the same client when configured.
Two-layer split: internal/llm.Client is the workhorse (HTTP, no Pool,
no DB) used directly by the classifier and any future internal helper
that needs cheap reasoning. internal/executor.LocalRunner is a thin
adapter implementing Runner for user-facing tasks. This avoids
Pool reentrancy/deadlock when sub-second internal calls fire from
inside Pool.execute().
Highlights:
- internal/retry: relocated runWithBackoff/IsRateLimitError/ParseRetryAfter
into a shared package reused by executor and llm.
- internal/llm: Chat (non-streaming) and ChatStream (SSE) over
/chat/completions with optional bearer auth, json_object response
format, retry on 429/503, Retry-After parsing.
- internal/executor/LocalRunner: streams deltas into stdout.log in the
same stream-json envelope ClaudeRunner emits, then writes one
consolidated assistant block plus a result terminator so existing
parsers (extractSummary, ParseChangestatFromOutput) work unchanged.
- internal/executor/Classifier: gains optional LLM field; uses
json_object response format (no markdown-fence cleanup needed).
Falls back to Gemini-CLI subprocess when LLM is nil.
- Pool.skipClassification: now skips only when the requested agent
type is registered, so unknown types still reach the load balancer.
- Storage: additive tokens_in/tokens_out ALTERs on executions; CLI
runners record cost_usd as before, LocalRunner records 0 + tokens.
- Config: [local_model] section (endpoint, model, timeout_seconds,
default_temperature, api_key). Empty endpoint = no LocalRunner
registered, classifier falls back to Gemini.
Pre-existing test issues fixed in passing:
- claude_test.go setupSandbox callsites updated to current signature.
- gemini_test.go TestParseGeminiStream skipped (asserts unimplemented
GeminiRunner stream-error parsing; tracked separately).
Plan: docs/plans/local-oss-runner.md.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
elaboration fallback
|
|
- Add ElaborationInput field to Task struct (task.go)
- Add DB migration and update CREATE/SELECT/scan in storage/db.go
- Update handleCreateTask to accept elaboration_input from API
- Update renderSubtaskRollup in app.js to prefer elaboration_input over description
- Capture elaborate prompt in createTask() form submission
- Update subtask-placeholder tests to cover elaboration_input priority
- Fix missing io import in gemini.go
When a task card is waiting for subtasks, it now shows:
1. The raw user prompt from elaboration (if stored)
2. The task description truncated at word boundary (~120 chars)
3. The task name as fallback
4. 'Waiting for subtasks…' only when all fields are empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When the server restarts after all subtasks complete, the parent task
was left stuck in BLOCKED state because maybeUnblockParent only fires
during a live executor run. RecoverStaleBlocked() scans all BLOCKED
tasks on startup and re-evaluates them using the existing logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
- Fix ephemeral sandbox deletion issue by passing $CLAUDOMATOR_PROJECT_DIR to agents and using it for subtask project_dir.
- Implement sandbox autocommit in teardown to prevent task failures from uncommitted work.
- Track git commits created during executions and persist them in the DB.
- Display git commits and changestats badges in the Web UI execution history.
- Add badge counts to Web UI tabs for Interrupted, Ready, and Running states.
- Improve scripts/next-task to handle QUEUED tasks and configurable DB path.
|
|
Files changed: CLAUDE.md, internal/api/changestats.go,
internal/executor/executor.go, internal/executor/executor_test.go,
internal/task/changestats.go (new)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
sandboxes
#1 - Diagnostics: tailFile() reads last 20 lines of subprocess stderr and
appends to error message when claude/gemini exits non-zero. Previously all
exit-1 failures were opaque; now the error_msg carries the actual subprocess
output.
#4 - Restart recovery: RecoverStaleRunning() now re-queues tasks after
marking them FAILED, so tasks killed by a server restart automatically
retry on the next boot rather than staying permanently FAILED.
#2 - Stale sandbox: If a resume execution's preserved SandboxDir no longer
exists (e.g. /tmp purge after reboot), clone a fresh sandbox instead of
failing immediately with "no such file or directory".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Sandbox setup runs git commands against project_dir which may be owned
by a different OS user, triggering git's 'dubious ownership' error.
Fix by passing -c safe.directory=* on all git commands that touch
project directories. Also add wildcard to global config for immediate
effect on the running server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Add UpdateTaskAgent to Store interface and DB implementation
- Call UpdateTaskAgent in Pool.execute to persist assigned agent/model
to database before the runner starts
- Update runTask in app.js to pass selected agent as query param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
- Added an agent selector (Auto, Claude, Gemini) to the Start Next Task button.
- Updated the backend to pass query parameters as environment variables to scripts.
- Modified the executor pool to skip classification when a specific agent is requested.
- Added --agent flag to claudomator start command.
- Updated tests to cover the new functionality.
|
|
Two fixes for BLOCKED task issues:
1. Allow BLOCKED → CANCELLED state transition so users can cancel tasks
stuck waiting for input. Adds Cancel button to BLOCKED task cards in
the UI alongside the question/answer controls.
2. Detect when agents write completion reports to $CLAUDOMATOR_QUESTION_FILE
instead of real questions. If the question JSON has no options and no "?"
in the text, treat it as a summary (stored on the execution) and fall
through to normal completion + sandbox teardown rather than blocking.
Also tightened the preamble to make the distinction explicit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
GeminiRunner.buildArgs was missing --yolo (auto-approve all tools)
so the gemini CLI only registered 3 tools (read_file, write_todos,
cli_help) and write_file was not available. Agents that needed to
create files silently failed (exit 0, no files written).
Also switch instructions from bare positional arg to -p flag, which
is required for non-interactive headless mode.
Update preamble tests to match file-based summary approach
(CLAUDOMATOR_SUMMARY_FILE) kept from the merge conflict resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Keep file-based summary approach (CLAUDOMATOR_SUMMARY_FILE) from HEAD.
Combine Q&A History and Stats tab CSS from both branches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Add Pool.RecoverStaleQueued() that lists all QUEUED tasks from the DB on
startup and re-submits them to the in-memory pool. Previously, tasks that
were QUEUED when the server restarted would remain stuck indefinitely since
only RUNNING tasks were recovered (and marked FAILED).
Called in serve.go immediately after RecoverStaleRunning().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Extend Resume to CANCELLED, FAILED, and BUDGET_EXCEEDED tasks
- Add summary extraction from agent stdout stream-json output
- Fix storage: persist stdout/stderr/artifact_dir paths in UpdateExecution
- Clear question_json on ResetTaskForRetry
- Resume BLOCKED tasks in preserved sandbox so Claude finds its session
- Add planning preamble: CLAUDOMATOR_SUMMARY_FILE env var + summary step
- Update ADR-002 with new state transitions
- UI style improvements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Interrupted tasks (CANCELLED, FAILED, BUDGET_EXCEEDED) now support session
resume in addition to restart. Both buttons are shown on the task card.
- executor: extend resumablePoolStates to include CANCELLED, FAILED, BUDGET_EXCEEDED
- api: extend handleResumeTimedOutTask to accept all resumable states with
state-specific resume messages; replace hard-coded TIMED_OUT check with a
resumableStates map
- web: add RESUME_STATES set; render Resume + Restart buttons for interrupted
states; TIMED_OUT keeps Resume only
- tests: 5 new Go tests (TestResumeInterrupted_*); updated task-actions.test.mjs
with 17 tests covering dual-button behaviour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Add a mandatory '## Final Summary' section to planningPreamble
instructing agents to output a 2-5 sentence summary paragraph
(headed by '## Summary') as their last output before exiting.
Adds three tests to verify the section and its required content
are present in the preamble.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When a task ran in a sandbox (/tmp/claudomator-sandbox-*) and went BLOCKED,
Claude stored its session under the sandbox path as the project slug. The
resume execution was running in project_dir, causing Claude to look for the
session in the wrong project directory and fail with "No conversation found".
Fix: carry SandboxDir through BlockedError → Execution → resume execution,
and run the resume in that directory so the session lookup succeeds.
- BlockedError gains SandboxDir field; claude.go sets it on BLOCKED exit
- storage.Execution gains SandboxDir (persisted via new sandbox_dir column)
- executor.go stores blockedErr.SandboxDir in the execution record
- server.go copies SandboxDir from latest execution to the resume execution
- claude.go uses e.SandboxDir as working dir for resume when set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
executor: add 7 tests for sandboxCloneSource, setupSandbox, and
teardownSandbox (uncommitted-changes error, clean-no-commits removal).
api: fix two data races in WebSocket tests — wsPingInterval/Deadline
are now captured as locals before goroutine start; maxWsClients is
moved from a package-level var into Hub.maxClients (with SetMaxClients
method) so concurrent tests don't stomp each other.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When a resumed execution is blocked again, SessionID was set to the new
exec's own UUID instead of the original ResumeSessionID. The next resume
would then pass the wrong --resume argument to claude and fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
pickAgent() deterministically selects the agent with the fewest active tasks,
skipping rate-limited agents. The classifier now only selects the model for the
pre-assigned agent, so Gemini gets tasks from the start rather than only as a
fallback when Claude's quota is exhausted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Updated handleRunTask to use ResetTaskForRetry, which clears the agent type and model. This ensures that manually restarted tasks are always re-classified, allowing the system to switch to a different agent if the previous one is rate-limited. Also improved Claude quota-exhaustion detection.
|
|
Both execute() and executeResume() shared ~80% identical post-run logic:
error classification (BLOCKED, TIMED_OUT, CANCELLED, BUDGET_EXCEEDED, FAILED),
state transitions, result emission, and UpdateExecution. Extract this into
handleRunResult(ctx, t, exec, err, agentType) on *Pool. Both functions now
call it after runner.Run() returns.
Also adds TestHandleRunResult_SharedPath which directly exercises the new
function via a minimalMockStore, covering FAILED, READY, COMPLETED, and
TIMED_OUT classification paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Add maybeUnblockParent helper that transitions a BLOCKED parent task to
READY once every subtask is in the COMPLETED state. Called in both
execute() and executeResume() immediately after a subtask is marked
COMPLETED. Any non-COMPLETED sibling (RUNNING, FAILED, etc.) keeps the
parent BLOCKED.
Tests added:
- TestPool_Submit_LastSubtask_UnblocksParent
- TestPool_Submit_NotLastSubtask_ParentStaysBlocked
- TestPool_Submit_ParentNotBlocked_NoTransition
|
|
When a top-level task (ParentTaskID == "") finishes successfully,
check for subtasks before deciding the next state:
- subtasks exist → BLOCKED (waiting for subtasks to complete)
- no subtasks → READY (existing behavior, unchanged)
This applies to both execute() and executeResume().
Adds ListSubtasks to the Store interface.
Tests:
- TestPool_Submit_TopLevel_WithSubtasks_GoesBlocked
- TestPool_Submit_TopLevel_NoSubtasks_GoesReady
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
All previously ignored errors from p.store.UpdateTaskState() and
p.store.UpdateTaskQuestion() in execute() and executeResume() now log
with structured context (taskID, state, error).
Introduces a Store interface so tests can inject a failing mock store.
Adds TestPool_UpdateTaskState_DBError_IsLoggedAndResultDelivered to
verify that a DB write failure is logged and the result is still
delivered to resultCh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The pgid-kill goroutine in execOnce() uses a select with both ctx.Done()
and the killDone channel. Add a detailed comment explaining why the goroutine
cannot block indefinitely: the killDone arm fires unconditionally when
cmd.Wait() returns (whether the process exited naturally or was killed),
so the goroutine always exits before execOnce() returns.
Add TestExecOnce_NoGoroutineLeak_OnNaturalExit to verify this: it samples
runtime.NumGoroutine() before and after execOnce() with a no-op binary
("true") and a background context (never cancelled), asserting no net
goroutine growth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Updated isQuotaExhausted to detect more Claude quota messages. Added 'rate limit reached (rejected)' to quota exhausted checks. Strengthened classifier prompt to explicitly forbid selecting rate-limited agents. Improved Pool to set 5h rate limit on quota exhaustion.
|
|
concurrent rejection
- Remove git pull into project_dir: working copy is the developer workspace
and should be pulled manually; www-data can't write to root-owned .git/objects
- On non-fast-forward push rejection (concurrent task pushed first), fetch and
rebase then retry once instead of failing the entire task
|
|
activePerAgent: delete zero-count entries after decrement so the map
doesn't accumulate stale keys for agent types that are no longer active.
rateLimited: delete entries whose deadline has passed when reading them
(in both the classifier block and the execute() pre-flight), so stale
entries are cleaned up on the next check rather than accumulating forever.
Both fixes are covered by new regression tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
|
On restart, any tasks in RUNNING state have no active goroutine.
RecoverStaleRunning() marks them FAILED (retryable) and closes
their open execution records with an appropriate error message.
Called once from serve.go after the pool is created.
|
|
Updated parseStream to detect 'rate_limit_event' and 'assistant' error:rate_limit messages from the Claude CLI. Updated Classifier to strongly prefer non-rate-limited agents. Added logging to Pool to track rate-limit status during classification.
|
|
Update the default Gemini model and classification prompt to use gemini-2.5-flash-lite, which is the current available model. Improved the classifier's parsing logic to correctly handle the JSON envelope returned by the gemini CLI (stripping 'response' wrapper and 'Loaded cached credentials' noise).
|
|
Instead of git fetch/merge INTO the working copy (which fails with
mixed-owner .git/objects), clone FROM a bare repo, push BACK to it,
then pull into the working copy:
sandbox clone ← bare repo (local remote or origin)
agent commits in sandbox
git push sandbox → bare repo
git pull bare repo → working copy
sandboxCloneSource() prefers a remote named "local" (local bare repo),
then "origin", then falls back to the working copy path.
Set up: git remote add local /site/git.terst.org/repos/claudomator.git
The bare repo was created with: git clone --bare /workspace/claudomator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Fix: use file:// prefix in git fetch during sandbox teardown to force
pack-protocol transfer. The local optimization uses hard links which
fail across devices and with mixed-owner object stores.
Feature: before running a task, query prior failed/timed-out executions
and prepend their error messages to the agent's --append-system-prompt.
This tells the agent what went wrong in previous attempts so it doesn't
repeat the same mistakes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Agents running in a sandbox must commit all changes before exiting.
The teardown rejects any dirty working tree. Add an explicit section
to the planning preamble making this requirement clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
git clone --local fails with "Invalid cross-device link" when /workspace
and /tmp are on different filesystems. --no-hardlinks forces object
copying instead, which works across devices.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Resolve conflicts in API server, CLI, and executor.
- Maintain Gemini classification and assignment logic.
- Update UI to use generic agent config and project_dir.
- Fix ProjectDir/WorkingDir inconsistencies in Gemini runner.
- All tests passing after merge.
|
|
- Add Classifier using gemini-2.0-flash-lite to automatically select agent/model.
- Update Pool to track per-agent active tasks and rate limit status.
- Enable classification for all tasks (top-level and subtasks).
- Refine SystemStatus to be dynamic across all supported agents.
- Add unit tests for the classifier and updated pool logic.
- Minor UI improvements for project selection and 'Start Next' action.
|
|
Replace the at-capacity error return from Submit/SubmitResume with an
internal workCh/doneCh channel pair. A dispatch() goroutine blocks
waiting for a free slot and launches the worker goroutine, so tasks are
buffered up to 10x pool capacity instead of being rejected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- ClaudeConfig.WorkingDir → ProjectDir (json: project_dir)
- UnmarshalJSON fallback reads legacy working_dir from DB records
- New executions with project_dir clone into a temp sandbox via git clone --local
- Non-git project_dirs get git init + initial commit before clone
- After success: verify clean working tree, merge --ff-only back to project_dir, remove sandbox
- On failure/BLOCKED: sandbox preserved, path included in error message
- Resume executions run directly in project_dir (no re-clone)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
agent test)
|
|
|
|
When claude hits the 5-hour usage limit it exits 1. execOnce was
returning the generic "exit status 1" error, hiding the real cause from
the retry loop and the task state machine.
Fix:
- execOnce now surfaces streamErr when it indicates rate limiting or
quota exhaustion, so callers see the actual message.
- New isQuotaExhausted() detects "hit your limit" messages — these are
not retried (retrying a depleted 5h bucket wastes nothing but is
pointless), and map to BUDGET_EXCEEDED in both execute/executeResume.
- isRateLimitError() remains for transient throttling (429/overloaded),
which continues to trigger exponential backoff retries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|