Skip to content

fix(sub-003): a turn walks every subscription and the API key before it fails (#3470) - #3486

Merged
vybe merged 1 commit into
devfrom
feature/3470-subscription-walk-pool
Oct 9, 2026
Merged

vybe merged 1 commit into
devfrom
feature/3470-subscription-walk-pool

Conversation

@vybe

@vybe vybe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The turn walks the whole pool before it fails. SUB-003 used to remediate 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 API key — and the same execution_id is re-issued with it as a request-scoped auth_override the 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.
  • Part 2 — the API key is a rung, not a reassignment. 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 whose use_platform_api_key is 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.
  • Honest ending. Only when every credential refused does the Workspace / /chat show 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 on TaskExecutionResult.subscription_switch.attempts and the auto_retry audit 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 (no X-Trinity-Auth-Override echo) 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 restart or a Redis outage and misattribute key refusals; the override is stateless and exact.

Changes

Agent server (docker/base-image/agent_server/): models.AuthOverride (SecretStr fields) on ParallelTaskRequest / ChatRequest; execution_env.build_execution_env(…, drop=) + auth_override_env_layers; both spawn sites apply it; routers/chat.py stamps X-Trinity-Auth-Override on 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 gains exclude_ids + last_resort_min_age_seconds; earliest_known_reset drops past instants. task_execution_service._call_agent_with_retries and chat_execution_service._walk_chat_dispatch are the two dispatch loops. client_portal/service.py AUTH/BILLING ladder. db.set_execution_subscription (SUB-004 re-attribution, one-column UPDATE, no schema change) + CAS kwarg on assign_subscription_to_agent. failure_classifier.is_container_fault keeps a container-level 503 out of the walk. agent_runtime_state registers 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…md history, learnings.md (two entries).

Test Plan

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

…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 trinity-ability left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train/20261009-2324-b (#3504)

@vybe
vybe merged commit 676898e into dev Oct 9, 2026
26 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants