Repository navigation
Conversation
…lling Managed sessions fail-closed on journal write errors, but the plain transactions commit tolerated no uncertainty: one dropped receipt (timeout or 5xx) parked the session authority in a permanent write failure. The broker already dedupes commits by command key and replays their receipts, so retrying uncertain failures is safe; do it on both commit paths with one shared predicate. In the managed runtime worker, a relative file path was resolved against the session directory without verifying containment, while the shell directory argument already required the same admitted-workspace check. Reject paths that resolve outside the registered workspace. The managed panel ran three independent fixed-3000ms loops (bootstrap snapshot, event-stream reconnect, summary poll) with no ceiling, no jitter, and no stop for definite 4xx answers. Give them a shared backoff (3s floor, 30s cap, jittered) and stop on non-retryable client errors. Merging streamed events rebuilt and re-sorted the whole transcript per delta; append strictly-newer tails directly and keep the general dedupe-and-sort path only for overlapping or disordered input.
|
Audit context for this PR: this came out of a five-lane deep review of the whole hosted Managed-agent surface (~60k lines across the two Java modules, The good news first: point-level quality is consistently high — tenant predicates on store queries, optimistic concurrency, digest chains, workspace-mount containment ( What this PR fixes are the four most contained items. What remains is tracked in six follow-up issues:
|
…vers allow it The hosted fault-gate drivers pin a single commit attempt per targeted fault (hosted-store-failure-driver 'Harness must not retry the failed write', hosted-shell-output-driver injections counters), so the retry added for plain /transactions:commit must land together with the driver restatement of bounded, byte-identical, effect-once retry semantics. Tracked in the retry-terminal-states issue; revert it here to keep this PR small and green. Co-authored-by: Qwen-Coder <[email protected]>
Conflicts in the file-history test resolved by keeping both new cases: the workspace containment test from this branch and the async-Hooks history-admission test from H2 (#13129). Co-authored-by: Qwen-Coder <[email protected]>
|
@qwen-code-ci-bot Addressed in 4e085e6 (and now on HEAD 99c465f after the main merge): the plain The retry itself stays on the roadmap as a deliberate pairing: it must land together with a driver restatement of bounded, byte-identical, effect-once retry semantics (byte-equality of attempts, |
When an SSE connection dies by proxy idle timeout it surfaces as a status-less throw, so the stream loop's end-of-iteration reset never ran and the shared failure counter climbed to the 30s cap for the life of the panel even though every reconnect succeeded and delivered events. Reset on progress inside the for-await body, matching the bootstrap and summary loops' notion of success. Also pin previously unwitnessed review invariants: the empty-incoming merge case (a load-bearing crash guard, not an optimization), the exact exponential backoff ladder under a deterministic random, the stream loop's non-retryable exit and its unchanged resubscribe cursor, and comments documenting the exact-3s first-failure floor and the merge fast path's sorted-input contract. Co-authored-by: Qwen-Coder <[email protected]>
|
On the triage review's non-blocking notes, also addressed in 3ad03d2: Merge tests / unmeasured perf. The two merge cases are semantic-preservation guards by intent — the fast path produces byte-identical output to the general path for every valid input, so no value-space assertion can discriminate them. What now pins the existence of the append path is the empty-incoming crash test (which the general path would survive but the naive fast path TypeErrors on). For the measurement: 4000 incremental single-event merges (the streamed-delta shape) run 9.0ms on the fast path vs 234.4ms on the old Map+sort path (~26× at 4k events, gap widening linearly with history length) — ymmv, but the complexity shape (O(n log n) rebuild per delta → amortized O(1) append) is the point. Two load-bearing invariants, now documented in code rather than tribal knowledge: Containment scope. Confirmed and intentional: the new check runs on the V2 execution path where the worker resolves a relative path itself; V3 relies on the hosted harness's |
…arately A connection delivering only replayed duplicates, or delivering nothing while simply staying alive, now also resets the reconnect ladder: the old progress-only reset treated every proxy idle-timeout death as a failure and let the delay climb to the cap on healthy quiet sessions. Gap-recovery snapshots climb their own failure ladder instead of being charged to (and constantly zeroed by) counter that stream delivery owns, and a terminal non-retryable stop is now recorded in a sticky stoppedReason that per-event updates cannot erase — previously a 4xx on the summary poll left the composer frozen with no banner after the next stream event cleared the transient error. Tests pin each merged-event guard clause (ascending input, strictly-newer boundary, and the empty-current short-circuit) with its own mutation witness, and the new mock generators declare the full subscribe options shape so they typecheck outside the repo's blind tsconfig gates. Co-authored-by: Qwen-Coder <[email protected]>
…nswers A transcript-only 4xx from the gap-snapshot read no longer tears down a live event stream: the stop is recorded but the loop keeps resubscribing behind the snapshot ladder. The sticky terminal reason now retires when a later authoritative read succeeds instead of staying painted over a session that demonstrably recovered, and wins the alert chain over a newer transient error so a surviving loop cannot mask the hook's permanent decision. The four fail-classify-stop sites share one closure instead of four verbatim copies, and the hook specs mount through the shared reactHarness primitives with one deterministic-backoff helper. New pins: bootstrap and stream ladder growth across consecutive undelivered rejections, gap-snapshot ladder reset after a healed outage, gap-snapshot 4xx recorded while the stream stays subscribed, stop-reason precedence and persistence at the only production render site, and retirement of the reason by a later successful read. Co-authored-by: Qwen-Coder <[email protected]>
|
Round summary for the R4 review (sha 3e84c17): all 8 inline comments are fixed, witnessed, replied to, and resolved. On the convergence note — the cluster is real, and this round went after the shared root rather than the instances: the stream lifecycle now has one stop protocol ( Verification for this round: full managed suite 225/225; seven mutants, each redding exactly the test that targets it (keep-alive |
A sticky terminal stop in the alert chain silently swallowed every page-level action error: with the panel still interactive, a rejected submit, cancel, or list load replaced nothing, so the user saw the stale stop reason above a visibly live transcript with no trace of the fresh failure. The page's own error now renders as its sibling element — the terminal reason keeps the first positional slot that existing alerts read through, and the hook's transient error stays behind it. An expired credential is no longer terminal anywhere either: this client re-derives its headers on every request, so a short-lived-token lapse is answered fresh by the very next attempt once the host refreshes — the loops back off and recover instead of wedging behind a banner until a manual Refresh. Definite 4xx answers for session existence stay terminal and still self-retire when a later authoritative read succeeds. Co-authored-by: Qwen-Coder <[email protected]>
|
Round summary for the R5 review (sha ca150cb): the Critical is fixed and each Suggestion has a landed fix or a recorded disposition. Fixed. The sticky terminal stop no longer masks the page's own action error — it renders as a sibling second alert element, keeping the first positional slot for the terminal reason (and the hook's transient error behind it), which also avoids the mirror image of a stale page error masking a later terminal reason. The phase-2 phase of the round-4 page spec now genuinely traverses the merge-clear path (fresh event id on the reconnect), and an expired credential is retryable everywhere by construction ( Recorded disposition (not landed, round-5 posture). R5-1's retirement-rule debt — origin-tagged retirement (transcript-derived verdicts retired only by transcript reads, summary-derived only by summary reads, which also closes the loadOlder gap) plus a dispatch-sequence guard against a pre-verdict answer erasing a newer verdict — is real, but its worst observed shape is a banner flapping on a ≤30s self-healing cadence, not a fails-closed or masking path. It is recorded here as the follow-up to land as one coherent iteration of the stop/retire rule, with the conservative intermediate ("clear only at the Also acknowledged: the two deferred probes (liveness-clause counter anchoring for slow-to-fail backends; the shared constant) stay recorded-and-unacted, and the disclosed review gaps (CLI integration lane skipped; reverse audit cut at round 6) are noted — the corresponding local verification here ran the full Verification: managed suite 227/227; four mutants red exactly their target (masked single chain, alert-condition drop, per-event sticky clear, auth-exclusion removal); eslint/prettier/ |
Resolves four semantic conflicts between this branch's stream health work and #13206's resync/paging rewrite, keeping both: the gap resync uses their paged-history-preserving merge (preserveLoadedPages) while definite-4xx reads keep this branch's record-don't-kill policy — the snapshot read climbs its own ladder, stalls surface only after three advance-less resyncs, and the liveness/delivery reset and retry ladders are unchanged. Test files merge both suites onto the shared reactHarness probe shape. Also lands the R6 review fixes on top: the bootstrap snapshot tags the transcript leg so a pruned history can no longer wedge the stream before it starts (record stickily, keep retrying), the clean-close summary read shares the gap branch's record-and-survive policy instead of terminating the stream, the summary poller records a definite 4xx and keeps backing off rather than dying for the life of the mount, and the page renders the hook's terminal reason beside the action error instead of under it.
|
Round summary for the R6 review (head eb6c54c): all three Criticals fixed on top of a non-trivial merge, each pinned by a spec that reds under its own mutation. The merge. #13206 landed its resync rewrite (paged-history-preserving gap snapshots, stall guard, corrupt-frame skipping) into the same hook this branch had already restructured for failure health. The conflict was resolved as a semantic weave rather than a pick-a-side: gap resync merges through The three Criticals.
Audit discipline. Every new pin was mutation-verified twice: once pre-merge and once again on the merged M5a base (each mutation reds exactly its spec, pristine revert byte-identical afterwards). Marker audit verified both sides' discriminating markers survived the auto-merges (fast-path merge, containment, |
The merge commit absorbed a NOTICES regeneration produced from my local worktree, whose pnpm store resolves a few transitive versions differently from CI's canonical frozen-lockfile environment (is-fullwidth-code-point, fdir, picomatch, get-east-asian-width). The lane regenerates from a clean install and therefore disagreed with the checked-in file. Co-authored-by: Qwen-Coder <[email protected]>
|
Two red lanes, two different causes — one fixed in 07a9756, one investigated below with a rerun requested. Lint & Static ( Hosted process fault gates / MySQL 8.4 —
Criterion seeded for the rerun: green ⇒ lane-transient confirmed; same point red again ⇒ deterministic lane/main regression, and I will file the tracking issue (normalised title) with the packaged-turn logs attached. Rerunning now. |
|
@qwen-code /resolve |
Real-stack verification, round 5 — #13179 @
|
Tool call (Session cwd <root>/child) |
Harness | Round 1, old main worker | Main worker now (#13166) |
|---|---|---|---|
read_file lnk2/outside-secret.txt (link already in the Workspace) |
normal | ✕ outside content returned | ✓ refused |
read lnk4/a.txt, then lnk4 is swapped for a link, then read again |
normal | ✕ outside content returned | ✓ refused |
read_file ../../secret.txt |
bypassed | ✕ outside content returned | ✓ refused |
read_file /…/ws/secret.txt (absolute) |
bypassed | not tested in round 1 (the PR's check skips absolute paths by design, R7-2) | ✓ refused |
write / edit / ../ / absolute |
either | refused by Harness or file history | same |
write inside-child.txt |
either | written | written |
Resolution. Take main's executor. Keep the PR's two tests but change the expected text to 'is not within the Session working directory' (conflict-resolution-test-messages.patch, 2 lines; it also applies to 1780afcd). With that, the cli plan suites pass 935/935. main's own suite already covers relative ../ reads and writes (managed-context-worker.test.ts, e.g. ../web/secret.txt, ../web/pwned.txt), so the PR's two tests can be kept with the new message or simply dropped.
Description. Item 1 should then be described as "superseded by #13166". The title's "worker path containment" half no longer corresponds to a code change in this PR.
2. Proof-of-life: the R11-1 gate never fires on this server
What the server sends. A raw capture of a 40 s idle resume (afterSequence = the Session head) shows no frame at all after the head, only :keepalive comments about every 15 s. java-managed-agent-client drops comments (if (!hasContent) return undefined; // heartbeat or comment), so on an idle Session delivered never becomes true and if (delivered) expireAnswered('stream') never runs.
| One-off failure, then healthy and idle | main | 9439da72 |
head 1780afcd |
+ candidate |
|---|---|---|---|---|
| (c) one 404 on a stream reconnect | next event | cleared 8.7 s | next event | cleared 20.5 s |
| (c′) one 502 on the same reconnect | next event | next event | next event | cleared 21 s |
| Real Spring outage (60 s) | still up 60 s after | still up | still up 60 s after | cleared 28.3 s after |
| Gateway 401 for 60 s | still up 45 s after | still up | still up 45 s after | cleared |
| (e) 404, then the reconnect is black-holed 30 s (open, no bytes) | held, then next event | — | held, then next event | held for the whole hole (no gap), cleared 50.6 s |
Candidate — candidate-keepalive.patch; applies cleanly to 1780afcd.
- Change.
streamEventstakes an optionalonAliveand calls it for each complete heartbeat or comment frame (not for a trailing fragment).- The provider passes
onAlivethrough. - The hook's
onAlivemarks the attempt as delivered and, if the proof-of-life point has already passed, runs the expiry it was waiting for.
- Why R11-1 still holds. A black-holed connection never sends a keep-alive, so the gate and the catch-side restore are unchanged.
- Size. Source +26/−2 across client, provider and hook; tests +124:
- one client spec: each keep-alive is reported, a trailing fragment is not;
- two hook specs: a 502 and a 404 followed by a keep-alive-only idle reconnect, where the keep-alive lands after the 3 s point. All three fail on the head.
- Tests. Web-shell managed 335/335.
- Real stack. The rows above. The deleted-Session line still stays, because each reconnect answers 404 within milliseconds and never stays open long enough to receive a keep-alive.
3. Holding / notes
- Bootstrap race. 20/20 pages stop at the first attempt.
- Deleted Session (240 s, 2 panels per arm). The head made 33 / 30 requests per panel and main made 160. The head's ladder averages 16.8 s at the cap, matching the expected 16.5 s. An earlier single-panel run showed 42 requests; I re-ran it with two panels per arm side by side and attribute that to sampling.
- Duplicate ids. None in 326 transcript reads taken while Turns were streaming (2 340 across all rounds).
- PR body. Unchanged since round 4, and now also out of date on item 1.
Evidence — wenshao assets-pr13179 @ 12a7a523, under pr13179/r5/:
- the two figures and the two patches;
results/: the trial-merge conflict log, the matrix with main's executor (including the absolute read), every (a)/(b)/(c)/(c′)/(d)/(e) / outage / 401 / deleted / unknown console, the raw SSE capturesse-idle-resume.raw, and static-check and candidate-suite logs;harness/: shape (e) and the absolute-read vector.
中文版
第 5 轮真实环境验证 — #13179 @ 1780afcd(macOS)
接第 1、2–3、4 轮。本轮覆盖两轮 autofix(9eecbc5c0e、1780afcd3c)与当前 main 933dc0a614。
结论
- 合并阻塞项——与 main 冲突。 PR 已无法合并:
managed-runtime-tool-executor.ts与今天合入 main 的 feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166 冲突。feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166 恰好在第 1 项改动的那段代码里加入了基于 realpath 的路径约束,取代了第 1 项。真实栈上,main 的执行器拒绝了第 1 轮发现的全部三种越界读取,外加一次绝对路径读取(第 1 项按设计把绝对路径留给 Harness 处理)。- 我验证过的解法:执行器取 main 一侧,再把 PR 两条约束测试的期望文案改掉(共两行)。改完后 cli 测试计划 935/935 通过。
- 维护者已经触发了
/resolve,它落地的形态应当就是这样。
- 面板——第 4 轮 F1 的修复在这个服务端上不生效。 autofix 第 1 轮落地了我第 4 轮 F1 的修复和
read_file测试;第 2 轮(R11-1)随后把存活过期改为"连接送达过帧才生效"。- 在这里,续传空闲会话时一个帧都不送,只有
:keepalive注释行,而客户端会丢弃它们。所以在真实栈上过期永远不会触发:(c) 404、(c′) 502、真实中断和 401 期间,全部又要等下一个事件,与 main 完全相同。 - 这相对 main 不算回退。但声称"空闲重连无错时过期"的那个用例,用的是一个回放帧来模拟空闲重连,而这个服务端从不发送这种帧。
- 一个小候选把 keepalive 计为应答:它保留 R11-1 的黑洞保证,并在真实服务端上于恢复后 20–28 秒清掉红条。
- 在这里,续传空闲会话时一个帧都不送,只有
- 已修好且保持:
- (a) summary 轮询吃到 404,6.1 秒清掉;(b) 启动时的 session 读吃到 404,3.1 秒加载;
- 启动竞态:20/20 页第一次就停;
- (d) "Older history" 翻页失败的红条,现在由下一个流事件清掉(与 main 一致;
9439da72会把它一直压在实时回合上); - 被删除会话的提示一直保持。
运行内容
- 代码树。 我在本地把
1780afcd试合并到 main933dc0a614,唯一的冲突取 main 一侧解决,因此执行器与 main 逐字节一致,其余是 PR 的 8 个文件。 - 重建,均来自这次合并:Spring jar,以及打包的 CLI、Harness 和 worker。
- 臂,全部同时看同一个会话:
- main;
- 上一个 head
9439da72(同一棵树,只把use-managed-session.ts换回旧版); - head;
- head 加 keepalive 候选。
- 静态检查。 web-shell managed 332/332;cli 与 web-shell 的
tsc、eslint、prettier 全部干净。cli 测试计划 933/935:2 个失败就是 PR 的约束测试,仍在断言旧文案(见第 1 节)。 1780afcd上的 CI。 测试与构建工作流全部通过(Qwen Code CI、SDK Java、Serve A/B、web-shell visuals、tui-parity)。两个红色的label/authorize是 2026-10-05 22:25 被取消的运行。
1. 合并冲突:main 的 #13166 取代了第 1 项
使用 main 的执行器跑的路径约束矩阵(试合并,真实 Spring + Broker + 打包 worker):
工具调用(会话 cwd 为 <root>/child) |
Harness | 第 1 轮,旧 main 的 worker | 现在 main 的 worker(#13166) |
|---|---|---|---|
read_file lnk2/outside-secret.txt(Workspace 中已有的链接) |
正常 | ✕ 把区外内容返回给了模型 | ✓ 拒绝 |
读 lnk4/a.txt,随后 lnk4 被换成链接,再读一次 |
正常 | ✕ 把区外内容返回给了模型 | ✓ 拒绝 |
read_file ../../secret.txt |
被绕过 | ✕ 把区外内容返回给了模型 | ✓ 拒绝 |
read_file /…/ws/secret.txt(绝对路径) |
被绕过 | 第 1 轮未测(PR 的检查按设计 R7-2 放过绝对路径) | ✓ 拒绝 |
写 / 改 / ../ / 绝对路径 |
两种都测 | 被 Harness 或 file history 拒绝 | 相同 |
写 inside-child.txt |
两种都测 | 写入 | 写入 |
解法。 执行器取 main 的版本。PR 的两条测试保留,把期望文案改成 'is not within the Session working directory'(conflict-resolution-test-messages.patch,共 2 行,也可直接应用到 1780afcd)。这样 cli 测试计划 935/935 通过。main 自己的套件已经覆盖了相对 ../ 的读和写(managed-context-worker.test.ts,如 ../web/secret.txt、../web/pwned.txt),所以 PR 的这两条测试改文案后保留或直接删掉都可以。
描述。 之后第 1 项应改述为"已被 #13166 取代"。标题里"worker path containment"这一半,在本 PR 中已经不再对应任何代码改动。
2. 存活过期:R11-1 的门槛在这个服务端上永远不会触发
服务端实际发送的内容。 抓了 40 秒空闲续传(afterSequence = 会话最新序号)的原始字节:在最新序号之后没有任何帧,只有大约每 15 秒一次的 :keepalive 注释行。java-managed-agent-client 会丢弃注释(if (!hasContent) return undefined; // heartbeat or comment),所以对空闲会话来说,delivered 永远不会变成真,if (delivered) expireAnswered('stream') 永远不会执行。
| 一次性失败,之后健康且空闲 | main | 9439da72 |
head 1780afcd |
+ 候选 |
|---|---|---|---|---|
| (c) 流重连吃到一次 404 | 等下一个事件 | 8.7 s 清掉 | 等下一个事件 | 20.5 s 清掉 |
| (c′) 同一次重连吃到一次 502 | 等下一个事件 | 等下一个事件 | 等下一个事件 | 21 s 清掉 |
| 真实停掉 Spring 60 s | 恢复 60 s 后仍在 | 仍在 | 恢复 60 s 后仍在 | 恢复后 28.3 s 清掉 |
| 网关 401 持续 60 s | 45 s 后仍在 | 仍在 | 45 s 后仍在 | 清掉 |
| (e) 404 之后,下一次重连被黑洞 30 s(连接开着,不发任何字节) | 保持,之后等下一个事件 | — | 保持,之后等下一个事件 | 整个黑洞期间都保持(无空档),50.6 s 清掉 |
候选修复 —— candidate-keepalive.patch;可直接应用到 1780afcd。
- 改动。
streamEvents新增可选参数onAlive,每收到一个完整的心跳或注释帧就调用一次(流末尾的残帧不算)。- provider 把
onAlive透传下去。 - hook 的
onAlive把这次连接标为已送达;如果存活检查点已过,就补做它一直在等的过期。
- 为什么 R11-1 仍然成立。 被黑洞的连接永远不会发 keepalive,所以门槛和 catch 里的恢复逻辑都不需要改。
- 规模。 源码在客户端、provider、hook 三处共 +26/−2;测试 +124:
- 一个客户端用例:每个 keepalive 都会上报,残帧不会;
- 两个 hook 用例:一次 502 和一次 404 之后,重连空闲且只发 keepalive,keepalive 在 3 秒检查点之后到达。三个用例在 head 上都失败。
- 测试。 web-shell managed 335/335 通过。
- 真实栈。 见上表各行。被删除会话的提示仍然保持,因为每次重连都在毫秒级返回 404,连接不会开到能收到 keepalive 的时候。
3. 保持项与说明
- 启动竞态。 20/20 页第一次就停。
- 会话被删除(240 秒,每臂 2 个面板)。 head 每面板 33 / 30 个请求,main 为 160 个。head 的阶梯封顶后平均间隔 16.8 秒,与理论值 16.5 秒一致。之前一次单面板运行出现过 42 个请求;我用每臂 2 个面板并排重跑后,判断那是抽样波动。
- 重复 id。 Turn 流式进行中读取 transcript 326 次,没有重复(各轮累计 2 340 次)。
- PR 正文。 自第 4 轮以来没有变化,现在在第 1 项上也已过时。
证据 —— wenshao assets-pr13179 @ 12a7a523,位于 pr13179/r5/ 下:
- 两张图和两份补丁;
results/:试合并的冲突日志,使用 main 执行器的约束矩阵(含绝对路径读取),(a)/(b)/(c)/(c′)/(d)/(e)、中断、401、删除、不存在会话的所有控制台输出,原始 SSE 抓包sse-idle-resume.raw,以及静态检查和候选套件日志;harness/:形态 (e) 与绝对路径读取向量。
Conflict resolution: - managed-runtime-tool-executor.ts: take main's realpath/ownership containment (glob admission, #13166/#13265/#13291), which subsumes this branch's lexical admitsDirectory check; the branch's two containment regression specs now assert main's 'Session working directory' message. - use-managed-session.ts: keep this branch's per-answer-authority signals model (the merged panel renders its stoppedReason contract); main's errorOwnership writers model covered the same ground and is superseded. - use-managed-session.test.tsx: union both suites; main's error-ownership specs are retimed onto this branch's jittered failure ladder (rung-1 3000..5999ms), and three specs pinning the replaced slot machine are dropped (open-but-silent establishment clearing, fixed reveal priority, persistent stall slot) with surviving coverage recorded in the round's test-weakening.json.
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Autofix round — PR #13179 (base-conflict resolution)Feedback triage
Conflict dispositions
No inline threads existed this round, so no Verification
中文说明Autofix 本轮处理 — PR #13179(基线冲突解决)反馈分类
冲突处置
本轮没有行内讨论串,因此没有 验证
🧪 Gate advisory — this round weakened or removed pre-existing tests (machine-measured, not agent-authored):
The round recorded evidence for each (below, agent-authored). Weakening is sound only when the pinned behaviour itself was wrong or the coverage demonstrably survives elsewhere — read each reason against the diff. · 本轮弱化或删除了既有测试(门自动测量,非 agent 文本)。下列理由由 agent 撰写:仅当被钉住的行为本身有误、或覆盖确有替代时才成立,请对照 diff 逐条审阅。
Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
🩺 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 |
…onnects (#13179) On an idle Session the server sends no frames after the resume head, only :keepalive comments, which the client dropped — so the R11-1 proof-of-life gate (expire a standing stream verdict only once the reconnect has answered) never fired, and a one-off 404/502, an outage, or a 401 period kept its banner until the next event. streamEvents now reports each complete heartbeat/comment frame through a new onAlive callback (a trailing fragment is a mid-frame disconnect and is not reported), the Java provider forwards it, and the hook counts it as the attempt's answer: it marks the attempt delivered and, when the proof-of-life point has already passed, lands the expiry the timer was holding. The black-hole guarantee is unchanged — a silent connection never sends a heartbeat — and the catch-side verdict restore still covers a keep-alive-credited attempt that goes on to fail.
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Autofix round — PR #13179 @
|
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterℹ️ No screenshot changed against the PR base — but this PR edits 1 render-shaping file:
Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
Qwen Code did not run conflict resolution for this request. PR #13179 does not currently have merge conflicts with main. |
Real-stack verification, round 6 — #13179 @
|
| Scenario | main | head |
|---|---|---|
| Real Spring outage, 60 s | 47 requests per panel (38/min) | 13 requests per panel (10.5/min) |
| Gateway 401, 60 s | 40.9/min | 13/min |
| Watched Session deleted, 240 s | 160 / 160 requests per panel | 30 / 34 |
| Unknown Session id, 90 s | 31–33 bootstrap attempts per page | 1 attempt on 10/10 pages |
It also keeps the merge fast path, and the deleted-Session line stays up.
2. (g) A keep-alive-expired verdict is resurrected when the healthy connection later drops
Setup. One reconnect was answered 404. The next connection was healthy, and its keep-alive cleared the line at about 21 s. At 40 s that connection was dropped abruptly. To get a real network error into the browser, the page talked to the recording proxy directly, because vite's dev proxy turns an upstream drop into a hang.
Result, 2/2 runs:
- main and the candidate: a transient "network error" from 40.5 to 58.5 s, then cleared.
- the head: the 37-second-old 404 restored as a terminal stop over the same window.
Cause.
- The proof-of-life timer captures the standing terminal verdict (
expiredVerdictMessage) for R11-1's restore. - The keep-alive path lands the expiry but leaves that capture armed; only a genuinely new frame clears it.
- So when the keep-alive-certified connection fails later, the catch runs
stop('stream', expiredVerdictMessage). The real network error is then recorded as transient, and the monotonicity guard keeps the restored terminal verdict standing.
Who sees it. Any panel whose stream once got a definite answer and whose next long-lived connection then ends by error: a server deploy, a load-balancer drain or a client network change.
Candidate — candidate-no-resurrect.patch; applies cleanly to 2061be72.
- Change. When a keep-alive lands the expiry, also drop the capture (
expiredVerdictMessage = undefined). That is one statement, plus a comment. R11-1's slow-failing attempt never sends a keep-alive, so its restore still works. - Unit witness. A 404, then a keep-alive at 8 s, then a network error at 68 s. On the head,
stoppedReasoncomes back as'session gone'; with the candidate it stays undefined anderroris'network error'. - Tests. The managed suite passes 403/403 (402 + 1), including the R11-1 restore specs.
3. Notes
-
The fix(web-shell): managed session UI correctness from the #12692 R2 review #13342 merge. Autofix round 3 kept the PR's per-leg hook and dropped three of fix(web-shell): managed session UI correctness from the #12692 R2 review #13342's hook specs; they are listed in
test-weakening.jsontogether with the coverage that replaces them. On the real stack, main and the merged head behave the same in every recovery shape above.- Left over: fix(web-shell): managed session UI correctness from the #12692 R2 review #13342's
onEstablishedplumbing (thestreamEvents(…, onOpen, …)parameter, the provider pass-through, and the interface doc) has no caller in the merged tree, because the hook only usesonAlive. Either remove it or wire it.
- Left over: fix(web-shell): managed session UI correctness from the #12692 R2 review #13342's
-
CI
Test (ubuntu-latest). The two failures areroutes tool_call through a hidden deferred tool in ACP: build errorandaccounts for delegated work when a tool_call tool result returns (validation_error), both on the bridged-validation message. The sequence:- fix(core): keep nested tool_call arguments open on Responses; name the target in bridged validation errors #12901 changed that message on main at 14:10.
- The test expectations were only updated by feat(hosted): show captured inputs in native approval cards #13407 at 16:53.
- This run (16:25) built
2061be72, whose main parent is from 16:22.
Both tests pass locally on main
ac497aeeand on the trial merge. Another merge of main, or a re-run after update-branch, should clear it. -
PR body. Still not updated: it still describes a "hard stop" on a definite 4xx, and still lists item 1 as a code change.
-
Inconclusive run, kept for the record. My first attempt at (g) used Playwright's offline mode, and Chromium does not break an established stream that way. That run is kept in
results/under a name that says so.
Evidence — wenshao assets-pr13179 @ 4e4cb558, under pr13179/r6/:
- the figures and
candidate-no-resurrect.patch; results/: every (a)/(b)/(c)/(c′)/(d)/(e)/(g) / outage / 401 / deleted / unknown console, and the static-check, candidate-suite and duplicate-id logs (0 duplicates in 348 reads);harness/:u8-restore.mjs, plus the direct-to-proxy mode (CORS and abrupt drop).
中文版
第 6 轮真实环境验证 — #13179 @ 2061be72(macOS)
接第 1、2–3、4、5 轮。本轮覆盖 autofix 第 3–4 轮(ae0f9f863f 冲突合并、f82bab116e keepalive)以及当前 main ac497aee。
结论:第 5 轮的两项问题都已解决,在真实栈上成立,PR 现在也能干净合入 main。keepalive 与 R11-1 的"恢复"逻辑组合后带出一个小的新问题,附了一行候选,我建议合并前带上。其余没有阻塞项。
- 冲突。 完全按建议解决:执行器与 main(feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166)逐字节一致,两条约束测试改用 main 的文案。第 1 项现在只剩测试。
- keepalive。 在真实服务端上生效。流重连吃到一次 404 / 一次 502 后,红条分别在 21.1 / 20.8 秒清掉;真实中断后,恢复 20.1 秒时清掉。30 秒黑洞期间红条一直保持(R11-1 得以保留)。
- 新问题 (g): 设想一次重连吃到 404,下一条健康连接上的 keepalive 把它清掉了。如果这条长连接之后因网络错误断开,那个旧的 404 会作为终止判定重新出现。在真实服务端上,这意味着一个正常运行的会话上方显示 "The Session was not found." 约 18 秒,直到下一次重连收到 keepalive 才消失(2/2 次复现)。
- CI。 红色的
Test (ubuntu-latest, Node 22.x)(acp-integration/session/Session.test.ts中 2 条用例)是从 main 继承来的,不是本 PR 引起的(见"说明")。
运行内容
- 代码树。
2061be72本地试合并到 mainac497aee,无冲突。相对 main 的差异就是 PR 的 13 个文件,执行器与 main 无差异。 - 重建,均来自这次合并:Spring jar,以及打包的 CLI、Harness 和 worker。
- 臂,全部同时看同一个会话:
- main;
- 上一个 head(同一棵树,
use-managed-session.ts用1780afcd的版本,即不含 keepalive 那个提交); - head;
- head 加候选。
- 试合并上的静态检查。 cli 测试计划 935/935,web-shell managed 402/402;cli 与 web-shell 的
tsc、eslint、prettier 全部干净。
1. 真实栈
keepalive 提交的前后对比。 同一棵树,只有 hook 不同:
- 之前(
1780afcd的 hook):(c)、(c′)、(e) 全部要等下一个事件,在 40–70 秒。 - 之后(head):(c) 在 21.1 秒清掉,(c′) 在 20.8 秒,(e) 在 50.7 秒,黑洞期间没有空档。
与 main 对比。 main 现在会在流(重新)建立时清掉流失败(#13342 的 onEstablished)。这个服务端在第一个 keepalive 时才建立响应,所以 main 与 PR 现在在同一时刻清掉红条:(c)、(c′)、(e)、中断和 401 都是如此。(a)、(b)、(d) 也相同。
PR 相对 main 仍然多出的部分:
| 场景 | main | head |
|---|---|---|
| 真实停掉 Spring 60 秒 | 每面板 47 个请求(38 次/分钟) | 每面板 13 个请求(10.5 次/分钟) |
| 网关 401 持续 60 秒 | 40.9 次/分钟 | 13 次/分钟 |
| 正在看的会话被删除,240 秒 | 每面板 160 / 160 个请求 | 30 / 34 个 |
| 不存在的会话 id,90 秒 | 每页启动 31–33 次 | 10/10 页只启动 1 次 |
此外还保留了合并快路径,被删除会话的提示也会一直保持。
2. (g) 健康连接后来断开时,已被 keepalive 过期的判定又被恢复出来
设置。 一次重连被回了 404;下一条连接是健康的,它的 keepalive 在约 21 秒时清掉了红条;40 秒时这条连接被粗暴断开。为了让浏览器收到真正的网络错误,页面直接连录制代理,因为 vite 的开发代理会把上游断开变成挂起。
结果(2/2 次):
- main 与候选: 40.5–58.5 秒显示临时性的 "network error",之后清掉。
- head: 同一时段内,37 秒前的那个 404 被作为终止判定恢复出来。
原因。
- 存活计时器会为 R11-1 的恢复逻辑记下当前的终止判定(
expiredVerdictMessage)。 - keepalive 路径完成了过期,却没有清掉这份记录;只有真正的新帧才会清掉它。
- 所以当这条已被 keepalive 证明健康的连接后来失败时,catch 会执行
stop('stream', expiredVerdictMessage)。真实的网络错误随后只被记为 transient,单调性保护让恢复出来的终止判定一直保持。
影响范围。 流曾经收到过一次确定性应答、之后的长连接又以错误结束的任何面板,例如服务端部署、负载均衡摘流、客户端网络切换。
候选修复 —— candidate-no-resurrect.patch;可直接应用到 2061be72。
- 改动。 keepalive 完成过期时,同时清掉那份记录(
expiredVerdictMessage = undefined),一条语句加一段注释。R11-1 针对的"慢慢失败的连接"从不发送 keepalive,所以它的恢复逻辑照常工作。 - 单测见证。 先 404,8 秒时 keepalive,68 秒时网络错误。head 上
stoppedReason变回'session gone';候选上它保持 undefined,error为'network error'。 - 测试。 managed 套件 403/403 通过(402 + 1),包括 R11-1 的恢复用例。
3. 说明
-
与 fix(web-shell): managed session UI correctness from the #12692 R2 review #13342 的合并。 autofix 第 3 轮保留了 PR 的按腿 hook 模型,删掉了 fix(web-shell): managed session UI correctness from the #12692 R2 review #13342 的 3 条 hook 用例;这些用例和替代它们的覆盖都列在
test-weakening.json里。真实栈上,main 与合并后的 head 在上面每一种恢复形态里表现都相同。- 遗留: fix(web-shell): managed session UI correctness from the #12692 R2 review #13342 的
onEstablished链路(streamEvents(…, onOpen, …)参数、provider 透传、接口文档)在合并后的代码里没有调用方,因为 hook 只用onAlive。建议删除,或者把它接上。
- 遗留: fix(web-shell): managed session UI correctness from the #12692 R2 review #13342 的
-
CI
Test (ubuntu-latest)。 两条失败用例是routes tool_call through a hidden deferred tool in ACP: build error和accounts for delegated work when a tool_call tool result returns (validation_error),都与桥接校验的文案有关。时间线:- fix(core): keep nested tool_call arguments open on Responses; name the target in bridged validation errors #12901 在 14:10 修改了 main 上的这条文案;
- 测试的期望值直到 16:53 才由 feat(hosted): show captured inputs in native approval cards #13407 更新;
- 这次运行(16:25)构建的是
2061be72,它合入的 main 停在 16:22。
这两条在本地当前 main
ac497aee和试合并上都通过。再合一次 main,或在 update-branch 之后重跑,应该就能转绿。 -
PR 正文。 仍未更新:仍写着遇到确定性 4xx 时"硬停止",仍把第 1 项列为代码改动。
-
无效运行,留档备查。 我第一次测 (g) 用的是 Playwright 的离线模式,而 Chromium 的离线模式不会断开已建立的流。那次运行保留在
results/中,文件名注明了这一点。
证据 —— wenshao assets-pr13179 @ 4e4cb558,位于 pr13179/r6/ 下:
- 各张图与
candidate-no-resurrect.patch; results/:(a)/(b)/(c)/(c′)/(d)/(e)/(g)、中断、401、删除、不存在会话的所有控制台输出,以及静态检查、候选套件和重复 id 的日志(348 次读取,重复 0 次);harness/:u8-restore.mjs,以及直连代理模式(CORS 与粗暴断开)。
|
CI attribution for the Root cause: the lane compiled a stale merge ref whose upstream-main half was internally inconsistent on
Action taken: the branch now merges current upstream ( |
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
onEstablishedleft with no production caller after the hook switched toonAlive— already reported (issue comment 6022424066)- PR body/title stale on items 1 and 2 — already reported (issue comments 6018332618 and 6022424066)
Not reviewed: reverse audit — stopped before round 5 by the review time budget.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:反向审计——评审时间预算不足,未能开始第 5 轮。
— qwen3.8-max via Qwen Code /review (v0.25.0)
| // leg's own slot, so re-asserting on every stall keeps the | ||
| // banner up for the whole stall, and a resync that advances | ||
| // the cursor retires it. | ||
| expireFinal('stream'); |
There was a problem hiding this comment.
[Suggestion] R1-31: a successful resync no longer clears a transient stream failure, unlike at the merge base
In the non-advancing-resync branch the expiry was narrowed from clearing the stream leg's error to expireFinal, which deletes only a final record. A transient stream-leg failure recorded by an earlier attempt therefore survives a reconnect that demonstrably succeeded — a gap frame was delivered and snapshot(true) fulfilled. At the merge base snapshot()'s setState carried error: undefined, so every successful resync cleared it; the deleted comment in this very branch stated that contract ('clears a transient error once resyncs succeed again'). The sibling branch at :452 uses the wider expireAnswered('stream') for exactly this reason, so the two resync outcomes now disagree.
A stream attempt throws connection reset by peer (no status, so a transient record). The reconnect then delivers a stream_gap and the durable read fulfils, but the head does not advance — the normal shape for a low-latency daemon whose resync completes in under BASE_RETRY_DELAY_MS (a slower pass would fire the proof-of-life timer, where delivered is already true and expireAnswered clears it by the other route). The panel keeps displaying 'connection reset by peer' for two further passes (~6 s) while the stream is connected and its durable read is succeeding, telling the user the connection is broken when it is not.
Witness:
three-arm probe (attempt 1 throws `TypeError('connection reset by peer')`; attempts 2+ yield `{...event(1), type:'stream_gap'}`; `getTranscript` always resolves `transcript(1)` so the head never advances) — `BASE (ac497aee): t~3050 error=undefined`, `PR (as filed): t~3050 error="connection reset by peer"`, `PR + fix (:427 -> expireAnswered('stream')): t~3050 error=undefined`. With the fix applied the whole `client/components/managed` directory is 406/406 green (402 pre-existing + 4 probe tests), so the pins at `:631` and `:5063/:5070/:5082` hold and the `entry.stall` exemption at `:207` keeps the stall banner up.
Suggested fix. Use expireAnswered('stream') at :427, matching the error-free-completion branch at :452. expireAnswered is a strict superset of expireFinal for every record constructible today (the one exception, a record both final and stall, cannot arise: a StreamStallError carries no status, so isNonRetryableClientError is false and stop() never fires on it) and it preserves the stall exemption.
The fix must not violate this. The entry.stall carve-out at use-managed-session.ts:207 and the StreamStallError doc at :39-42 require that an armed stall survives an error-free answer, so the wider expiry must be expireAnswered (which exempts stall records) and not an unconditional retire('stream').
Acceptance criterion. use-managed-session.test.tsx — a case where a transient stream failure stands, the reconnect then delivers a stream_gap whose resync fulfils without advancing the cursor, and the assertion is that latest?.error is undefined after that resync. Reverting :427 to expireFinal must turn it red.
中文说明
在“未推进的 resync”分支里,过期动作从“清除 stream leg 的错误”收窄成了 expireFinal,而它只删除 final 记录。因此早先某次尝试留下的临时 stream leg 失败,会在一次明确已成功的重连(收到了 gap 帧且 snapshot(true) 已 fulfil)之后继续存在。在合并基线上 snapshot() 的 setState 带有 error: undefined,所以每次成功的 resync 都会清掉它;本分支中被删掉的注释也写明了这一契约(“clears a transient error once resyncs succeed again”)。同级的另一分支 :452 正是为此使用更宽的 expireAnswered('stream'),于是两种 resync 结果现在互相矛盾。
触发场景:某次流尝试抛出 connection reset by peer(无 status,属临时记录)。重连随后送来 stream_gap,持久化读取成功但游标未推进——这正是低延迟守护进程的常见形态(resync 在 BASE_RETRY_DELAY_MS 之内完成;更慢的话存活计时器会触发,delivered 已为真,expireAnswered 会从另一条路清掉它)。于是在流已连接、其持久化读取正在成功的情况下,面板还会继续显示约 6 秒(两个 pass)的“connection reset by peer”,告诉用户连接已断,而事实并非如此。
见证(Witness)。 判定所依据的实际运行输出见上方英文 Witness 代码块;日志与命令输出按约定逐字保留,不作翻译。
修复不得违反的前提。 见上方英文 “The fix must not violate this.” 一节,其中引用了具体常量与 file:line,属代码标识,按约定保留原文。
验收标准。 见上方英文 “Acceptance criterion.” 一节,内容为测试名与断言,属代码标识,按约定保留原文。
— qwen3.8-max via Qwen Code /review (v0.25.0)
There was a problem hiding this comment.
Deferred to the next round (batch cap): widening expireFinal to expireAnswered at the non-advancing-resync branch is probe-verified, but it changes what a successful gap resync clears and needs its own red-green witness (transient stream failure, then a non-advancing resync, assert error clears). Next round, together with the other resync-semantics leftovers.
中文说明
推迟到下一轮(批次上限):把未推进 resync 分支的 expireFinal 放宽为 expireAnswered 已被探针验证,但它改变了“成功的 gap resync 清除哪些记录”的语义,需要自己的红绿见证(先有临时 stream 失败,再来一次未推进的 resync,断言 error 被清除)。下一轮与其他 resync 语义遗留项一起处理。
There was a problem hiding this comment.
[Suggestion] R1-31: still stands — the replay guard conflates an exact duplicate with a strictly older frame.
Because the guard treats any frame at or below the cursor as a replay, a strictly older frame (a disordered or overlapping tail from a resume) is handled identically to an exact duplicate, so the cursor and the delivered/retire decisions cannot distinguish "already seen" from "out of order", and an out-of-order tail is dropped as if it were a duplicate. The site is untouched by this round's commit.
This round's verification independently confirmed two things about the neighbourhood that make the entry worth keeping open rather than closing as intentional. First, the guard's placement is load-bearing for a different reason: delivered = true at :395 sits above the guard at :400, which is the mechanism behind a separate low-confidence observation in this round's terminal report. Second, the repo's own tests model overlapping and disordered tails as duplicates (managed-session-messages.test.ts:194/:231; java-managed-agent-client.test.ts:277/:300, "retrying the same cursor replays this frame identically"), so the conflation is not merely theoretical — it is the shape the surrounding contract already assumes.
Witness:
witness: not run - carried ledger entry re-ruled against unchanged code. The delta
(264823fc..d8cf0a0aca) touches use-managed-session.ts only at :352, :366-367, :372-376 and
:386-390; the replay guard and cursor write at :395-402 are unchanged. The neighbouring
facts quoted above were established by this round's verification runs.
Distinguish an exact duplicate (event.id === lastEventId) from a strictly older frame (event.id < lastEventId) and handle the out-of-order case explicitly rather than folding it into the replay path — or, if folding is intended, say so where the guard is and adjust the tests that currently describe an "overlapping or disordered tail".
The witness to add: a test feeding a strictly older frame after a newer one must observe the distinct handling; it goes red while both cases share the replay branch.
中文说明
[建议] R1-31:依然存在——重放守卫把“完全重复的帧”与“严格更旧的帧”混为一谈。
由于该守卫把任何等于或低于游标的帧都当作重放,一个严格更旧的帧(来自 resume 的乱序或重叠尾部)会与完全重复的帧被同样处理,因此游标以及 delivered/retire 的决策无法区分“已见过”与“乱序”,乱序尾部会像重复帧一样被丢弃。本轮提交没有触碰该位置。
本轮验证独立确认了该邻近区域的两件事,使这一条目值得继续保留而不是当作有意行为关闭。第一,守卫的位置因另一个原因而关键::395 处的 delivered = true 位于 :400 的守卫之上,而这正是本轮终端报告中另一条低置信度观察背后的机制。第二,仓库自身的测试把重叠与乱序尾部建模为重复帧(managed-session-messages.test.ts:194/:231;java-managed-agent-client.test.ts:277/:300,“以相同游标重试会逐字节重放该帧”),因此这种混淆并不只是理论上的——它正是周边契约已经假定的形态。
修复方式:区分完全重复(event.id === lastEventId)与严格更旧的帧(event.id < lastEventId),显式处理乱序情形,而不是把它并入重放路径——如果并入是有意的,请在守卫处写明,并调整目前描述“重叠或乱序尾部”的那些测试。
需要补的见证:一个在较新帧之后送入严格更旧帧的测试,必须能观察到两者的不同处理;在两种情形共用重放分支时它会变红。
— qwen3.8-max via Qwen Code /review (v0.25.0)
There was a problem hiding this comment.
Deferred to the next round by the per-round batch bound (eight findings landed this round). Direction note for that round: the repo's own contract tests model an overlapping or disordered tail as an identical replay (managed-session-messages.test.ts 'still reorders and deduplicates an overlapping or disordered tail', java-managed-agent-client.test.ts 'retrying the same cursor replays this frame identically'), so distinguishing event.id === lastEventId from a strictly older frame needs a decided handling for the out-of-order case first — or a deliberate documentation-only resolution at the guard. The thread stays open.
中文说明
按每轮处理批次上限延后到下一轮(本轮已落地八条发现)。给下一轮的方向说明:本仓库自身的契约测试把重叠或乱序的尾部建模为逐字节重放(managed-session-messages.test.ts 的 “still reorders and deduplicates an overlapping or disordered tail”,以及 java-managed-agent-client.test.ts 的 “retrying the same cursor replays this frame identically”),因此要区分 event.id === lastEventId 与严格更旧的帧,需要先确定乱序情形的处理方式——或者在守卫处明确写成有意为之的文档化解决。此线程保持开启。
| error, | ||
| error instanceof SnapshotLegError ? error.leg : 'session', | ||
| ); | ||
| delayMs = failureRetryDelayMs(snapshotFailures++); |
There was a problem hiding this comment.
[Suggestion] R1-27: snapshotFailures never decays, so a rung earned in one incident is spent in a later one
snapshotFailures is the only backoff counter in the file with no decay path other than the success of the operation it counts, and that operation (snapshot(true)) runs only on a stream_gap. failures is reset by delivery or by a long-lived connection (:481-482) and gapStalls by an advancing cursor (:394, :414); snapshotFailures has neither. Its value also feeds delayMs, which is the stream reconnect delay, so a counter earned by resync failures is spent on a different loop.
One incident produces three failed resyncs, leaving snapshotFailures at 3. Hours later the connection is healthy and a single unrelated stream_gap arrives; the resync fails once, and failureRetryDelayMs(4) puts the stream reconnect at the top of the ladder (~24-30 s) instead of the 3 s floor — a slow recovery bought by an incident that ended long ago, on a loop the counter does not measure.
Witness:
probe (round-3 verifier, isolated copy) — an incident/idle/incident sequence: the second incident's first failure starts at the ladder rung the first incident left behind rather than at rung zero; baselines 96/96 and 402/402 green.
Suggested fix. Give the counter a decay path matching the other two — reset snapshotFailures when a resync succeeds (it already does at :424) AND when the stream has been healthy for a pass, or scope the delay it feeds to the resync rather than the stream reconnect.
The fix must not violate this. use-managed-session.ts:46-48 pins rung zero at exactly BASE_RETRY_DELAY_MS for the gap-recovery cadence that ManagedSessionsPage.test.tsx:1692 asserts at a 2999/3000 ms boundary, so a decay must restore exactly 3000 ms and not a jittered value.
Acceptance criterion. A new case in use-managed-session.test.tsx: three failed resyncs, then a long healthy idle period, then one further resync failure — asserting the next stream delay is at rung zero rather than at the accumulated rung. Removing the decay must turn it red.
中文说明
snapshotFailures 是本文件中唯一没有衰减路径的退避计数器——除了它所计数的那个操作成功之外没有任何归零方式,而那个操作(snapshot(true))只在 stream_gap 时执行。failures 会因交付或连接存活足够久而重置(:481-482),gapStalls 会因游标推进而重置(:394、:414),snapshotFailures 两者都没有。它喂给的还是 delayMs,也就是流的重连延迟,于是一个由 resync 失败累积起来的计数被花在另一个循环上。
触发场景:一次事故造成三次 resync 失败,把 snapshotFailures 留在 3。数小时后连接一直健康,此时来了一个毫不相关的 stream_gap;resync 失败一次,failureRetryDelayMs(4) 就把流重连推到阶梯顶端(约 24–30 秒)而不是 3 秒地板——一次早已结束的事故换来一次缓慢恢复,而且发生在该计数器并不度量的那个循环上。
见证(Witness)。 判定所依据的实际运行输出见上方英文 Witness 代码块;日志与命令输出按约定逐字保留,不作翻译。
修复不得违反的前提。 见上方英文 “The fix must not violate this.” 一节,其中引用了具体常量与 file:line,属代码标识,按约定保留原文。
验收标准。 见上方英文 “Acceptance criterion.” 一节,内容为测试名与断言,属代码标识,按约定保留原文。
— qwen3.8-max via Qwen Code /review (v0.25.0)
There was a problem hiding this comment.
Deferred to the next round (batch cap): giving snapshotFailures a decay path touches the reconnect-delay contract pinned at exactly 3000 ms by ManagedSessionsPage.test.tsx:1692, and the acceptance probe (incident / long idle / incident) needs careful fake-timer staging. Kept out of this round so the Critical cluster stayed small; next round.
中文说明
推迟到下一轮(批次上限):为 snapshotFailures 增加衰减路径会触及被 ManagedSessionsPage.test.tsx:1692 精确钉在 3000 毫秒的重连延迟契约,且其验收探针(事故 / 长空闲 / 再事故)需要小心的假定时器编排。为保证本轮 Critical 簇足够小而未纳入;下一轮处理。
| const rest = { ...current.signals }; | ||
| delete rest.transcript; | ||
| return rest; |
There was a problem hiding this comment.
[Suggestion] R1-14: nothing pins that a successful older-page load clears only the transcript leg
The delete's scope is load-bearing and unpinned: existing cases cover only the opposite direction. The neighbouring test that looks like it covers this (keeps the stall alert across a successful older-page load and a poll blip, test:5009-5074) never reaches the mutated branch — its stall was armed by three gap resyncs that each ran snapshot(true) to success, ending in retire('session'); retire('transcript'), so at the page load current.signals?.transcript is already undefined and the ternary at :557 takes the {} arm where pristine and mutant are identical.
If a refactor widens this delete, a user who clicks 'Load older' while a stream-leg record stands (an armed stall warning, or a terminal stream verdict) has that banner erased by a page fetch that certifies nothing about the stream, and it does not come back until the stream leg fails again.
Witness:
mutation + probe pair in an isolated copy — `return rest;` replaced with `return {};` leaves 96/96 hook tests green (only the probe fails); probe `P-F14` (stream-leg 502 transient standing, then a failed page click, then a successful retry): `MID error="older page fetch failed (500)" stoppedReason=undefined` / `PRISTINE error="upstream unavailable"` (stream leg survives the page success) / `MUTANT error=undefined` (every leg wiped) with `AssertionError: expected undefined to be 'upstream unavailable'`.
Suggested fix. Add a case where a stream-leg record is standing (a transient failure or an armed stall) and loadOlder() then succeeds, asserting the stream record is still displayed.
The fix must not violate this. test:5009's stall survival and keeps a paging failure when a clean stream pass follows both depend on the transcript leg being cleared by a page success, so the pin must assert the OTHER legs survive rather than that none is cleared.
Acceptance criterion. use-managed-session.test.tsx — the new case must go red when the success path deletes every signal instead of only transcript (measured green today, 96/96).
中文说明
这个删除的作用范围承载逻辑却没有被钉住:现有用例只覆盖了相反方向。看起来覆盖了它的邻近测试(keeps the stall alert across a successful older-page load and a poll blip,test:5009-5074)根本到不了被变异的那条分支——它的 stall 是由三次 gap resync 武装的,而每一次都成功执行了 snapshot(true),末尾都会 retire('session'); retire('transcript'),所以在分页加载时 current.signals?.transcript 已是 undefined,:557 的三元表达式走 {} 那一支,原始版与变异版完全相同。
触发场景:如果某次重构放宽了这个删除,一个在 stream leg 记录立着(武装的 stall 警告,或终止的流判定)时点击“加载更早历史”的用户,会看到那条提示被一次对流毫无证明力的分页请求抹掉,而且要等到 stream leg 再次失败才会回来。
见证(Witness)。 判定所依据的实际运行输出见上方英文 Witness 代码块;日志与命令输出按约定逐字保留,不作翻译。
修复不得违反的前提。 见上方英文 “The fix must not violate this.” 一节,其中引用了具体常量与 file:line,属代码标识,按约定保留原文。
验收标准。 见上方英文 “Acceptance criterion.” 一节,内容为测试名与断言,属代码标识,按约定保留原文。
— qwen3.8-max via Qwen Code /review (v0.25.0)
There was a problem hiding this comment.
Deferred to the next round (batch cap): the pin is test-only (a standing stream-leg record, then a successful loadOlder, asserting the stream record survives), but this round already rewrote one long verdict test and added four more; one more careful paging/stream interaction case belongs to the next round's test-hardening batch.
中文说明
推迟到下一轮(批次上限):该钉测试仅涉及测试代码(先让 stream leg 记录立着,再成功执行一次 loadOlder,断言 stream 记录仍然存活),但本轮已经重写了一条较长的判定测试并新增四条;这条需要仔细编排的分页/流交互用例归入下一轮的测试加固批次。
…13179) A keep-alive landed the proof-of-life expiry but left the attempt's verdict capture armed, so a later drop of that certified connection restored the terminal verdict the heartbeat had refuted, and the monotonicity guard then hid the real network error. Disarm the capture on every heartbeat and never arm it for a heartbeat-certified attempt. Also: a heartbeat no longer resets the stream reconnect ladder (it certifies the connection, not the data path), and the session page renders the hook's stoppedReason and error channels as sibling alerts instead of collapsing them, suppressing exact repeats so one server condition is announced once. Pins the catch-side verdict restore (replay shape), the new-frame disarm past the proof-of-life point, the verdict mirror clears, and a pre-proof-of-life heartbeat's early-expiry gate.
|
🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下: Autofix round — PR #13179Commit Resolved in code
Resolved as coverage pins (behavior confirmed correct, now witnessed)
Deferred to the next round (batch cap ~8) — reason recorded on each threadR1-4 (dead Issue-level items
Mutation probes (guard deleted → focused test fails → restored → green)
Verification
中文说明Autofix 本轮总结 — PR #13179提交 已在代码中解决
作为覆盖钉测试解决(行为确认正确,现在有了见证)
推迟到下一轮(批次上限约 8 条)——每条线程上已记录原因R1-4(死字段 议题级评论
变异探针(删除保护 → 聚焦测试失败 → 恢复 → 转绿)
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 8 selected review thread(s). · 已关闭全部选中的 8 条评审线程。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline. 7 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- onEstablished left with no production caller while its plumbing and a contradictory interface doc stay behind — already reported (issue comment 6022424066, whose 'Left over' bullet names the callback, the provider pass-through and the inter…
2 candidate finding(s) this round's reviewers re-derived matched entries already carried on this PR and were set aside before verification (R1-4, R1-11) — a matched posted finding is ruled in the previous-round status as always, and a matched deferral stays on the standing deferral record.
Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (gh >= 2.72.0 is required for closingIssuesReferences; the fetch exited 0 with no issue sections), so fidelity was judged against the PR body's own 'Linked Issues: None closed' statement plus a zero-match closing-keyword sweep and the narrated-incident replay, not against issue evidence.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 3)": none of my six hunks went unwalked; the two checks I did not execute are mutation runs — every "this reds it" claim above is a traced data-flow argument against…; "agent invariant-a (packages/web-shell/client/components/man…": none — every check in my slice (mutable fields, timers, collections) was walked to a conclusion.; "agent reverse-audit (round 2)": the hunks of packages/web-shell/client/components/managed/use-managed-session.test.tsx past diff line 231 were not walked — my chunk ends mid-file at the harn….
Not reviewed: reverse audit — stopped before round 5 by the review time budget.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/components/managed/use-managed-session.ts:634 — [review] stoppedLeg is returned by the hook but has no production reader (grep: 44 hits = 1 write site + 43 test reads), so a terminal transcript verdict and a termin…packages/web-shell/client/components/managed/ManagedSessionsPage.test.tsx:1738 — [review] the sticky-verdict test asserts with querySelector, i.e. only the first of up to three alerts, so removing retire('stream') leaves the transient alert…packages/web-shell/client/components/managed/use-managed-session.ts:205 — [review] the verdict-mirror clears in expireAnswered (:205) and expireFinal (:191) are load-bearing and unpinned: deleting either ships 100/100 green and reintroduces…packages/web-shell/client/components/managed/use-managed-session.test.tsx:5405 — [review] the rewritten comment claims 'the re-arm beat below pins' a displaced stall's return, but that beat re-arms from a reset gapStalls; resetting gapStall…packages/web-shell/client/components/managed/use-managed-session.test.tsx:669 — [review] the test named 'restores a terminal stream verdict when a reconnect fails past the proof-of-life point' never expires anything, so deleting the catch-s…packages/web-shell/client/components/managed/use-managed-session.ts:435 — [review] a successful non-advancing gap resync calls expireFinal('stream'), which cannot clear a transient record, so a refuted 'Failed to fetch' alert stands ~9 s an…packages/web-shell/client/components/managed/use-managed-session.ts:218 — [review] Promise.allSettled with no deadline on either read wedges the bootstrap for the whole mount when /transcript hangs and the session read rejects; getTranscrip…packages/web-shell/client/components/managed/use-managed-session.test.tsx:613 — [review] nothing pins the per-attempt lifetime of expiredVerdictMessage: hoisting its declaration ships 100/100 green while re-enabling resurrection of a verdic…packages/web-shell/client/components/managed/use-managed-session.test.tsx:4062 — [review] the new 50 ms settle wait is awaited outside any act scope (the only such site of 17 in the file), so hook updates land unbatched — act warnings in 8 …
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。 7 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
本轮评审重新推导出的 2 条候选发现与本 PR 已携带的条目匹配,已在验证前搁置(R1-4, R1-11)——被匹配的已发布条目照常在上一轮状态区裁定,被匹配的延后条目仍保留在延后清单记录中。
未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (gh >= 2.72.0 is required for closingIssuesReferences; the fetch exited 0 with no issue sections), so fidelity was judged against the PR body's own 'Linked Issues: None closed' statement plus a zero-match closing-keyword sweep and the narrated-incident replay, not against issue evidence.
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 3)":none of my six hunks went unwalked; the two checks I did not execute are mutation runs — every "this reds it" claim above is a traced data-flow argument against…;"agent invariant-a (packages/web-shell/client/components/man…":none — every check in my slice (mutable fields, timers, collections) was walked to a conclusion.;"agent reverse-audit (round 2)":the hunks of packages/web-shell/client/components/managed/use-managed-session.test.tsx past diff line 231 were not walked — my chunk ends mid-file at the harn…。
未审查:反向审计——评审时间预算不足,未能开始第 5 轮。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 9 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.25.0)
|
🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下: Address-review round — PR #13179Committed as Addressed
Deferred to the next round (batch bound — 9 findings landed)
Notes
Verification
中文说明审查处理轮次总结 — PR #13179已提交为 已处理
延后到下一轮(批次上限——本轮已落地 9 条)
说明
验证
Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. ( 中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 9 selected review thread(s). · 已关闭全部选中的 9 条评审线程。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R2-1 the catch-side restore re-books an expired verdict with a fresh seq, suppressing another leg's live record (use-managed-session.ts:511) — already reported (comment 4202165052, R1-12, same catch-side restore block); overlap drop, not re…
- the signals type doc states an authority rule the new-frame path contradicts (use-managed-session.ts:78) — already reported (comment 4202165039, R1-26)
- SnapshotLegError.code is written and never read (use-managed-session.ts:35) — already reported (comment 4202164980, R1-4)
- the bootstrap catch hand-inlines failed()'s terminality predicate (use-managed-session.ts:353) — already reported (comment 4202165030, R1-11)
- the attempt-3 branch never reads the streamVerdictMessageRef mirror it claims to pin (use-managed-session.test.tsx:477) — already reported (comment 4202164966, R1-21)
- the mid-attempt checkpoint sits outside the proof-of-life window its comment names (use-managed-session.test.tsx:697) — already reported (round-2 deferral list, use-managed-session.test.tsx:669)
- both alert checkpoints read only the first role=alert node (ManagedSessionsPage.test.tsx:1753) — already reported (round-2 deferral list, ManagedSessionsPage.test.tsx:1738)
- stoppedLeg is a public return field with no production read site (use-managed-session.ts:669) — already reported (round-2 deferral list, use-managed-session.ts:634)
Not reviewed: reverse audit — stopped before round 5 by the review time budget.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/components/managed/use-managed-session.ts:356 — [probe] a superseded effect run can set the shared endedRef (the only cross-run write not behind the abort guard), so a late 404 from run A tears down healthy run Bpackages/web-shell/client/components/managed/use-managed-session.ts:497 — [probe] the clean-pass summary refresh is a second, uncounted entry point to getSession, so the 30s ladder never bounds the aggregate session-read rate (26 calls/60s …packages/web-shell/client/components/managed/use-managed-session.ts:659 — [probe] the stream-over-transcript half of the standing precedence is exercised by no test (swapping the tuple leaves 417 passed)packages/web-shell/client/components/managed/use-managed-session.test.tsx:994 — [probe] the expiredVerdictMessage reset onAlive gained as the R1-1 fix has no witness (deleting it leaves 106/106 green)packages/web-shell/client/components/managed/use-managed-session.test.tsx:4526 — [probe] the stall-recovery spec cannot distinguish retiring the stall record from escalating it to a terminal verdict
Convergence: round 3 posted 6 inline comment(s), 4 of them reported for the first time; the previous round posted 11 (5 new). Findings keep coming back to the same files: packages/web-shell/client/components/managed/use-managed-session.ts (findings in rounds 1, 2; 2 more now); packages/web-shell/client/components/managed/use-managed-session.test.tsx (findings in round 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. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:反向审计——评审时间预算不足,未能开始第 5 轮。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 6 条行内评论,其中 4 条是首次提出;上一轮发布了 11 条(其中 5 条首次提出)。发现反复回到同一批文件:packages/web-shell/client/components/managed/use-managed-session.ts(第 1、2 轮已出过发现,本轮又有 2 条);packages/web-shell/client/components/managed/use-managed-session.test.tsx(第 2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.25.0)
…d re-arms (#13179) A reconnect whose first frame lands past its own proof-of-life point — a slow accept replaying the tail from lastEventId — set delivered but never landed the expiry the point was holding, so a terminal verdict the stream had already refuted kept rendering over a live, answered connection. The frame path now mirrors the heartbeat path: a frame past the point lands the expiry (a replay counts) and it is never restored by that connection's later drop. Also: when both bootstrap reads reject, a definite transcript answer is now recorded on its own leg before the session error's throw discards it, provided the session leg is transient and will retry (a terminal session answer keeps owning the display); the session-leg re-arm is bounded per session so an alternating definite-4xx/success backend can no longer remount the effect every rung-zero cadence forever; the stall guard drops an operand no observation can reach (every stall re-assertion is preceded by the same pass's successful resync, which retires any competing record first); SnapshotLegError loses its never-read code field; the bootstrap catch reuses failed()'s terminality predicate; and the signals doc now states the cross-leg rule the frame path actually implements. Pins the late-replay expiry, the both-reject classification, the re-arm bound, the masked stream record in the paging spec, and the message-keyed alert node identity through the 2-to-1 transition.
|
🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — PR #13179Growth audit (required this window)Verdict: sound ( Actionable feedback
Standing findings also resolved in this round
Files changed
No base-conflict resolution was needed ( Verification
中文说明Autofix 本轮总结 — PR #13179增长审计(本窗口必需)结论:sound(见 workdir 中的 可执行反馈
本轮一并解决的存量发现
变更文件
无需解决基线冲突( 验证
Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete and the PR's diff grew src 47 / test 735 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. ( 中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 47 / 测试 735 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 8 selected review thread(s). · 已关闭全部选中的 8 条评审线程。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
5 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- the late-frame disarm's two-sided replay rule and the stale :418-420 comment — already reported (comment 4208617735, use-managed-session.ts:469)
- N2 no witness for the onAlive disarm at use-managed-session.ts:454 — already reported (round-3 deferral list, use-managed-session.test.tsx:994)
- N3 clearing assertions cannot distinguish retire from escalate — already reported (round-3 deferral list, use-managed-session.test.tsx:4526)
- N4 stoppedLeg has no production read site — already reported (round-2 deferral list, use-managed-session.ts:634)
- N11 the MAX_SESSION_REARMS spec never reads latest — already reported (this round's R1-10 fix witness, use-managed-session.test.tsx:280)
Not reviewed: reverse audit — stopped before round 5 by the review time budget.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/components/managed/use-managed-session.test.tsx:4530 — [review] the 50 ms settle sleep is a probabilistic band-aid over a live race in the assertions that follow it
Convergence: round 4 posted 4 inline comment(s), 4 of them reported for the first time; the previous round posted 6 (4 new). Findings keep coming back to the same files: packages/web-shell/client/components/managed/use-managed-session.ts (findings in rounds 1, 3; 3 more now); packages/web-shell/client/components/managed/use-managed-session.test.tsx (findings in round 3; 1 more now). The rate of new findings is not falling. 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. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 5 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:反向审计——评审时间预算不足,未能开始第 5 轮。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 4 条行内评论,其中 4 条是首次提出;上一轮发布了 6 条(其中 4 条首次提出)。发现反复回到同一批文件:packages/web-shell/client/components/managed/use-managed-session.ts(第 1、3 轮已出过发现,本轮又有 3 条);packages/web-shell/client/components/managed/use-managed-session.test.tsx(第 3 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.25.0)
Round 4 review follow-ups on the managed session hook. retire() deleted the standing terminal verdict even after the session-leg re-arm budget was spent, and cleared endedRef, so an alternating definite-4xx/success backend ended at a healthy-looking empty panel with no alert and no path back but reload(). Past the bound the verdict now survives the poll's successes. The bootstrap catch's endedRef write was the one cross-run shared-ref write without an abort guard: a superseded run's late definite session answer could arm the live run's re-arm path and tear its effect down on the next poll success. It is now guarded like its siblings. record() let an identical terminal answer re-stamp its record with a fresh seq, suppressing a newer live record on another leg; a repeat booking of the same final message is now a no-op. Also: the both-reads-reject spec's retry claim now counts the bootstrap's own transcript read instead of getSession, which the poll shares; the onAlive disarm and the late-replay landing disarm each get a witness (replay-armed capture, then a heartbeat or a late replay, then the connection's drop); the stall recovery beats now assert retired-not-escalated; and the proof-of-life comment names the late-frame disarm alongside the heartbeat's.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Round summary — review round 4 (head 79d77b2)This round addresses review round 4 ( Findings
Declined after probing (answered on its thread, left open)
Not in this round (recorded, not silently dropped)
Mutation probes (each guard's witness)Each probe mutated exactly one line of
Verification
中文说明轮次总结 —— 第 4 轮评审(head 79d77b2)本轮处理第 4 轮评审( 发现逐条处理
探测后婉拒(已在原线程回复,保持打开)
本轮未处理(已记录,非静默丢弃)
变异探测(每个守卫的见证)每个探测只改
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧵 Resolved all 9 selected review thread(s). · 已关闭全部选中的 9 条评审线程。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
|
Closeout status at The automated review completed without posting a new Critical, but its report explicitly says it was partial: reverse audit and one test-ordering verification were not completed. Its Suggestion deferrals and the five existing unresolved Suggestion threads remain follow-up. A fresh local GitHub query confirms the closing-issue set is empty, consistent with the PR body's “None closed”; this only fills that metadata gap. @qqqys please review the complete current diff and those dispositions before approval. The historical UI/runtime reports retain their original revision attribution; no fresh soak acceptance or full-review approval is claimed by this status update. |
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- use-managed-session.ts:721 — stall-vs-paging reveal precedence (R1-8) — already reported (comment 4202165063, re-report 4204452977)
- use-managed-session.ts:269 — bootstrap allSettled deadline — already reported (round-2 deferral list, use-managed-session.ts:218)
- use-managed-session.ts:556 — clean-pass summary read counter — already reported (round-3 deferral list, use-managed-session.ts:497)
- use-managed-session.ts:725 — stoppedLeg production reader (N4) — already reported (round-2 deferral list, use-managed-session.ts:634; declined with evidence in issue comment 6049734072)
Not reviewed: issue-fidelity — the closing-issue reference set could not be fetched (gh >= 2.72.0 is required for closingIssuesReferences; this environment has gh 2.45.0), so fidelity was judged against the PR body's own 'Linked Issues: None closed' statement plus the narrated-incident replay, not against issue evidence.
Not reviewed: R5-6 (use-managed-session.ts:523 expire-before-book ordering pin) — the review time budget ended the loop before a verifier ruled on it.
Not reviewed: reverse audit — stopped before round 5 by the review time budget.
4 Suggestion(s) were drafted inline past the resolved critical posting floor — the floor engaged early: the first-time-finding rate has not fallen for 2 consecutive round(s); the CLI moved them into the deferral list below (floor enforcement).
Deferred under the convergence posture (round 5, not a blocker) — the floor engaged early: the first-time-finding rate has not fallen for 2 consecutive round(s) — recorded, not requested in this round:
packages/web-shell/client/components/managed/use-managed-session.ts:213 — [review] R5-1: the spent re-arm budget is refillable only by a *completed* bootstrap ( :410 ) or a session switch ( :137-140 ) — never by reload() , which is the rec…packages/web-shell/client/components/managed/use-managed-session.ts:140 — [review] R5-2: neither refill of the MAX_SESSION_REARMS budget has a witness — deleting this one, or the completed-bootstrap one at :410 , leaves the whole suite g…packages/web-shell/client/components/managed/use-managed-session.ts:410 — [review] R5-2: neither refill of the MAX_SESSION_REARMS budget has a witness — deleting this one, or the session-boundary one at :137-140 , leaves the whole suite …packages/web-shell/client/components/managed/use-managed-session.test.tsx:280 — [review] R5-3: this round halved the spec's advance window ( for (let step = 0; step < 10; step++) → < 4 at :273 , i.e. t=30000 → t=12000) but left the comm…packages/web-shell/client/components/managed/use-managed-session.test.tsx:4646 — [review] The failed-page-fetch spec asserts only invariance and…packages/web-shell/client/components/managed/use-managed-session.test.tsx:6095 — [review] The comment claims both stall exits are pinned below, but…packages/web-shell/client/components/managed/use-managed-session.ts:535 — [review] The gap-resync catch neither restores the…packages/web-shell/client/components/managed/use-managed-session.ts:545 — [probe] The error-free-completion branch expires the stream leg…packages/web-shell/client/components/managed/use-managed-session.ts:566 — [review] The catch-side restore re-stamps an old terminal answer…packages/web-shell/client/components/managed/use-managed-session.ts:578 — [review] The reconnect-ladder reset measures attempt age, not…
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查(原文为英文):issue-fidelity — the closing-issue reference set could not be fetched (gh >= 2.72.0 is required for closingIssuesReferences; this environment has gh 2.45.0), so fidelity was judged against the PR body's own 'Linked Issues: None closed' statement plus the narrated-incident replay, not against issue evidence.
未审查(原文为英文):R5-6 (use-managed-session.ts:523 expire-before-book ordering pin) — the review time budget ended the loop before a verifier ruled on it.
未审查:反向审计——评审时间预算不足,未能开始第 5 轮。
4 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论——发布下限因首次发现速率连续 2 轮未下降而提前生效;CLI 已将其移入下方延后清单(下限强制执行)。
收敛姿态下延后(第 5 轮,非阻断)——发布下限因首次发现速率连续 2 轮未下降而提前生效——已记录,本轮不要求修改:共 10 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.25.0)




What this PR does
Three small robustness fixes for the hosted Managed session path, each pinned by new unit tests:
Why it's needed
These paths turn ordinary disturbances into lasting damage or nuisance: the worker resolved file paths with no containment of its own, leaving a single validation layer between tool input and the host filesystem; a broker outage or a misconfigured token made every open panel poll two endpoints every three seconds forever, and rejoin in lockstep on recovery (thundering herd); and per-event full re-sorting made long sessions progressively slower on the UI thread. None changes any success-path behavior; all are exercised by tests that fail without the fix.
Reviewer Test Plan
How to verify
Run the two affected suites from their package directories and confirm the new cases pass and all pre-existing cases (including the panel's 2999ms gap-recovery boundary) stay green:
Key behaviors to look at in the new tests: a relative file path escaping the workspace is rejected and nothing is written outside it, while a path inside still succeeds; after a 404 the panel makes exactly one attempt and stops, and after a 500 it retries with backoff and recovers; a strictly-newer event tail merges without rewriting prior event identity, while an overlapping or disordered tail is still re-sorted and deduplicated.
Evidence (Before & After)
N/A — robustness paths, no user-visible happy-path change.
Tested on
Environment (optional)
Unit tests only (run
npm run buildfirst for dist prerequisites): 888 cli managed-serve tests + 204 web-shell managed tests pass;tsc --buildand eslint/prettier clean on the touched files.Risk & Scope
Linked Issues
None closed — follow-up audit items are tracked in #13180–#13185.
中文说明
本 PR 为托管(Hosted)Managed 会话链路做三处小幅健壮性修复,每项都配了新单测:
动机:这些路径过去会把普通扰动放大成持久损伤或噪音——worker 自己没有任何 containment 校验,工具输入到宿主文件系统之间只剩一层校验;broker 故障或 token 配置错误会让每个打开的面板每三秒对两个端点开火永不停歇,恢复瞬间同步重连(惊群);每事件全量排序让长会话在 UI 线程上越来越慢。三条都不改变成功路径行为,且全部有「无此修复即失败」的测试钉住。
验证:在各自包目录运行 packages/cli 的五个 managed serve 套件(888 通过)、packages/web-shell 的 components/managed(204 通过),并确认既有 2999ms gap 恢复边界不变;重点看新测试:逃逸路径被拒且区外不落盘、区内正常写入;404 后恰好一次尝试即停、500 后退避重试并恢复;严格更新的尾追加不重写既有事件 identity,乱序重叠输入仍去重排序。
说明:普通事务提交的不确定失败重试曾随本 PR 准备,随后刻意撤出——hosted 故障闸门 driver 目前钉死「commit 恰好尝试一次」,该重试必须与「有界、字节级一致、effect-once」的 driver 语义重写一同落地,已在 #13182 跟踪。