fix(llm): drain delegated retry recovery

This commit is contained in:
Turtle
2026-07-25 14:23:55 +08:00
parent da54a61639
commit 38ce422f71
7 changed files with 94 additions and 64 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-24-provider-retry-policies.md: 0b9456eb1563bfcbfa06d93403fd68124ed1f707
2026-07-24-provider-retry-policies.zh.md: d3ef90ec739eec7300b7d0bb526cb5de958ed93f
2026-07-24-provider-retry-policies.md: 3799dbee9658c883f4084ac9c792d4552cc8e2fb
2026-07-24-provider-retry-policies.zh.md: 8024ea707343686d2d1b1351cd7c81ecb8767b85

View File

@@ -36,7 +36,7 @@ providers:
The listener reads the provider from the durable `request/header` in force when the failed step closed, excluding later recovery mutations, but never re-resolves policy from the mutable provider registry. Normal mode retains the bounded transient behavior: it retries configured codes up to `maxRetries`, counts retries scheduled by the same provider policy in the current consecutive failure sequence, and otherwise delegates.
Always mode asks downstream recovery first so a specialized policy such as context-overflow compaction can make progress. A downstream retry wins. A downstream failure decision or thrown recovery error falls back to an unbounded retry of the same provider request; the thrown error is logged. Success, turn cancellation, and plugin disposal are the only termination paths.
Always mode asks downstream recovery first so a specialized policy such as context-overflow compaction can make progress. A downstream retry wins. A downstream failure decision or thrown recovery error falls back to an unbounded retry of the same provider request; the thrown error is logged. The retry listener owns and drains delegated recovery before cancellation or plugin disposal can finish, then applies the abort instead of a late downstream decision. Success, turn cancellation, and plugin disposal are the only termination paths.
Both modes use exponential local delays from `initialDelayMs` to `maxDelayMs`. `jitterRatio` multiplies each target by a uniform sample in `[1 - jitterRatio, 1 + jitterRatio]`, then applies the cap. A positive provider `Retry-After` within the cap remains exact and unjittered. An over-cap provider delay makes normal mode delegate; always mode retains its guarantee by using the configured local backoff.
@@ -56,7 +56,7 @@ Each scheduled retry appends a non-surface `llm/retry` event with the failed pro
## Verification
Adapter tests validate nested policies at provider load, prove registration captures configured and default policies, and retain the serving policy across in-flight route replacement. Unit and real-Loader composition tests select policies from the failed request's serving registration, exercise always mode beyond the normal budget, pin jitter and delay caps, prove downstream recovery ordering, prove cancellation interrupts stalled downstream recovery, and prove cancellation and disposal stop active backoff waits. Request-level coverage compares the complete messages of failed and retried attempts and rejects both provider error text and discarded partial output. JSONL and SQLite tests round-trip an always event without `Infinity`; invariant tests bind its provider to the request header and its retry number to the active provider policy; ACP and TUI tests render finite and infinite limits.
Adapter tests validate nested policies at provider load, prove registration captures configured and default policies, and retain the serving policy across in-flight route replacement. Unit and real-Loader composition tests select policies from the failed request's serving registration, exercise always mode beyond the normal budget, pin jitter and delay caps, prove downstream recovery ordering, prove cancellation and disposal drain delegated recovery before reaching quiescence, and prove both abort active backoff waits. Request-level coverage compares the complete messages of failed and retried attempts and rejects both provider error text and discarded partial output. JSONL and SQLite tests round-trip an always event without `Infinity`; invariant tests bind its provider to the request header and its retry number to the active provider policy; TUI tests render finite and infinite limits.
## Consequences

View File

@@ -36,7 +36,7 @@ providers:
监听器从失败步骤关闭时生效的持久 `request/header` 读取提供方后续恢复产生的改动不参与选择但绝不会从可变的提供方注册表重新解析策略。normal 模式保留有界瞬态错误处理行为:它重试配置的错误代码,次数不超过 `maxRetries`;在当前连续失败序列中,同一提供方策略安排的重试都计入次数;其他情况委托后续处理。
always 模式先请求下游恢复使上下文溢出压缩compaction之类的专用策略有机会取得进展。下游若决定重试则以该决定为准。下游若决定失败或恢复过程抛出错误则回退为无界重试同一提供方请求抛出的错误会写入日志。成功、轮次取消插件 dispose资源释放是仅有的终止路径。
always 模式先请求下游恢复使上下文溢出压缩compaction之类的专用策略有机会取得进展。下游若决定重试则以该决定为准。下游若决定失败或恢复过程抛出错误则回退为无界重试同一提供方请求抛出的错误会写入日志。重试监听器会持有并排空已委托的恢复,轮次取消插件 dispose资源释放只能在其结束后完成;随后监听器会应用取消,而不会采用迟到的下游决定。成功、轮次取消和插件 dispose 是仅有的终止路径。
两种模式的本地延迟都按指数增长,从 `initialDelayMs` 增至 `maxDelayMs``jitterRatio``[1 - jitterRatio, 1 + jitterRatio]` 区间内的均匀随机样本乘以每次目标值,再应用上限。提供方给出的正数 `Retry-After` 若未超过上限则保持精确且不加抖动。若提供方延迟超过上限normal 模式会委托后续处理always 模式则改用配置的本地退避,以维持无限重试保证。
@@ -56,7 +56,7 @@ always 模式先请求下游恢复使上下文溢出压缩compaction
## 验证
适配器测试会在提供方加载时校验嵌套策略,证明注册流程会捕获已配置策略和默认策略,并证明请求进行期间替换路由后仍会保留实际提供服务的策略。单元测试与真实 Loader 组合测试根据失败请求实际使用的注册项选择策略、验证 always 模式可越过 normal 预算、固定抖动和延迟上限、证明下游恢复顺序、证明取消会中断停滞的下游恢复,并证明取消与 dispose 会停止正在进行的退避等待。请求级覆盖会比较失败尝试与重试尝试的完整消息并排除提供方错误文本和丢弃的部分输出。JSONL 与 SQLite 测试会往返读写不含 `Infinity` 的 always 事件;不变式测试会将事件中的提供方绑定到请求头,并将重试编号绑定到活跃的提供方策略;ACP 与 TUI 测试会分别渲染有限和无限上限。
适配器测试会在提供方加载时校验嵌套策略,证明注册流程会捕获已配置策略和默认策略,并证明请求进行期间替换路由后仍会保留实际提供服务的策略。单元测试与真实 Loader 组合测试根据失败请求实际使用的注册项选择策略、验证 always 模式可越过 normal 预算、固定抖动和延迟上限、证明下游恢复顺序、证明取消与 dispose 会在达到静止状态前排空已委托的恢复,并证明二者都会停止正在进行的退避等待。请求级覆盖会比较失败尝试与重试尝试的完整消息并排除提供方错误文本和丢弃的部分输出。JSONL 与 SQLite 测试会往返读写不含 `Infinity` 的 always 事件不变式测试会将事件中的提供方绑定到请求头并将重试编号绑定到活跃的提供方策略TUI 测试会渲染有限和无限上限。
## 后果