fix(acp-bridge): backpressure ACP NDJSON queue saturation instead of tearing down the channel - #10731
Conversation
…tearing down the channel When the decoded NDJSON queue saturates (slow consumer plus large session/update frames), the fail-closed guard tears down the whole ACP channel, killing every session multiplexed on it (#10162). Wait up to a bounded grace window (default 10s, overridable via queueSaturationGraceMs) for the consumer to drain before falling back to the original fail-closed guard. The wait pauses the pump, which backpressures the agent's stdout pipe while keeping the memory bound intact. Fire onQueueSaturated once per episode (wired to a daemon WARN in qwen serve) so saturation is visible before any eviction. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
|
… validation Address review findings on #10162: add a test that cancels the readable while the pump is parked on a saturated queue (a missed cancel wake would expire the wait and fail closed), a test that a frame larger than maxQueuedBytes fails immediately without the saturation warning or the grace delay, a validation test for queueSaturationGraceMs, and tighten the onQueueSaturated payload assertion to all five fields. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…monotonic deadline Correctness review findings on #10162: a chronically borderline consumer saturated on every frame and re-fired onQueueSaturated indefinitely, flooding the daemon log the warning exists to make diagnosable. Track an episode: warn once per uninterrupted saturation episode and re-arm only after the queue fully drains. Also compute the grace deadline with performance.now() so wall-clock steps cannot extend or shorten the backpressure window. Adds a test pinning one warning across multiple saturating frames in a single episode. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Thanks for the PR! Template looks good ✓
Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓
进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewReviewed all three commits up to The mechanism holds up. My independent proposal for this problem was exactly this shape — a bounded grace wait in the enqueue path, woken when the consumer drains, with the original fail-closed error at the end of the deadline — and the implementation matches it without extra machinery:
The third commit tightens two things that a careful reviewer would otherwise have raised: the saturation warning is now deduped per episode — the episode state resets exactly when The tests are the strongest part overall: the pre-existing fail-closed tests keep their assertions (with a 25ms grace to stay fast), and the new ones pin recovery-after-drain, warn-then-fail with the exact hook payload, once-per-episode dedup, cancel wake, oversized-frame immediate failure, and limit validation. No blockers, no convention issues, no drive-by changes. Test evidence — the PR's own CI at
|
| Check | Conclusion |
|---|---|
Test (ubuntu-latest, Node 22.x) |
❌ failure |
Classify PR |
✅ success |
Dependency CVE audit |
✅ success |
Desktop Shell (ubuntu-22.04) |
✅ success |
Desktop Shell (windows-2022) |
✅ success |
Integration Tests (no-AK, No Sandbox) |
✅ success |
macos-latest / Java 21 |
✅ success |
Post Coverage Comment (ubuntu-latest, 22.x) |
✅ success |
Real daemon E2E / Java 11 |
✅ success |
Secret scan (TruffleHog) |
✅ success |
Serve A/B (ubuntu-latest, Node 22.x) |
✅ success |
ubuntu-latest / Java 11 |
✅ success |
ubuntu-latest / Java 17 |
✅ success |
ubuntu-latest / Java 21 |
✅ success |
web-shell E2E Smoke (ubuntu-latest, Node 22.x) |
✅ success |
windows-latest / Java 21 |
✅ success |
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。
Sandboxed verification would settle the remaining gap: @qwen-code /verify — that a transiently slow consumer really keeps a live daemon channel alive (not just the unit-harness streams) is not substantiated yet, and this PR's own testing was unit + typecheck on Linux only, no live serve replay.
中文说明
代码审查
审查覆盖 5141bedf 之前全部三个 commit(核心改动 → 纯测试补充 → episode 去重 + 单调时钟截止时间;已逐个 commit 互相对 diff 确认)。
机制是成立的。我对这个问题的独立方案就是这个形态——在入队路径上做有界宽限等待、消费者腾出空间时唤醒、宽限到期仍走原来的 fail-closed 错误——实现与之一致且没有多余机关:
- 流的 high-water mark 就是
maxQueuedBytes且按帧计费,因此controller.desiredSize正是等待循环比较的剩余字节预算。 - 只存在一个泵(
start()只启动一个pumpBoundedInput)且顺序等待,单槽wakeQueueWaiter不会丢等待者;即使唤醒先于注册发生,超时后也会重读desiredSize,最坏只损失时延,不会误报失败。 cancel()会唤醒等待者,每次 await 后的isCanceled()检查防止取消后继续入队;"背压中取消"测试故意等过宽限窗口,正好钉住"漏唤醒"这一失效模式。- 两条立即失败路径保留不变:超过
maxQueuedBytes的帧(队列清空也放不下)与desiredSize === null都直接跳过等待,超大帧测试确认该路径不触发饱和告警。 - 内存上限不变:等待发生在入队之前,等待期间泵停止读取,背压落在 agent 的 stdout 管道上而不是堆上。
- serve 接线完整:
run-qwen-serve.ts三处 hooks 构造点全部接上onQueueSaturated,WARN 双重保护(callHook本身吞掉钩子异常,CLI 又包了一层 try/catch)。 validateNdJsonStreamLimits对新可选字段沿用"正的安全整数"规则,并有测试拒绝0。
第三个 commit 收紧了两个严格审阅者本来也会提出的点:饱和告警改为按 episode 去重——episode 恰好在 desiredSize === maxQueuedBytes(即队列完全排空)时复位,长期处于临界状态的消费者每个 episode 只告警一次,而不是每帧一次;宽限截止时间从 Date.now() 换成 performance.now(),墙钟跳变既不会延长也不会缩短宽限窗口(且与 Node 自身定时器使用的单调时钟一致)。两者都有对应测试钉住。
测试整体是最强的部分:原有 fail-closed 测试断言不变(传 25ms 宽限保持快速),新测试分别钉住"腾出空间后恢复送达""先告警后失败(精确断言钩子负载)""每 episode 只告警一次""取消唤醒""超大帧立即失败""参数校验"。无阻塞问题、无规范问题、无夹带改动。
测试证据 —— 该 PR 自身在 5141bedf 上的 CI(一次性抓取,未轮询)
抓取时无失败。已完成项全绿:Desktop Shell(ubuntu/windows)、Java 矩阵(ubuntu 11/17、macOS 21、windows 21)、依赖 CVE 审计、密钥扫描;关键套件仍在运行:单元测试 Test (ubuntu-latest, Node 22.x)、Serve A/B、Real daemon E2E / Java 11、Integration Tests (no-AK, No Sandbox)、ubuntu-latest / Java 21。macOS/windows 的 Node 腿在该事件下被跳过。CI 落定后 finalize 任务会重写上方表格。
沙箱验证可以补上剩余缺口:@qwen-code /verify —— "短暂变慢的消费者在真实 daemon 上确实能保住通道"(而非仅在单测的内存流上)目前尚未被证实,且本 PR 自身仅做了 Linux 上的单元测试 + 类型检查,没有线上 serve 回放。
— Qwen Code · qwen3.8-max
Reviewed at 5141bedf198ea899609be2b5906125c50804d622 · re-run with @qwen-code /triage
|
Confidence: 4/5 — clean, minimal, well-tested fix for a field-observed failure mode; withholding the last point only because the live-serve behavior is so far exercised by unit tests and CI rather than a daemon replay. Stepping back: this earns its merge. The problem is real and quantified in #10162, and the approach matches my independent proposal — there was no materially simpler path that keeps the memory bound: raising the limits only delays the same cliff, dropping frames is lossy, and per-session teardown is a much larger change that the issue itself splits out. The follow-up commits landed exactly what a skeptical reviewer would have asked for — the cancel/oversized/validation coverage, then the per-episode warning dedup and the monotonic deadline — and the pre-existing fail-closed tests kept their assertions throughout. Every line in the diff serves the stated goal; nothing unrelated rode along. The tradeoff is honestly framed and acceptable: a genuinely wedged consumer now holds the pump for up to the grace window before teardown instead of failing instantly, the channel-liveness monitor still kills wedged agent children independently, and CI on 中文说明置信度:4/5 —— 针对现场观测到的故障模式,修复干净、最小化、测试充分;只扣一分,因为真实 serve 场景目前仅由单元测试和 CI 覆盖,尚无 daemon 回放验证。 整体看:这个 PR 值得合入。问题在 #10162 中真实且有量化证据,方案与我的独立提议一致——在保持内存上限的前提下不存在更简单的路径:调大上限只是推迟同一个悬崖,丢帧是有损的,按 session 拆通道是 issue 本身拆分出去的更大改动。后续两个 commit 恰好补上了严格审阅者会要求的内容——取消/超大帧/参数校验覆盖,再到按 episode 的告警去重与单调时钟截止时间——且原有 fail-closed 测试的断言全程保留。diff 中每一行都服务于既定目标,没有夹带无关改动。 权衡表述诚实且可接受:消费者真卡死时,泵会在宽限窗口内保持暂停再拆通道(而非立即失败),通道活性监控仍会独立杀掉卡死的 agent 子进程,且
— Qwen Code · qwen3.8-max Reviewed at |
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. |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Test Plan (not a blocker): 35 tests pass — this review observed 1904, 27712, 256, 1753, 504, 5468, 94 passed.
中文说明
已审查。 建议见行内评论。
Test Plan(非阻断):35 tests pass — this review observed 1904, 27712, 256, 1753, 504, 5468, 94 passed。
— qwen3.8-max via Qwen Code /review (v0.22.3)
Review + E2E report (head
|
A stale merge left two `language` interface members and two destructured
defaults in ChatEditor.test.tsx, breaking esbuild transform
("The symbol language has already been declared") in CI.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtj0ojdzar
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): 35 tests pass — this review observed 5575, 504 passed.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/acp-bridge/src/ndJsonStream.ts:259 — [review] R1-1 validateNdJsonStreamLimits accepts any positive safe integer for queueSaturationGraceMs (setTimeout clamps above 2^31-1 ms) — still stands from round 1packages/acp-bridge/src/ndJsonStream.test.ts:606 — [review] R1-2 cancel-wake regression test pins nothing (mutant passes with wakeQueueWaiter?.() removed) — still stands from round 1packages/acp-bridge/src/ndJsonStream.ts:529 — [review] R1-3 grace deadline calls performance.now() directly while sibling components inject the clock — still stands from round 1packages/acp-bridge/src/ndJsonStream.ts:504 — [review] R1-4 episode-reset semantic ('episode ends when the queue fully drains') has no test — still stands from round 1packages/cli/src/serve/run-qwen-serve.ts:5094 — [review] R1-5 serve-side onQueueSaturated wiring (3 sites + five-field mapping) has no CLI test — still stands from round 1
中文说明
Test Plan(非阻断):35 tests pass — this review observed 5575, 504 passed。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.3)
…ut-20260902 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Clamp oversized timeout scheduling without changing the monotonic grace deadline, strengthen cancellation and episode regression coverage, and verify serve-side saturation hook wiring and log fields. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The grace window defaulted to 10s, exactly CHANNEL_LIVENESS_PROBE_TIMEOUT_MS. The liveness probe's response travels over this same NDJSON stream, so a pump parked on backpressure cannot answer it: at parity every saturation episode burns a probe, and two episodes inside two probe intervals tear the channel down as an acp_channel_liveness_timeout — the multi-session outage the grace window exists to prevent, reported as the wrong cause. Default to 5s and pin the invariant against channel-liveness.ts in ndJsonStream.test.ts. Also scope the saturation state (grace, waiter, episode dedup) to the createBoundedReadable closure instead of threading it through pumpBoundedInput and readBoundedChunk. Both drop from three added parameters to one, and DecodedQueueSpaceOptions / SaturationEpisodeState go away entirely. Drop the try/catch around the serve-side warn as well: callHook already isolates hook throws from the transport.
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
Reviewed at head e6106dd2.
- The backpressure design is sound: the pump waits a bounded grace window for the consumer (single waiter, woken from
pull()andcancel(), monotonic-clock deadline, timer clamped below the 2^31 setTimeout ceiling), and the original fail-closedNdJsonQueueLimitErrorstill fires when the grace expires — so the memory bound is unchanged, only the teardown is delayed for transiently slow consumers. Frames that can never fit a drained queue and null-desiredSize states keep the immediate error path. - Warning hygiene is deliberate: one
onQueueSaturatedper episode (reset on full drain), hook throws isolated by the existingcallHook, and the grace is pinned to stay below the liveness-probe timeout so a parked pump cannot mask a real outage as a probe failure — the relation is asserted in the test file. - Cancellation exits the wait loop before enqueue, and
knownSessionIds-style post-effects never run for a torn-down wait; the 280 new lines pin recover, grace-expiry teardown, cancel, and per-episode dedup. - 5/5 threads resolved, no prior review rounds outstanding, CI has no failures; per the channel convention the call is on the review itself.
yiliang114
left a comment
There was a problem hiding this comment.
Review findings (self-PR, cannot self-approve):
- The backpressure design is correct:
ensureQueueSpacewaits bounded (monotonic performance.now deadline, no wall-clock drift) for the decoded queue to drain before the fail-closedNdJsonQueueLimitErrorfires; a frame too large for a fully drained queue fails immediately;desiredSize === nullfails immediately; the per-episode warning resets only on a full drain so a borderline consumer warns once per episode. - Wakeup wiring is sound: single waiter woken by
pull()(consumer drained) and bycancel(); the missed-wakeup race degrades to one bounded re-check of desiredSize, never a hang; the timer is clamped below Node's 2^31-1 delay ceiling so a long grace waits instead of spinning. - The grace window is pinned below CHANNEL_LIVENESS_PROBE_TIMEOUT_MS by a dedicated test, with the reasoning documented (the probe response rides this same stream); pump awaits the now-async chunk read and re-checks cancellation.
- Test coverage matches the failure modes one-to-one (drain rescue, stalled-consumer fail-closed, per-episode warning, cancel-while-backpressured, oversize frame, timer cap, grace/liveness relation, limit validation).
CI on this head is still running; no blocking issues found.
chiga0
left a comment
There was a problem hiding this comment.
Scope: packages/acp-bridge/src/ndJsonStream.ts, ndJsonStream.test.ts, packages/cli/src/serve/run-qwen-serve.ts, run-qwen-serve.test.ts (all 4 changed files). NOT reviewed: macOS/Windows live-serve behaviour (author marks
No blocking findings.
Approval blockers: none.
Checked
Class 1 — contract asymmetry (writer/reader ends)
NdJsonQueueSaturationInfo.graceMs→ logged asqueueSaturationGraceMsinrun-qwen-serve.ts. Intentional rename;run-qwen-serve.test.tsassertsqueueSaturationGraceMs=10000in the log output. Consistent end-to-end.DAEMON_ACP_NDJSON_LIMITS(inspawnChannel.ts) omitsqueueSaturationGraceMs; the default (5 s) applies. All 3pipeHooksobjects inrun-qwen-serve.ts(lines 5109, 6016, 6675) includeonQueueSaturated. No site missed.
Class 2 — API/compatibility
queueSaturationGraceMsis optional; callers without it get the 5 s default grace window instead of the previous immediate fail-closed. This is the intended behavioral change; described and scoped in the PR.- New exports (
NdJsonQueueSaturationInfo,NDJSON_QUEUE_SATURATION_GRACE_MS,onQueueSaturated) are additive; no existing surface removed.
Class 3 — error handling / state transitions
- Cancel while backpressured:
cancel()setscanceled = true, clears pending, callswakeQueueWaiter?.(). Pump exitsensureQueueSpaceat theif (canceled) returnguard. No transport error emitted. Covered by the "cancel while backpressured" test. - Double-wake safety (timer fires AND
pull()fires):finish()setswakeQueueWaiter = undefinedand callsclearTimeoutbefore resolving; second call via stale reference is a no-op on an already-resolved Promise. Safe in the JS single-threaded model. - Fail-closed path preserved: when
remainingMs <= 0,queueLimitErroris thrown and propagates through the existingpumpBoundedInputcatch block toonTransportError. Covered by the "warns once before failing closed" test. cancelReaderis wrapped in try/catch, so ifreader.releaseLock()(in the pump'sfinally) races it, the thrown TypeError is swallowed. Pre-existing pattern, correct here too.
Class 5 — test validity
- 8 new tests. Mutation-detectable mechanics: (a) the backpressure test would fail on
origin/mainper PR claim becausereadBoundedChunkpreviously threw immediately; (b) the fail-closed test explicitly waits foronTransportError; (c) the "caps timer" test spies onglobalThis.setTimeoutand verifies the exact clamped value. vi.waitForuses real microtasks; tests with 5 s grace do not block—the consumer read wakes the pump viapull()before the timer fires.- Pre-existing saturation tests retain fail-closed assertions and now pass
queueSaturationGraceMs: 25to stay fast.
Class 7 — state lifecycle
saturationWarnedresets only whendesiredSize === maxQueuedBytes(queue fully empty). With customsize()callback andhighWaterMark: maxQueuedBytes, this is equivalent to queue size = 0. The "once per episode" semantic is documented, correct, and tested.
Class 10 — stated intent vs. code
- Grace (5 000 ms) < probe timeout (10 000 ms) < liveness interval (15 000 ms). Invariant pinned by the "keeps default grace window under liveness probe timeout" test against the live constants in
channel-liveness.ts. - PR description states "a frame larger than
maxQueuedBytesstill fails immediately without waiting." Confirmed: thequeueCharge > limits.maxQueuedBytesbranch throws before the backpressure path and beforeonQueueSaturatedfires. Covered by the "fails immediately for a frame that can never fit" test.
Not covered
- macOS and Windows runtime behaviour (no host; no OS-specific paths in diff, so this is a disclosure only, not an approval blocker).
- Live-serve replay: author explicitly scopes this out.
Reviewed with AI assistance.

What this PR does
When the daemon's bounded ACP NDJSON transport saturates its decoded queue, it no longer tears the channel down immediately. Instead the bounded reader waits up to a grace window (
queueSaturationGraceMsonNdJsonStreamLimits, default 5s viaNDJSON_QUEUE_SATURATION_GRACE_MS) for the consumer to drain, applying backpressure to the agent's stdout pipe while the memory bound stays intact. A newonQueueSaturatedhook fires once per saturation episode before the wait starts, andqwen servewires it to a daemon WARN with the queue occupancy details. If the consumer is still stalled after the grace window, the original fail-closedNdJsonQueueLimitErrorpath runs unchanged.Why it's needed
The guard introduced in #8911 is fail-closed: a slow consumer plus one large frame (long-lived sessions legitimately produce 1MB+
session/updateframes) kills the whole ACP channel, tearing down every session multiplexed on it — including sessions that were only idling. Field evidence in #10162: 14channel exited (transport=ndjson_queue_limit_exceeded, N session(s) torn down)events over ~3 days on one busy serve host, plus in-flight prompt turns failing withBridgeChannelClosedError: agent channel closed mid-request. Saturation means the consumer is slow, not that the producer is malicious; pausing the producer is the proportionate response, and it converts a multi-session outage into a transient slowdown for the common case (channel clients reconnecting, briefly blocked outbound SSE).Reviewer Test Plan
How to verify
cd packages/acp-bridge && npx vitest run src/ndJsonStream.test.ts— 41 tests pass, including:backpressures a saturated queue until a transiently slow consumer drains: second frame does not fit,onQueueSaturatedfires, no transport error; after the consumer drains one message the waiting frame is delivered and the stream closes cleanly. This test fails on origin/main (immediate teardown).warns once before failing closed when the consumer stays stalled: with a 25ms grace the guard still fires (NdJsonQueueLimitError) but only after the warning hook.queueSaturationGraceMs: 25so they stay fast.npm run typecheckinpackages/acp-bridgeandpackages/cli; ESLint/Prettier clean on all touched files.packages/acp-bridgesuite: 1898/1900 pass; the 2 failures areprocess-registry.process.test.tsreal-process-tree tests that fail identically on clean origin/main on this host (environmentalprocess-table query returned no parseable rows), unrelated to this change.Evidence (Before & After)
N/A (daemon internals; no TUI/UI change). Log-visible difference: before, the first and only signal was
qwen serve: channel exited (..., transport=ndjson_queue_limit_exceeded, N session(s) torn down). After, a saturation episode logs[WARN] ACP NDJSON decoded queue saturated {requiredBytes, availableBytes, maxQueuedMessages, maxQueuedBytes, queueSaturationGraceMs}first, and the channel survives when the consumer recovers within the grace window.Tested on
Environment (optional)
Unit tests + typecheck only; no live serve replay in this pass.
Risk & Scope
maxQueuedBytesstill fails immediately without waiting).CHANNEL_LIVENESS_PROBE_TIMEOUT_MS(5s vs 10s) — at parity every saturation episode would burn a probe, and two episodes inside two probe intervals would tear the channel down as anacp_channel_liveness_timeout, i.e. the same multi-session outage this PR prevents, reported as the wrong cause.ndJsonStream.test.tspins the invariant againstchannel-liveness.ts.channel exitedis being worked separately.queueSaturationGraceMsis optional; without it the behavior differs from before only by the bounded wait on saturation.Linked Issues
Refs #10162 (follow-up to the #8911 guard; part of the daemon resource-protection theme in #8051 / #7306)
中文说明
这个 PR 做了什么
当 daemon 的有界 ACP NDJSON 传输的解码队列饱和时,不再立刻拆掉整条通道。有界读取器会先等待一个宽限窗口(
NdJsonStreamLimits上的queueSaturationGraceMs,默认NDJSON_QUEUE_SATURATION_GRACE_MS= 5 秒)让消费者把队列腾出来,期间对 agent 的 stdout 管道施加背压,内存上限保持不变。新增onQueueSaturated钩子在每次饱和开始时触发一次,qwen serve把它接到一条带队列占用详情的 daemon WARN 日志。如果宽限窗口结束后消费者仍然卡住,则照旧走原来的 fail-closedNdJsonQueueLimitError路径。为什么需要
#8911 引入的守卫是 fail-closed 的:消费者短暂变慢加上一个大帧(长生命周期会话合理地产生 1MB+ 的
session/update帧)就会杀掉整条 ACP 通道,把复用在上面的所有 session 一起打死——包括只是空闲的 session。#10162 的现场证据:一台繁忙 serve 主机约 3 天内出现 14 次channel exited (transport=ndjson_queue_limit_exceeded, N session(s) torn down),进行中的 prompt turn 报BridgeChannelClosedError: agent channel closed mid-request。饱和说明消费者慢,不是生产者恶意;暂停生产者是成比例的响应,对常见场景(通道客户端重连、出站 SSE 短暂阻塞)把多 session 故障降级为一次短暂变慢。审阅者测试计划
如何验证
cd packages/acp-bridge && npx vitest run src/ndJsonStream.test.ts— 41 个测试全过,含:backpressures a saturated queue until a transiently slow consumer drains:第二帧放不下,onQueueSaturated触发、无传输错误;消费者腾出一条后等待中的帧被送达,流干净关闭。该测试在 origin/main 上会失败(立刻拆通道)。warns once before failing closed when the consumer stays stalled:25ms 宽限下守卫仍会触发(NdJsonQueueLimitError),但一定在告警钩子之后。queueSaturationGraceMs: 25保持快速。packages/acp-bridge与packages/cli的npm run typecheck通过;改动文件 ESLint/Prettier 干净。packages/acp-bridge全量:1898/1900 通过;2 个失败是process-registry.process.test.ts的真实进程树测试,在干净 origin/main 上同样失败(本机环境问题),与本改动无关。前后证据
N/A(daemon 内部,无 TUI/UI 变化)。日志可见差异:之前唯一信号是
qwen serve: channel exited (..., transport=ndjson_queue_limit_exceeded, N session(s) torn down);现在饱和会先打[WARN] ACP NDJSON decoded queue saturated {requiredBytes, availableBytes, maxQueuedMessages, maxQueuedBytes, queueSaturationGraceMs},消费者在宽限窗口内恢复时通道存活。测试平台
环境(可选)
仅单元测试 + 类型检查;本轮未做线上 serve 回放验证。
风险与范围
maxQueuedBytes仍然立即失败不等待)。CHANNEL_LIVENESS_PROBE_TIMEOUT_MS(5 秒 vs 10 秒)——若两者相等,每次饱和都会烧掉一次探测,两次饱和落在两个探测周期内就会以acp_channel_liveness_timeout拆掉通道,也就是本 PR 要避免的多 session 故障,却报成了错误的原因。该不变量已在ndJsonStream.test.ts中对channel-liveness.ts做断言。channel exited现场细节的可观测性另有工作在并行进行。queueSaturationGraceMs为可选字段;不传时与旧行为的唯一差别是饱和时的有界等待。关联 Issue
关联 #10162(#8911 守卫的后续;属于 #8051 / #7306 跟踪的 daemon 资源保护主题)