diff --git a/release-notes/v0.5.20.md b/release-notes/v0.5.20.md index 4efbe89a..a6019b47 100644 --- a/release-notes/v0.5.20.md +++ b/release-notes/v0.5.20.md @@ -1,6 +1,6 @@ # v0.5.20 -> 子代理限流器从单门改双门:cap 放宽 + 连续 FAIL 流式阻断,正常多步工作不再被误杀。 +> 重试链路两处去噪:子代理限流器从单门改双门,正常多步工作不再被误杀;HTTP 重试期间相同错误不再每次都堆到对话气泡。 ## 修复 @@ -11,22 +11,36 @@ - 错误文案区分 `cappedReason: 'total' | 'streak' | 'none'`,用户能看到具体哪个门触发。 - 环境变量出口保留:`CLAUDE_CODE_AGENT_LIMIT_=N` 调高 total cap、`CLAUDE_CODE_AGENT_LIMITER_OFF=1` 整体禁用。 +- **HTTP 重试期间相同错误不再刷屏对话**(#79)。原 `withRetry` 默认 `maxRetries=10`,每次失败只要错误是 `APIError` 就把 `SystemAPIErrorMessage` yield 到对话流。一次 flaky 上游恢复需要 2-3 次重试时,对话气泡里就会堆出 2-3 条一模一样的错误(用户原话「100% 显示出来」)。本版加同错误去重门控: + - **首次出现的错误 key**:立即 yield(携带新信息)。 + - **连续相同错误**:沉默到累计 ≥ 3 次(默认)才再 yield。 + - **任何 429 rate-limit**:始终 yield —— wait-time 信息是它存在的意义。 + - **错误 key**:用 `Error.name + APIError.status + Error.message` 拼接,401 与 503 即使 message 相同也不会折叠。 + - **State 是 generator 闭包局部变量**,跨 withRetry 调用不泄露;persistent 模式 heartbeat 复用一次性算定的结果,避免长 persistent 等待重复增计数。 + - 环境变量 `CLAUDE_CODE_RETRY_REPORT_AFTER=N` 可调阈值(默认 3)。 + - 效果:一段 3 次相同错误的重试现在显示 2 个气泡而非 3 个;更长 streak 压缩更明显,但用户始终能看到通话还在重试。 + ## 测试 - `src/tools/AgentTool/invocationLimiter.test.ts`:28 cases / 153 expects 全过。 - 14 个原测试按新 cap=12 适配 - 5 个 streak 行为新测试:3 FAIL trip / PASS clears / UNKNOWN soft-PASS / per-agent-type 隔离 / 12 PASS 触 total cap not streak - 9 个 parseVerdict 测试:empty / 标准 VERDICT 行 / FAIL 同义词 / APPROVE / 专家技能同义词 / 无 verdict / markdown 容忍 / 长 input tail -- 验证子代理独立 PASS,含 8 个对抗性场景:5 PR PASS 序列、verifier loop 模拟、PASS 中断 streak、跨 type 隔离、真实子代理输出、UNKNOWN soft-PASS、orphan FAIL、12 PASS 后 total cap。 +- `src/services/api/withRetry.test.ts`:4 cases,3 个新测试覆盖 same-error 抑制、新 key 立即 yield、env override 提高阈值。 +- 两个 PR 各自的 verification 子代理均独立 PASS,含状态隔离、heartbeat 放大、UNKNOWN soft-PASS 攻击面、throw/abort 场景 streak 残留等对抗性 probe。 ## 设计审查 -- **UNKNOWN 当 soft-PASS 的攻击面**:恶意子代理可省略 VERDICT 行避开 streak,但 total cap=12 仍兜底;且子代理 prompt 由调用方完全控制,不是普通输入面。 -- **throw / abort 场景的 streak 状态**:`noteInvocation` 已增计数但 `noteOutcome` 不跑——streak 保持原值不偏移,下次调用基于既有 streak 起步,PASS 仍能清零。 -- **接入点对称性**:`AgentTool.tsx` 中 `noteOutcome` 与 `noteInvocation` 用完全相同的 guard `(!toolUseContext.agentId && isBuiltInAgent(selectedAgent) && !isLimiterDisabled())`,避免 streak/total 计数不同步。 +- **UNKNOWN 当 soft-PASS 的攻击面**(#78):恶意子代理可省略 VERDICT 行避开 streak,但 total cap=12 仍兜底;且子代理 prompt 由调用方完全控制,不是普通输入面。 +- **throw / abort 场景的 streak 状态**(#78):`noteInvocation` 已增计数但 `noteOutcome` 不跑——streak 保持原值不偏移,下次调用基于既有 streak 起步,PASS 仍能清零。 +- **接入点对称性**(#78):`AgentTool.tsx` 中 `noteOutcome` 与 `noteInvocation` 用完全相同的 guard `(!toolUseContext.agentId && isBuiltInAgent(selectedAgent) && !isLimiterDisabled())`,避免 streak/total 计数不同步。 +- **非 APIError 不进对话流**(#79):`Stream ended without receiving any events` 这类普通 Error 仍走外层 `CannotRetryError` 路径,本 PR 不扩大 `SystemAPIErrorMessage` 的类型契约(它要求 `error: APIError`)。改契约会扩散到所有消费者,超出本 PR 范围。 ## 范围 - `src/tools/AgentTool/invocationLimiter.ts`:核心改造,加 streak 状态、noteOutcome、parseVerdict、双门判定。 - `src/tools/AgentTool/AgentTool.tsx`:completed 分支加 `noteOutcome` 调用。 - `src/tools/AgentTool/invocationLimiter.test.ts`:14→28 测试。 +- `src/services/api/withRetry.ts`:加 `DEFAULT_RETRY_REPORT_THRESHOLD` / `errorIdentityKey` / `shouldReportRetry`,2 处 yield 改 guard。 +- `src/services/api/withRetry.test.ts`:1→4 测试。 +