## 问题 `runWithRetry()` 只在 `await sleep(delay)` 前检查注入的 `RetryBudget.isExhausted()`。预算在退避等待期间耗尽时,下一轮仍然先执行 provider operation,再检查预算;如果这次额外调用成功,还会返回 `stoppedByBudget: false`。 这违反了 RetryBudget 对同一 Provider 重试次数的限制。公开导出的 helper 和 ProviderRouter 的 `retry.budget` 接线均可复现。 ## 确定性复现 基线:main `a6da0059ecafe4426c9a841ec59215655ebcabd1`。 注入假时钟和 sleep,预算截止点为 100ms,首轮失败后等待 200ms: ```ts let now = 0 let calls = 0 const result = await runWithRetry(async () => { calls += 1 return { ok: false, error: { code: 'TIMEOUT', message: 'timeout' } } }, { maxAttempts: 3, baseDelayMs: 200, jitter: false, sleep: async (ms) => { now += ms }, budget: { isExhausted: () => now >= 100 }, }) ``` 实际:调用 2 次;第二次发生在预算已耗尽后。 预期:调用 1 次,返回首轮错误及 `attempts: 1`、`stoppedByBudget: true`。 ## 验证 Bun 1.4.2 / Windows 11 (10.0.26200)。 新回归先在未修复生产代码上执行: ```text bun test packages/shared/src/providers/resilience.test.ts → 25 pass / 4 fail ``` 四项失败覆盖 TIMEOUT、RATE_LIMITED、第二次退避耗尽预算,以及 ProviderRouter 的实际同源重试路径;不访问外部 API。 ## 修复方向与边界 在退避返回后、下一次 operation 前重新检查预算,保留最后错误和已完成尝试数。此预算 hook 约束同一 Provider 的重试,保持现有 fallback 策略;不扩大为整个路由/Agent 运行的终止策略,也不提前中断 sleep。 当前开放 PR #242 修改同文件的 cache key 区域,#281 修改错误分类表,#274 修改路由缓存门禁;均未处理退避后的预算判定。 Codex 在用户授权下准备报告和修复。
Activity
huyengiang101086-spec commentedon Oct 12, 2026
/claim
github-actions commentedon Oct 12, 2026
@huyengiang101086-spec 已认领该 Issue,并已自动设为 Assignee。提交 PR 时请在描述中写
Closes #320。fix(providers): recheck retry budget after backoff (#321)