Repository navigation
fix(sub-003): a turn walks every subscription and the API key before it fails (#3470) - #3486
Merged
Merged
Conversation
…it fails; the key is a rung, not a reassignment (#3470) SUB-003 remediated once per turn (one switch or one API-key fallback, one re-issue) and three surfaces then handed the retry back to the person. Now each refused attempt asks `advance_subscription_walk` for the next credential — another subscription, then the platform key — and the SAME execution_id is re-issued with it as a request-scoped `auth_override` the agent applies to that one spawn (`X-Trinity-Auth-Override` echoes it; an old image falls back to the #792 commit-and-retry, one restart max, no key rung). Nothing moves until a trial serves; the serving subscription is committed once with a CAS on the assignment the turn started from. Bounded by the remaining turn budget (#2789, the only ceiling) over a 30 s floor and by the walk's own rung cap. Part 2: `fallback_to_api_key` no longer clears the assignment, flips `use_platform_api_key` or restarts the container. The key is tried by override only, skipped while recently refused (auth/billing 2h, a 429 5 min), never offered to an agent with `use_platform_api_key` explicitly off; a key refusal is one more exhausted candidate and the assignment is preserved throughout. Agent server: `AuthOverride` (SecretStr) on /api/task + /api/chat, `build_execution_env(…, drop=)`, header on success and HTTPException, value staged for redaction. Backend: the walk core in subscription_auto_switch, both dispatch loops, the portal ladder, `db.set_execution_subscription` (SUB-004 re-attribution, no schema change), `is_container_fault` so a container-level 503 never walks, two EXEMPT dedupe keyspaces. Scheduler classifier mirror re-synced. Docs + two learnings entries. Tests: tests/unit/test_3470_subscription_walk.py (51); test_792/test_2789 re-seamed on the walk; test_2638's one-remediation test reframed. Fixes #3470 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
trinity-ability
approved these changes
Oct 9, 2026
trinity-ability
left a comment
Contributor
There was a problem hiding this comment.
merge-train: batch validated on train/20261009-2324-b (#3504)
vybe
added a commit
that referenced
this pull request
Oct 10, 2026
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>
7 of 8 tasks
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.
Summary
advance_subscription_walkfor the next credential — another subscription, then the platform API key — and the sameexecution_idis re-issued with it as a request-scopedauth_overridethe agent applies to that one spawn. Nothing in the DB or the container moves until a trial serves; the serving subscription is then committed once (assign + hot-reload + one notification), with a compare-and-set on the assignment so two workers never both commit.fallback_to_api_keyno longer clears the assignment, flipsuse_platform_api_keyor restarts the container. The key is tried by override only, skipped while recently refused (auth/billing ⇒ 2h, a 429 ⇒ 5 min), never offered to an agent whoseuse_platform_api_keyis explicitly off, and a key refusal is one more exhausted candidate. "Switched to the platform API key" / "Returned to subscription" are transition notifications; the assignment is preserved throughout./chatshow the usage-limit message, naming the earliest reset across everything tried; the attempts trail is appended to the FAILED row (Tried: A (rate limit) -> B (auth) -> platform API key (billing); every candidate refused.), carried onTaskExecutionResult.subscription_switch.attemptsand theauto_retryaudit rows, and one operator alert per agent per hour names what was tried. The "send that again" copy remains only when the turn's time budget ran out before a re-issue.Bounds: the turn's remaining wall clock (#2789 — still the only ceiling) over a floor of settle + HTTP slack + 30 s, plus the walk's own rung cap (
len(subscriptions) + 2, "already tried ⇒ exhausted"). Attribution is exact (the walk knows what it sent). Interactive triggers may take any untried skip-listed subscription as a last resort; autonomous ones only one whose last failure is ≥ 30 min old. Old agent images (noX-Trinity-Auth-Overrideecho) degrade to the #792 commit-and-retry per rung, capped at one restart-based rung and with no key rung.Design note: the plan review (two independent reviewers) converged on replacing a container-level hot-reload + Redis "fallback active" marker with the per-spawn override — a marker would drift across a plain
docker restartor a Redis outage and misattribute key refusals; the override is stateless and exact.Changes
Agent server (
docker/base-image/agent_server/):models.AuthOverride(SecretStrfields) onParallelTaskRequest/ChatRequest;execution_env.build_execution_env(…, drop=)+auth_override_env_layers; both spawn sites apply it;routers/chat.pystampsX-Trinity-Auth-Overrideon success and on the HTTPException, stages the value for redaction, drops it for non-Claude runtimes;AgentRuntime.supports_auth_override.Backend:
services/subscription_auto_switch.py—SubscriptionWalk/Rung/advance_subscription_walk/commit_subscription_walk/apply_rung_legacy/api_key_rung/ refusal + notice markers; selector gainsexclude_ids+last_resort_min_age_seconds;earliest_known_resetdrops past instants.task_execution_service._call_agent_with_retriesandchat_execution_service._walk_chat_dispatchare the two dispatch loops.client_portal/service.pyAUTH/BILLING ladder.db.set_execution_subscription(SUB-004 re-attribution, one-column UPDATE, no schema change) + CAS kwarg onassign_subscription_to_agent.failure_classifier.is_container_faultkeeps a container-level 503 out of the walk.agent_runtime_stateregisters two EXEMPT dedupe keyspaces.Docs:
requirements/security.md§20.4,architecture/{execution,agent-runtime,backend}.md,feature-flows/subscription-auto-switch.md§#3470,docs/requirements/SUB-003…mdhistory,learnings.md(two entries).Test Plan
tests/unit/test_3470_subscription_walk.py— 51 tests: state machine, last-resort rung, key rung, commit/CAS/notifications, legacy mode, four end-to-end turns through the realexecute_task+ real walk,/chatre-issue, portal ladder, agent env layers / header / redaction / SecretStr, review-hardening pins.test_792/test_2789re-seamed onadvance_subscription_walk;test_2638's one-remediation test became "pre-dispatch switch feeds the walk and the origin is never retried"; pingpong / concurrency / auto-switch picks an alternative subscription without checking headroom — it can move agents onto a nearly-exhausted one #2409 / Default model claude-opus-5-5 fails on cached Claude Code 2.1.278, and the rejection is misclassified as auth (seat rotation + API-key fallback) #3012 / bug(execution): a chat turn continues whatever session was persisted last — after a long scheduled run it inherits that run's context, pays its compaction, and the execution record says nothing #2958 / env / reload-token suites green unmodified in contract.tests/unittier green locally (two pre-existingtest_2889collection failures are a localNo module named 'tests'cwd issue, unrelated).Not covered (stated)
Pull pilots: a Workspace turn on a piloted agent is claimed from the durable queue and the pull sink has no re-delivery — follow-up issue filed. Multi-worker commits are CAS-guarded; trials on different workers remain independent by design.
Fixes #3470
🤖 Generated with Claude Code