feat(web-search): pool Tavily API keys with failover and cooldowns - #278
Conversation
Tavily can now hold up to eight encrypted API keys. Keys are used in order or round-robin. A rejected key (401/403) or a quota-limited key (429/432/433) goes on an in-memory cooldown with exponential backoff, and the search moves on to the next key. When every key is cooling down, no request is sent and automatic routing falls back to the next provider. - Storage: the existing single-key slot becomes the pool's primary entry, so keys saved before this change keep working with no migration. The order, labels and strategy live in an encrypted index next to the keys. - IPC: the new webSearch:keyPool:* channels return only redacted state. Keys are write-only. - Settings: Tavily's setup dialog replaces the single key field with a pool editor for adding, removing, reordering (buttons or Alt+Arrow) and retrying keys, with live cooldown badges. - Subagent budget: every keyed request is charged through the existing beforeProviderAttempt fence. - Tests: rotation and failover run through the real Tavily adapter with a fake fetch. Also added: credential pool storage, service-level fallback, register-and-invoke IPC, and rendered list state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Important
Provider-wide key removal can leave orphaned encrypted API keys behind.
Reviewed changes This review covers the initial implementation of Tavily key pooling across encrypted storage, routing, IPC, Settings, and tests.
- Credential pool: Keeps the legacy key slot as
primaryand stores additional keys with encrypted metadata. - Failover and cooldowns: Adds ordered or round-robin selection, auth/quota backoff, and per-key request budget checks.
- IPC and Settings: Adds redacted pool operations and a key editor for adding, labeling, reordering, removing, and retrying keys.
- Coverage and docs: Adds storage, runtime, service, IPC, and rendered UI tests, plus the implementation plan and project memory.
GPT Luna | 𝕏
Hermes Review BotConfidence: 5 Engine: SummaryEnables multiple API keys for Tavily Web Search (up to 8 keys) with automated in-memory rotation, failover, and exponential backoff cooldowns. Keys are partitioned into dedicated encrypted secret slots with an encrypted index, preserving the single Confidence Score: 5/5Fully traced and verified across secret storage encryption, IPC message boundaries, rotation and cooldown algorithms, search failover orchestration, and UI accessibility. 📁 Important Files Changed
FindingsNo findings. 📊 Sequence DiagramsequenceDiagram
autonumber
participant Caller as Caller / Subagent
participant Service as WebSearchService
participant Tracker as WebSearchKeyPoolTracker
participant Adapter as TavilyAdapter
participant Tavily as Tavily API
Caller->>Service: search(query)
Service->>Tracker: order(keys, strategy)
Tracker-->>Service: [key1, key2]
Service->>Service: beforeKeyAttempt(key1)
Service->>Adapter: search(key1)
Adapter->>Tavily: POST /search (key1)
Tavily-->>Adapter: 429 Rate Limit
Adapter-->>Service: WebSearchError("quota")
Service->>Tracker: recordFailure(key1, "quota")
Service->>Service: beforeKeyAttempt(key2)
Service->>Adapter: search(key2)
Adapter->>Tavily: POST /search (key2)
Tavily-->>Adapter: 200 OK
Adapter-->>Service: WebSearchResultSet
Service->>Tracker: recordSuccess(key2)
Service-->>Caller: WebSearchResultSet
Machine-Readable Findings[]
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes This incremental review covers the commit landed since the prior pullfrog review, which resolves that review's orphaned-secret finding.
- Family-scoped secret deletion: Added
deleteSecretKeyFamilyto the encrypted secret map andsecrets.deleteKeyFamily, deleting a root ID, every colon-delimited descendant, and matching binding entries in one map write. - Index-independent provider removal: Provider-wide credential removal now sweeps the credential's secret family directly instead of walking the parsed pool index, erasing orphaned slots left by a corrupt index or an interrupted add/remove mutation.
- Regression coverage and docs: Added family-deletion and stale-guard tests to the secret-map and credential suites, updated the plan and project memory, and registered the new test in the CI registry (verified with
test:ci:registryand the policy check).
Verified locally: test:web-search passes (177 tests including the touched suites), npm run type-check passes, and the CI registry/policy validators accept the re-sorted registry. A namespace-collision audit of deleteKeyFamily found no surface that stores foreign credentials under a web-search:<provider>:api-key descendant: the family root is closed registry (tavily only via webSearchKeyPoolSupported), the web-search: prefix has exactly one producer, and custom model providers are custom:-rooted and removed via single-entry deleteKey.
GLM Flash | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes This incremental review covers the iOS lifecycle-test synchronization follow-up and its project note.
- Separated the competing event streams. Updated the removal lifecycle test to hold only
/streams/stream-recovery/events, run the denied/progress/eventsobserver independently, and wait for it to stop before sending the turn.
GPT Luna | 𝕏
# Conflicts: # .papercuts/troubleshooting.md # scripts/ci-test-registry.json
# Conflicts: # .papercuts/troubleshooting.md
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes This incremental review covers the two origin/main sync merges added since the prior Pullfrog review.
- Reconciled CI inventory. Kept the key-pool test suites registered while incorporating the current main test list.
- Synchronized project notes. Preserved Web Search pool documentation and troubleshooting entries alongside current main notes.
GPT Luna | 𝕏

Summary
Tavily can now hold up to 8 encrypted API keys. When one key is rejected or runs out of quota, the search moves to the next key.
quota(orauthif every key was rejected), so automatic routing falls back as usual.Source: pi-web-access #453 (idea only; no code copied).
Changes by surface
main/services/web-search-credential-core.ts)primaryentry. Keys saved before this change keep working with no migration.…:pool:<id>slots with the same endpoint binding.…:pool-indexstores the order, labels and strategy.main/services/web-search-key-pool-core.ts,web-search.ts)runWithWebSearchKeyPoolplus an in-memoryWebSearchKeyPoolTrackerfor cooldowns and the round-robin cursor.beforeProviderAttemptbudget check runs once per keyed request.main/handlers/web-search-key-pool.ts)webSearch:keyPool:get|add|remove|reorder|setStrategy|resetCooldownchannels.webSearch:removeCredentialnow also clears pool cooldowns.renderer/components/settings/web-search-key-pool.tsx)docs/settings-design-system.mdand the AGENTS UI rules: semantic tokens, the shared Button, and focus-visible rings kept.Badgegains awarningcolor built from the existing status-warning tokens.docs/plans/web-search-key-pool-plan.md, with an index row indocs/plans/README.md..memory/web-search-key-pool.mdand entries in.papercuts/troubleshooting.md.This does not change onboarding, since the feature is an optional extra for an existing provider, or the Aiden Remote contract, so there are no mobile changes.
Tests run
npm run test:web-search: 175 pass. It includes these new files, now registered in the script:main/services/web-search-key-pool-core.test.tsmain/handlers/web-search-key-pool.test.tsrenderer/components/settings/web-search-key-pool.test.tsxnpm run test:settings-design: 60 pass.npm run type-check: pass.eslinton the touched files: pass.What the tests cover:
fetchand check which bearer key each request carried:No live provider requests were made.
Follow-ups
packages/cli, which still supports one key per provider.🤖 Generated with Claude Code