Repository navigation
fix(mcp): a usage-limit 429 after the run is no longer reported as agent_busy (#3244) - #3499
Merged
Merged
Conversation
…ent_busy (#3244) Root cause: TrinityClient.chat() turned every 429 from POST /chat that the gate parser did not claim into queue_status "queue_full", and runAgentChat answered it agent_busy ("Please wait N seconds before retrying"). Two different 429s reach that branch. The admission refusal (X-Trinity-Error-Code: capacity) dispatched nothing. The usage / rate-limit 429 is raised by _finalize_http_failure -> _apply_sub003_autoswitch after the agent ran the turn and the row was written failed, so a retry repeats what the partial run did. That 429 also carried nothing naming its row. Fix: - Backend: the failures raised from _finalize_http_failure name their row on an additive X-Trinity-Execution-Id response header, beside X-Trinity-Error-Code (the #2889 shape; bodies unchanged, so the frontend and every other reader of that 429 see what they saw). - MCP client: only a 429 with code "capacity" is agent_busy. Any other 429 is a rate_limited result, retryable: false, which names the failed execution and points at get_execution_result; with no execution id it says the run may have started and points at list_recent_executions. A 429 with no code fails the same way: "nothing ran, retry" is the one answer that is unsafe when wrong. The backend's own "Please retry." is not passed on. - Delegation contract (both copies): agent_busy now means nothing ran. - Docs that stated the old behaviour. Not changed: client.task() still throws "API error (429)" for /task. It never claimed that nothing ran, and a billing failure on /task answers 503. Tests: src/mcp-server/src/chat-429-split.test.ts drives both 429 shapes through the real chat() and the real chat_with_agent tool; tests/unit/test_3244_post_run_429_names_execution.py drives the real finalizer. Mutation: with client.ts and tools/chat.ts reverted, 7 of the 8 MCP tests go red; with the header removed from _apply_sub003_autoswitch, 5 of the 7 Python tests go red. Restored byte-identical, all green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…429-not-busy # Conflicts: # src/backend/services/chat_execution_service.py
Mechanical, per the merge-train note on the PR. #3486 (#3470) landed on dev after this branch forked: its exhausted-walk 429 raises with `auto_switch: walk.summary()`, a non-empty dict whose `switched` is false, so `Boolean(auto_switch)` would append "The agent's subscription was switched after the failure." when nothing was switched. Read the flag when the body carries it; keep presence as the fallback for older bodies. mcp-server: 836/836, tsc clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
|
merge-train (2026-10-10): on this train. Two commits pushed to this branch, both mechanical:
Validation ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
TrinityClient.chat()answered every HTTP 429 fromPOST /api/agents/{name}/chatas "queue full", andchat_with_agentreported it asagent_busy— "Please wait N seconds before retrying". Two different 429s reached that branch. The admission refusal (X-Trinity-Error-Code: capacity) dispatched nothing. The usage / rate-limit 429 is raised after the agent ran the turn and the row was writtenfailed, so a retry repeats whatever the partial run already did. That second 429 also carried nothing naming its execution.chat_execution_service.py): failures raised from_finalize_http_failurenow carry an additiveX-Trinity-Execution-Idresponse header beside the error code. Response bodies are unchanged.capacityisagent_busy. Any other 429 is a newrate_limitedresult withretryable: false.agent_busysentence now says nothing ran and to re-send afterretry_after_seconds. This changes the prompt every agent receives (6 characters shorter, inside the budget).The post-run result:
{ "status": "rate_limited", "agent": "target", "code": "billing", "execution_id": "<failed row>", "error": "<agent's own error text>", "retryable": false, "message": "The run on 'target' started and then failed on a usage or rate limit; it is recorded as failed. Part of the work may already be done, so do not re-send this message as it is. Read what happened with get_execution_result(...) and decide from that." }A 429 with no error code (a proxy, or a backend older than #2919) is also
rate_limited, without anexecution_id, pointing atlist_recent_executions. "Nothing ran, retry" is the one label that is unsafe when wrong.For the reviewer:
X-Trinity-Execution-Idalready exists as a request header meaning the caller's own turn. On a response it now means the turn the response is about. Say if a distinct name is preferred.run_playbook,ask) callchat()and pass the result through, so a connector caller can now receiverate_limited, whose message namesget_execution_result/list_recent_executions— tools that scope may not have. No connector-specific wording was added.client.task()is unchanged: it throwsAPI error (429)and never claimed nothing ran; a billing failure on/taskanswers 503.fan_outraises no 429.Related Issue
Fixes #3244
Journey Impact
Journey Impact: none: bug fix to the label on an existing error result; no journey promise is added or changed
Type of Change
Testing
I have tested this locally
New tests added (if applicable)
All existing tests pass
Every new test executes the changed path
src/mcp-server/src/chat-429-split.test.ts(8 tests) drives both 429 shapes through the realchat()and the realchat_with_agenttool and asserts the answers differ.tests/unit/test_3244_post_run_429_names_execution.py(7 tests) drives the real finalizer.MCP server
npm test: 833 pass, 0 fail;tsc --noEmitclean. 73 neighbouring Python unit files: 2363 passed, 4 skipped, 7 xfailed.Not run: the full
tests/unitsuite, and no live backend — the header is verified through the finalizer and a FastAPIHTTPExceptionround-trip, not the full route.Two existing fixtures that used an unmarked 429 to mean "busy" (
chat-gate.test.ts,chat-parent-execution.test.ts) now send thecapacityheader.Mutation: with
client.tsandtools/chat.tsreverted, 7 of the 8 MCP tests go red; with the header removed from_apply_sub003_autoswitch, 5 of the 7 Python tests go red. Restored byte-identical.Checklist
🤖 Generated with Claude Code