fix(core): prevent Anthropic streams from hanging indefinitely - #9945
Conversation
The OpenAI wire wraps its stream in an inactivity watchdog plus a non-resetting lifetime cap, while the Anthropic wire had neither: a stream that returns 200 and then goes silent — or drip-feeds low-content thinking_delta frames that keep resetting any idle-only timer — hangs the CLI until the process is killed. Wire the same guards into the Anthropic generator so a stalled stream aborts with a retryable ETIMEDOUT instead of hanging, and the two wires stop differing on whether a stalled stream is recoverable. The guard mechanics, error classes, and timeout resolvers move to a shared stream-guards module used by both wires; the OpenAI pipeline re-exports the error classes so existing imports keep working. Also rename six stale popPartialIfPushed comment references in geminiChat.ts to the real popPendingPartialAssistantTurn method. Issue #9005 findings 4 and 6.
|
|
|
Thanks for the PR! Template looks good ✓ Problem: real and documented — issue #9005 (P1, type/bug) catalogs the gap with pinned line references: the OpenAI wire has Direction: aligned. Stream-hang safety is an already-settled project decision (the OpenAI wire proves it), and this is parity, not new direction. The reference agent ships the same idea — claude-code's CHANGELOG records its streaming idle watchdog now on by default for all providers (abort + retry after 5 minutes of silence), plus a string of stream-idle-timeout fixes across Bedrock/Vertex/gateway deployments. Size: core paths touched — 636 production lines (additions + deletions) vs 327 test lines, 0 generated/schema. That clears the 500-line awareness mark, but the author is a maintainer and the bulk is the guard machinery moving verbatim out of Approach: scope feels right. Extracting the guard mechanics into a shared Risk: Stage 1e high-risk paths matched — Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:真实且有据可查 —— issue #9005(P1、type/bug)以固定行号引用记录了这一缺口:OpenAI 链路有 方向:对齐。流挂死防护是项目已定下的决策(OpenAI 链路即是证明),本 PR 是补齐对等,不是新方向。参考产品也有同样思路 —— claude-code 的 CHANGELOG 记录其流式 idle 看门狗已对所有提供商默认开启(静默 5 分钟后中止并重试),另有多条 Bedrock/Vertex/网关部署下的流 idle 超时修复。 规模:触及核心路径 —— 636 行生产代码(增 + 删)对 327 行测试代码,0 行生成/schema。超过 500 行关注线,但作者是维护者,且大头是看门狗机制从 方案:范围合理。把看门狗机制抽入共享的 风险:Stage 1e 高风险路径命中 —— 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
|
Code review — no blocking issues found. I wrote my independent proposal before reading the diff: extract Things I verified against the code rather than taking from the PR description:
The five new tests mirror the OpenAI gated-stream suite (fake timers, sentinel races instead of bare awaits): idle fire, shared default, healthy-stream passthrough, lifetime cap under a drip that resets the idle timer, and CI test evidence — fetched via API for the reviewed commit; nothing was run locally (per policy, PR code is never executed by the triage agent). The main unit suite is still running; macOS/Windows matrix jobs are gated behind the in-progress CI run. Green so far: Security Checks (Secret scan, Dependency CVE audit) and both Desktop Shell jobs. No red checks. The table below is updated automatically once CI settles. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle the remaining gap: 中文说明代码审查 —— 未发现阻塞问题。 读 diff 之前我先写了独立方案:把 以下是我对照代码核实、而非取自 PR 描述的内容:
5 个新测试与 OpenAI 的门控流套件同构(假定时器、哨兵竞速取代裸 await):idle 触发、共享默认值、健康流不受干扰、滴灌重置 idle 计时器时由总时长封顶、 CI 测试证据 —— 通过 API 获取审查提交的检查结果;按规则 triage agent 从不执行 PR 代码。主单测套件仍在运行;macOS/Windows 矩阵任务被进行中的 CI 运行所门控。目前绿色:Security Checks(Secret scan、Dependency CVE audit)与两个 Desktop Shell 任务。无红色检查。下表在 CI 结束后自动更新。 沙箱验证可以补齐剩余缺口: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean review with every claim verified against the code; docking the last point only because the unit suite is still in flight and the live-connection behavior rests on a mocked event source. Stepping back: this is exactly the kind of PR the gate should wave through. The problem is real and documented (#9005 finding 4), the fix reuses machinery the project already validated on the OpenAI wire instead of inventing anything, and the extraction is verbatim — I diffed the deleted pipeline code against the new shared module, and the only delta is the chunk-type generalization. Every load-bearing claim checked out against the base code: the abort handle is the controller the SDK request runs under, If I had to maintain this in six months I'd thank the author: one shared guard module with real docblocks, instead of two watchdog copies drifting apart — which is precisely how finding 4 happened. The five new tests pin the behavior the same way the OpenAI suite does, and there is nothing in the diff beyond what the stated goal needs. Two caveats, both non-blocking: the unit suite for the reviewed commit is still in progress (the approval below lands only if it comes back green), and the tests drive a mocked event source — the real-connection behavior is what the Verdict: approve. CI is still running, so approval is deferred until CI lands green on 中文说明置信度:4/5 —— 审查干净,所有论断均已对照代码核实;扣掉一分仅因为单测套件仍在进行,且真实连接行为目前只由 mock 事件源覆盖。 退一步看:这正是门禁应当放行的那类 PR。问题真实且有据(#9005 发现 4),修复复用了项目已在 OpenAI 链路上验证过的机制而非新造轮子,抽取是逐字搬运 —— 我对比了 pipeline 中删除的代码与新共享模块,唯一差异是对 chunk 类型的泛型化。所有关键论断都经基础代码核实:中止句柄正是 SDK 请求所用的控制器, 六个月后维护这段代码我会感谢作者:一个带真实文档注释的共享看门狗模块,而不是两份各自漂移的副本 —— 发现 4 正是那么造成的。5 个新测试以与 OpenAI 套件相同的方式钉住行为,diff 中没有任何超出既定目标的内容。 两点保留,均不阻塞:审查提交的单测套件仍在进行(下面的批准仅在它变绿后落地);测试驱动的是 mock 事件源 —— 真实连接行为正是审查评论中点名的 结论:批准。CI 仍在运行,批准将推迟到 CI 在 — Qwen Code · qwen3.8-max Reviewed at |
doudouOUC
left a comment
There was a problem hiding this comment.
Code Review
Verdict: COMMENT — no blockings issues found.
What was verified against the code
-
Extraction is a pure move. The deleted pipeline code and the new
stream-guards.tsmatch line for line; the only delta is the type parameter generalization toAsyncIterable<T>. The error classes' only consumers live inside pipeline.ts itself, and the re-export keepsinstanceofidentity intact. -
perRequestAc.abort()is the correct handle. It's the child controller the SDKmessages.createcall runs under — aborting it releases the actual connection, identical to the OpenAI wire. -
ETIMEDOUTis classified as retryable in both lists. Confirmed inretryErrorClassification.ts(line 232) andstream-transport-retry.ts(line 14). No new retry policy is introduced. -
Error propagation through the generator chain is correct.
processStream'scapturedStreamcatches the watchdog error, setsupstreamStreamFailed, and the code after the loop re-throws it.processStreamWithEmptyFallbackdoes not swallow watchdog errors — they propagate through thefor awaitloop before the fallback probe is reached. -
redactStreamErrorspreserves error properties via a prototype-preserving clone;code,name, and the diagnostic fields survive redaction. -
<= 0disables both guards independently, checked in both the caller (anthropicContentGenerator.ts) andwithStreamGuardsitself. -
The five new tests mirror the OpenAI pipeline's guard suite (gated event stream, fake timers, sentinel races instead of bare awaits).
-
Finding 6 is accurate. Six stale
popPartialIfPushedcomment references renamed topopPendingPartialAssistantTurn— comment-only, no behavior change. -
Docblock updates are complete.
contentGenerator.tsandsettingsSchema.tsaccurately reflect the new scope.
CI note
The triage bot already flagged the CI failures (Post Coverage Comment and Test on ubuntu-latest) — these are pre-existing CI infrastructure issues, not related to this PR's code changes.
中文说明
代码审查
结论:COMMENT — 未发现阻塞问题。
经代码核实的内容
-
抽取是纯搬运。 pipeline 中被删除的代码与新
stream-guards.ts逐行一致,唯一差异是泛型化AsyncIterable<T>。两个错误类的消费者全在 pipeline.ts 内部,re-export 保证instanceof同一性不变。 -
perRequestAc.abort()是正确的句柄。 它就是 SDKmessages.create所用的控制器,中止它即释放真实连接,与 OpenAI 链路一致。 -
ETIMEDOUT在两个可重试清单中。 已在retryErrorClassification.ts(第 232 行)和stream-transport-retry.ts(第 14 行)确认。未引入任何新重试策略。 -
错误传播链正确。
processStream的capturedStream捕获看门狗错误,设置upstreamStreamFailed,循环后的代码重新抛出。processStreamWithEmptyFallback不会吞掉看门狗错误——它们在for await循环中传播,不会到达回退探测。 -
redactStreamErrors保留错误属性,通过原型保留克隆;code、name和诊断字段均存活。 -
<= 0独立禁用两个看门狗,在调用方和withStreamGuards自身都有检查。 -
5 个新测试与 OpenAI pipeline 的看门狗套件同构(门控事件流、假定时器、哨兵竞速)。
-
发现 6 准确。 6 处过时的
popPartialIfPushed注释引用已改为popPendingPartialAssistantTurn——纯注释,无行为变化。 -
文档注释更新完整。
contentGenerator.ts和settingsSchema.ts准确反映了新范围。
CI 说明
triage bot 已标记 CI 失败(Post Coverage Comment 和 Test on ubuntu-latest)——这些是已有的 CI 基础设施问题,与本 PR 的代码改动无关。
— Qwen Code @ a3f3e37
|
Closeout update: the exact-head Ubuntu Test job had no retrievable failed log via |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Test Plan (not a blocker): src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
Test Plan(非阻断):src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.22.0)
…bort The shared stream guard aborts the per-request controller the moment the source stream drains, but the Anthropic empty-stream fallback probe runs after that drain and was reusing the now-aborted signal, so the SDK rejected it with a spurious AbortError instead of surfacing the provider's real error (e.g. a 402 credit-balance response). Pass the caller's signal into the fallback and derive a fresh short-lived child for the probe, aborting it once the probe settles to release the SDK's abort listener.
Propagate the streamIdleTimeoutMs description change (OpenAI-compatible and Anthropic models) to the generated settings.schema.json via npm run generate:settings-schema, keeping the CI lockstep check green.
Anthropic twin of the OpenAI pipeline.test.ts case: a user abort landing while the idle-watchdog timer is pending must surface as a non-retryable AbortError, not the retryable ETIMEDOUT the watchdog would otherwise raise (ETIMEDOUT is retryable, so the retry loop would resume the cancelled turn). Pins the parentSignal argument threaded into withStreamGuards — replacing it with undefined leaves this test red.
…omments The rename sweep to popPendingPartialAssistantTurn missed four comment references in geminiChat.test.ts (lines ~7737, ~10919, ~10980, ~13284). Update them so grep for the new method name reaches every site describing the rollback mechanism.
Closeout round — 4 findings handled, 2 deferredPushed Handled
Verification: Deferred to next round: settings.md prose update ( |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 5": executing the new vitest suites in packages/core (worktree has no node_modules/dist; npm ci + full workspace build not run).
Test Plan (not a blocker): src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts:5071 — [review] gatedEventStream() is a near-copy of pipeline.test.ts's gatedStream() helper
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 5":executing the new vitest suites in packages/core (worktree has no node_modules/dist; npm ci + full workspace build not run)。
Test Plan(非阻断):src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
The docblock and regenerated settings schema already declare Anthropic coverage; settings.md still scoped the guards to OpenAI-compatible providers and claimed neither guard exists on Anthropic. Update the heading and scope the remaining gap to the Gemini generator only.
…kage Close the round-2 review gaps on the Anthropic wire: - Honour QWEN_STREAM_MAX_LIFETIME_MS and the shared default lifetime cap through the Anthropic wiring (both previously untested; mutation-verified). - Stub QWEN_STREAM_* envs in the fallback-probe regression so ambient disable values cannot vacate it. - Pin the probe's caller-signal linkage (mid-probe cancel surfaces AbortError) and its post-probe abort cleanup.
|
Round-2 closeout: all six remaining findings addressed in two commits.
All additions mutation-checked (each cited mutant now fails its target test and only that). 163/163 pass in |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
Test Plan (not a blocker): src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts:5161 — [review] gatedEventStream() is a near-copy of pipeline.test.ts's gatedStream() helper
Convergence: round 3 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 4 (2 new). Findings keep coming back to the same files: packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts (findings in rounds 1, 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
已审查——无阻断问题。 建议见行内评论。
Test Plan(非阻断):src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 4 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts(第 1、2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
…hropic wire Add the idle-knob twin of the lifetime env test in the watchdog suite: with no explicit streamIdleTimeoutMs config, the constructor must resolve the idle window from QWEN_STREAM_IDLE_TIMEOUT_MS. Mirrors the OpenAI pipeline.test.ts case. Mutant check: replacing the constructor's resolveStreamIdleTimeoutMs(contentGeneratorConfig) with the direct config ?? DEFAULT fallback leaves this test red (env 3000 no longer fires at 3000ms).
|
Patrol follow-up on the 02:57Z Suggestion: the missing The new test |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. LGTM! ✅
Test Plan (not a blocker): src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts:5161 — [review] gatedEventStream is a near-copy of pipeline.test.ts's…packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts:5529 — [probe] default-cap test hardcodes drip arithmetic coupled to the…
中文说明
无阻断问题。LGTM!✅
Test Plan(非阻断):src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/geminiChat.test.ts — no such file or directory。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
# Conflicts: # packages/core/src/core/geminiChat.ts # packages/core/src/core/openaiContentGenerator/pipeline.ts
# Conflicts: # packages/core/src/core/geminiChat.ts # packages/core/src/core/openaiContentGenerator/pipeline.ts
# Conflicts: # packages/core/src/core/geminiChat.ts
|
Latest-main closeout is complete at
The PR is conflict-free and mergeable; the remaining merge gate is maintainer review. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory; src/core/openaiContentGenerator/constants.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts:457 — [probe] Anthropic wrap condition's maxLifetimeMs > 0 disjunct has no test (OpenAI twin exists)
— qwen3.8-max via Qwen Code /review (v0.22.2)
This reverts commit 7b4e0c9.
|
Scope cleanup is now on |
|
The cleaned head CI confirms the attribution: 442/443 helper tests passed, with only |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. LGTM! ✅
Test Plan (not a blocker): src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts:458 — [probe] Anthropic wrap condition's lifetime-only disjunct has no test; mutant survives the suite
中文说明
无阻断问题。LGTM!✅
Test Plan(非阻断):src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.test.ts — no such file or directory; src/core/openaiContentGenerator/pipeline.concurrent.test.ts — no such file or directory。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
Maintainer verification — real local build, live A/B against an Anthropic-protocol upstreamI built this branch locally and drove the shipped bundle against a real HTTP upstream speaking the Anthropic Messages API, rather than relying on the unit suite alone. Summary up front: the hang is real, the PR ends it, and I found no regression. Details, evidence and three non-blocking notes below. Pinned at: PR head How the environment was built (click to expand)
1. The hang is real, and the PR ends it
The drip case is the important one: it is the shape an idle-only guard cannot catch, and the run confirms the two guards divide the work exactly as the PR describes — 50 chunks arrived, the idle timer never expired, the cap did.
2. No false positives, and the opt-out still works
3. The riskiest hunk in the diff — and it is correctThe guard's
So the empty-stream diagnostic path is preserved byte-for-byte in user-visible behaviour, and the one-line signal change is exactly what preserves it. The PR's own test
4. The shared module did not break the wire it came fromMost of this diff is a code move out of
The generalisation to 5. The documentation changes are accurate, not aspirationalThe PR rewrites
6. Do the new tests actually have teeth? (mutation matrix)Five load-bearing lines flipped one at a time, running the Anthropic + OpenAI-pipeline suites (334 tests) each time. All five mutants died, and the three in the shared module died from both wires — which is the evidence that the extracted module is genuinely exercised from both sides, not just re-covered on one:
7. Repo checks (all on PR head, local macOS)
One caveat on my environment, not the PR: 8. Notes for the merge decision (none blocking)
VerdictApprove from my side. The claimed hang reproduces on 中文说明维护者验证 —— 本地真实构建,对着 Anthropic 协议上游做 A/B我在本地构建了该分支,并用打包产物去打一个真实的 HTTP 上游(Anthropic Messages API 协议),而不是只跑单测。结论先说:**卡死是真的、这个 PR 确实解决了它、我没发现回归。**证据和三条非阻塞备注在下面。 验证基准: PR head 环境是怎么搭的(点击展开)
1. 卡死是真的,PR 确实终结了它
滴灌这个用例是关键:它正是只有空闲 watchdog 抓不住的形态,实测确认两个 guard 的分工与 PR 描述完全一致 —— 50 个 chunk 到达,空闲定时器始终没超时,是生命周期上限兜住的。 2. 没有误杀,且 opt-out 仍然有效
3. diff 里风险最高的那一处 —— 而它是对的Guard 的
所以空流诊断路径在用户可见行为上被逐字保留,而那一行 signal 改动正是保留它的原因。PR 自带的用例 4. 抽出去的共享模块没有伤到它原来那条线这个 diff 的主体是把代码从
泛型化为 5. 文档改动是实测属实的,不是「愿景」PR 把
6. 新加的测试真的有辨别力吗?(变异矩阵)逐个翻转 5 处承重代码,每次跑 Anthropic + OpenAI pipeline 两套用例(334 个)。5 个变异体全部被杀死,而且共享模块里的那 3 个是被两条线同时杀死的 —— 这正是「抽出来的模块确实被双方共同覆盖」的证据,而不是只在一边重新覆盖了一遍:
7. 仓库检查(全部在 PR head 上,本地 macOS)
一条与 PR 无关的环境说明:我一开始在仓库根跑 8. 给合并决策的备注(都不阻塞)
结论我这边同意合并。 所声称的卡死在 |










What this PR does
This PR applies the existing stream idle and maximum-lifetime watchdogs to Anthropic-backed generation and consolidates the watchdog behavior shared with OpenAI-compatible providers. A stream that goes silent, or keeps emitting chunks without ever completing, now aborts with a retryable
ETIMEDOUTinstead of hanging the CLI. Healthy streams, explicit user cancellation, and the existing OpenAI retry and fallback behavior remain unchanged.It also corrects stale method references in the chat rollback invariant documentation.
Why it's needed
Before this change, Anthropic streams had no idle or total-lifetime bound and could leave the CLI waiting until the process was killed. An idle-only guard is insufficient because recurring thinking deltas can keep resetting it, so both providers now use the same idle and non-resetting lifetime protections.
This addresses findings 4 and 6 in #9005. Finding 1 and the tool-use portion of finding 3 were already addressed by #9013. Findings 2 and 5 remain out of scope pending maintainer direction around #9006.
Reviewer Test Plan
How to verify
ETIMEDOUT.AbortError.Focused verification:
cd packages/core npx vitest run src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts src/core/llm-chat.test.ts src/core/openaiContentGenerator/pipeline.test.ts src/core/openaiContentGenerator/pipeline.concurrent.test.tsEvidence (Before & After)
After syncing with the latest
main, the focused regression run passes 721 tests. The current required GitHub CI checks also pass.Tested on
Risk & Scope
0to disable either guard.Linked Issues
Addresses #9005 findings 4 and 6. Related: #9006.
中文说明
本 PR 做了什么
本 PR 将现有的流空闲超时和最大生命周期 watchdog 应用于 Anthropic 后端,并合并了 Anthropic 与 OpenAI 兼容提供方共用的 watchdog 行为。流如果长时间静默,或持续输出 chunk 却始终不结束,现在会以可重试的
ETIMEDOUT中止,不再让 CLI 一直卡住。正常流、用户主动取消,以及现有 OpenAI 重试和 fallback 行为保持不变。同时修正了聊天回滚不变量文档中过期的方法引用。
为什么需要
改动前,Anthropic 流没有空闲或总生命周期限制,可能让 CLI 一直等待,直到进程被手动终止。只有空闲 watchdog 还不够,因为持续出现的 thinking delta 会不断重置它;因此两个提供方现在使用相同的空闲保护和不可重置的生命周期保护。
本 PR 处理 #9005 的 finding 4 和 6。Finding 1 与 finding 3 中 tool-use 相关部分已经由 #9013 处理。Finding 2 和 5 暂不在本 PR 范围内,等待维护者围绕 #9006 确定方向。
Reviewer Test Plan
如何验证
ETIMEDOUT。AbortError。聚焦验证命令:
cd packages/core npx vitest run src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts src/core/llm-chat.test.ts src/core/openaiContentGenerator/pipeline.test.ts src/core/openaiContentGenerator/pipeline.concurrent.test.ts证据(改动前后)
同步最新
main后,聚焦回归测试共 721 项通过。当前 GitHub 必要 CI 也全部通过。测试平台
风险与范围
0禁用任一 guard。关联 Issue
处理 #9005 的 finding 4 和 6。相关:#9006。