Skip to content

[DRAFT] fix(credential-pool): harden multi-account pool against rate limits and subagent stampede - #5374

Closed
MestreY0d4-Uninter wants to merge 2 commits into
NousResearch:mainfrom
MestreY0d4-Uninter:fix/credential-pool-hardening
Closed

[DRAFT] fix(credential-pool): harden multi-account pool against rate limits and subagent stampede#5374
MestreY0d4-Uninter wants to merge 2 commits into
NousResearch:mainfrom
MestreY0d4-Uninter:fix/credential-pool-hardening

Conversation

@MestreY0d4-Uninter

Copy link
Copy Markdown
Contributor

Summary

With 10+ accounts in the credential pool, the chance of all being rate-limited simultaneously is very low. However, the current code was not reaching those accounts in practice. This PR fixes the structural issues that caused the pool to underperform.

Root causes addressed

  1. Subagents weren't seeing the pool — they inherited a single fixed credential from the parent. With 10 accounts available, a subagent knew only 1. If that account hit 429, it got stuck.

  2. Parallel subagent stampede — batch delegation of 3 subagents caused all 3 to acquire the same "current" credential simultaneously, hitting 429 on one account while 9 others were idle.

  3. Blind TTL cooldowns — on 429, credentials were blocked for 1 hour by default, even when the provider header said "retry in 15 seconds".

  4. Fallback triggered too earlyfallback_providers could activate before the pool's other credentials were tried.

  5. No health-aware selection — a credential with 5 consecutive 429s could be selected before a clean one.

What changed

Layer 1 — PooledCredential hardening + Retry-After extraction

  • New fields: cooldown_until, last_retry_after_seconds, last_error_category, last_success_at, consecutive_failures, consecutive_429s, transient_error_count, structural_error_count
  • _entry_health_score(): scores entries by failure history, prefers healthier entries
  • _available_entries(): checks cooldown_until (from Retry-After), then last_error_reset_at, then fixed TTL
  • _extract_retry_after_seconds(): reads Retry-After, x-ratelimit-reset, etc. from API error headers
  • _classify_api_failure(): categorizes errors into stable categories (transient_rate_limit, quota_exhausted, auth_structural, etc.)
  • retry_after_seconds + error_category propagated through the full rotation chain

Layer 2 — Subagent pool participation

  • _resolve_child_credential_pool(): same provider → share parent pool (synchronized state); different provider → load its own pool
  • Children now participate in pool rotation on 429/402 instead of using a fixed credential

Layer 3 — Lease-based concurrency control

  • acquire_lease() / release_lease() / active_lease_count() on CredentialPool
  • Soft cap (DEFAULT_MAX_CONCURRENT_PER_CREDENTIAL = 2) — distributes parallel subagents across accounts
  • Lease acquired before run_conversation(), released in finally (even on error/interrupt)
  • acquire_lease() selects least-leased + healthiest entry, then rebinds child via _swap_credential()

Layer 4 — Task-tier delegation profiles

  • SUPPORTED_TIERS: light, heavy, review, planning, research
  • resolve_tier_config(cfg, tier): merges flat config with tier overrides
  • Floor guardrails: review/planninghigh, heavy/researchmedium
  • tier field in delegate_task schema (top-level and per-task in batch)
  • override_reasoning_effort in _build_child_agent()
  • delegation.reasoning_effort added to config defaults

Layer 5 — Fallback precedence

  • _should_defer_fallback_to_credential_pool(): returns True when pool still has available credentials
  • Wired into the rate-limit error path — fallback only fires after the pool is genuinely exhausted

Validation

python3 -m py_compile agent/credential_pool.py tools/delegate_tool.py run_agent.py hermes_cli/config.py
python -m pytest tests/test_credential_pool.py tests/test_credential_pool_routing.py tests/tools/test_delegate.py -o 'addopts=' -q

Result: 89 passed

Notes

  • The pool sharing design is backward-compatible: when no pool exists, children inherit the parent credential as before (graceful fallback).
  • Lease management uses a soft cap — never blocks, always returns the least-leased entry.
  • cooldown_until from Retry-After headers takes priority over fixed TTL and over last_error_reset_at.

Hermes Audit added 2 commits April 6, 2026 01:27
…nd subagent stampede

Layer 1 — PooledCredential + dynamic cooldown + health scoring
- Add fields: cooldown_until, last_retry_after_seconds, last_error_category,
  last_success_at, consecutive_failures, consecutive_429s, transient_error_count,
  structural_error_count
- Add helpers: _is_structural_error(), _is_transient_error()
- Add _entry_health_score() to CredentialPool for smart entry selection
- Update _mark_exhausted() to accept retry_after_seconds and error_category
- Update _available_entries() to check cooldown_until, last_error_reset_at, then fixed TTL
- Update _select_unlocked() to sort by health score
- Update mark_exhausted_and_rotate() signature (retry_after_seconds, error_category)
- Add size() method
- Add acquire_lease(), release_lease(), active_lease_count() methods

Layer 1 — run_agent Retry-After extraction + failure classification
- Add _extract_retry_after_seconds(): reads Retry-After, x-ratelimit-reset, etc.
- Add _classify_api_failure(): categorizes errors into stable categories
- Add _should_defer_fallback_to_credential_pool(): defer fallback when pool available
- Update _recover_with_credential_pool() to propagate retry_after_seconds + error_category
- Wire _should_defer_fallback_to_credential_pool() into the rate-limit error path

Layer 2 — Subagent pool participation
- Add _resolve_child_credential_pool() in delegate_tool.py
- Call it in _build_child_agent() so children share the parent pool

Layer 3 — Lease-based concurrency control
- In _run_single_child(): acquire_lease() before run_conversation()
- Release in finally block, bind child to leased credential via _swap_credential()

Layer 4 — Task-tier delegation profiles
- Add SUPPORTED_TIERS and resolve_tier_config() to delegate_tool.py
- Add tier param to delegate_task() and per-task in batch schema
- Add override_reasoning_effort to _build_child_agent()
- Add reasoning_effort to _resolve_delegation_credentials() all return paths
- Add delegation.reasoning_effort default to hermes_cli/config.py
- Tier floor guardrails: review/planning >= high, heavy/research >= medium

Layer 5 — Fallback precedence
- Wire _should_defer_fallback_to_credential_pool() to replace inline pool check
…ssing delegation tests

- Replace string-matching is_rate_limited with failure_category from
  _classify_api_failure (transient_rate_limit, quota_exhausted)
- Add failure_category to error logging output
- Add TestDelegationReasoningEffort (2 tests)
- Add TestChildCredentialPoolResolution (7 tests)
- Add TestChildCredentialLeasing (2 tests)
- Add TestTierResolution (9 tests)
@MestreY0d4-Uninter

Copy link
Copy Markdown
Contributor Author

Closing this draft in favor of smaller validated slices that were extracted and revalidated on clean worktrees against current origin/main:\n- #5576 child pool participation\n- #5580 child pool leasing\n- #5583 tiered delegation profiles\n\nThe remaining unsplit portion now overlaps too much with active upstream work (notably #5441 and issue threads #5449 / #5570) to justify keeping this large draft open in its current form. Keeping the branch as historical reference is fine, but the PR itself is now more confusing than helpful.

@MestreY0d4-Uninter
MestreY0d4-Uninter deleted the fix/credential-pool-hardening branch April 27, 2026 01:42
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.

1 participant