Skip to content

fix(managed-agent): harden the managed panel failure lifecycle and worker path containment - #13179

Open
wenshao wants to merge 26 commits into
mainfrom
fix/managed-agent-quality-hardening
Open

wenshao wants to merge 26 commits into
mainfrom
fix/managed-agent-quality-hardening

Conversation

@wenshao

@wenshao wenshao commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Three small robustness fixes for the hosted Managed session path, each pinned by new unit tests:

  1. The managed runtime worker now rejects relative file paths that resolve outside the registered workspace. Only the resolve-and-admit decision changed; the shell working-directory argument already enforced the identical check, so this removes an asymmetry in the worker's own defense layer (the harness boundary validation is unchanged). A traversal-written file is rejected and nothing lands outside the workspace; legitimate relative writes keep working.
  2. The managed panel's three polling/reconnect loops (bootstrap snapshot, event-stream reconnect, summary poll) now share one backoff policy: a three-second floor preserved exactly for the first failure (keeps the documented gap-recovery cadence), exponential growth to a 30-second cap with jitter for repeated failures, and a hard stop when the service gives a definite 4xx answer (e.g. deleted session or unauthorized) instead of polling it forever.
  3. Merging newly streamed events no longer rebuilds and re-sorts the entire transcript per delta: a strictly-newer tail is appended directly, and the general dedupe-and-sort path still handles overlapping or disordered input with identical semantics.

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:

cd packages/cli && npx vitest run src/serve/managed-runtime-file-history.test.ts src/serve/managed-context-worker.test.ts src/serve/managed-runtime-tool-v3-routes.test.ts src/serve/remote-shell-result-publication.test.ts src/serve/managed-mcp-runtime.test.ts
cd packages/web-shell && npx vitest run client/components/managed

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

OS Status
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Unit tests only (run npm run build first for dist prerequisites): 888 cli managed-serve tests + 204 web-shell managed tests pass; tsc --build and eslint/prettier clean on the touched files.

Risk & Scope

  • Main risk or tradeoff: the worker's containment check uses the same admitted-workspace predicate as the shell directory argument. Absolute file paths remain caller-trusted at this layer (the harness boundary rejects absolute incoming paths); symlink escapes resolved through relative paths are out of scope here.
  • Not validated / out of scope: a broader packaged change — retrying the plain journal commit on uncertain failures — was prepared and then deliberately dropped here: the hosted fault-gate drivers currently pin a single commit attempt, so that retry must land together with a driver restatement of bounded, byte-identical, effect-once retry semantics. It is tracked in fix(managed-agent): retry loops without terminal states and a permanently wedged projection #13182. Real-broker failover soak and the remaining audit items (broker authentication, DB amplification) are likewise out of scope.
  • Breaking changes / migration notes: none.

Linked Issues

None closed — follow-up audit items are tracked in #13180–#13185.

中文说明

本 PR 为托管(Hosted)Managed 会话链路做三处小幅健壮性修复,每项都配了新单测:

  1. managed runtime worker 现在拒绝解析后落在注册工作区之外的相对文件路径。只改了解析加准入这一个判断——shell 的 directory 参数早已执行同样的检查,本次消除 worker 自身防线的不对称(harness 边界校验不变)。逃逸写入被拒绝且区外不落盘;合法相对写入不受影响。
  2. Managed 面板的三条轮询/重连环(启动快照、事件流重连、摘要轮询)现在共用一套退避:首次失败保留三秒地板(不打乱既有 gap 恢复节奏),连续失败指数增长到 30 秒封顶并加 jitter,服务给出确定性 4xx(如会话已删除、未授权)时彻底停止,不再永久打枪。
  3. 流式事件合并不再对每条 delta 全量重建并排序整个 transcript:严格更新的尾部直接追加,重叠或乱序输入仍走原去重加排序路径,语义零变化。

动机:这些路径过去会把普通扰动放大成持久损伤或噪音——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 跟踪。

…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.
@wenshao

wenshao commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

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, packages/core/src/managed-runtime, and the Web Shell managed panel). Every High/Critical claim was re-verified line-by-line against main (a7deb01) before being acted on.

The good news first: point-level quality is consistently high — tenant predicates on store queries, optimistic concurrency, digest chains, workspace-mount containment (startsWith+NOFOLLOW+toRealPath), and the TS↔Java wire contract (spot-checked acquire/commit fields, headers, lease bounds, error envelopes, SSE frames) all held up, with zero contract drift found.

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]>
@wenshao

wenshao commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code-ci-bot Addressed in 4e085e6 (and now on HEAD 99c465f after the main merge): the plain /transactions:commit retry was withdrawn from this PR, so the fault gates' single-attempt contract no longer conflicts with it — Hosted process fault gates / MySQL 8.4 / Java 21 is green on the current head. The diagnosis in your note was exactly right: both drivers pin exactly one targeted commit attempt (hosted-store-failure-driver.ts "Harness must not retry the failed write", hosted-shell-output-driver.ts injections counter, whose receipt commit travels the plain non-publication path).

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, replayed: true forwarding on reply cases, and recoveryBlocked verdicts waiting out the bounded backoff). The packaged implementation and unit tests are preserved in this branch's history (ea4e495) and the concrete driver adaptation is written up in #13182.

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]>
@wenshao

wenshao commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

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: failureRetryDelayMs carries a comment that rung zero must stay exactly 3000 because ManagedSessionsPage pins the gap-recovery cadence on a 2999/3000ms boundary in another file; and mergeManagedEvents documents that the append path assumes current is already ascending and deduplicated (stream state and transcript pages are), so unsorted first arguments must go through the general path.

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 normalizeWorkspaceRelativePath rejecting absolutes and .. at the boundary, which is unchanged — that is why the PR body says only the worker's own defense-in-depth changed.

wenshao and others added 2 commits October 2, 2026 18:44
…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]>
@wenshao

wenshao commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

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 (failed()), one health model per endpoint (delivery/liveness reset for the stream, an independent ladder for the snapshot read, a private counter for the poller), and one observability slot (stoppedReason that retires on later authoritative success) — most of rounds 1–4's siblings were different projections of those three rules applied inconsistently. The deferred probes (one constant doing four jobs; rung-zero comment naming) are acknowledged and consciously not acted on: splitting BASE_RETRY_DELAY_MS into purpose-named constants is a naming refactor, and the hook semantics it feeds are now pinned per site, so it would be churn without new information. Recorded for any future dedicated cleanup.

Verification for this round: full managed suite 225/225; seven mutants, each redding exactly the test that targets it (keep-alive return, snapshot success reset, snapshot clearing write, alert precedence, alert chain removal, stream ladder flatten, bootstrap ladder flatten); eslint/prettier/tsc --build clean; the mock type-shape probe (tsc --noEmit --strict over the hook spec) reports zero diagnostics.

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]>
@wenshao

wenshao commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

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 (getHeaders is re-per-request), while 404/410-class session-existence answers stay terminal and self-retiring.

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 Promise.all snapshot site") deliberately skipped because it makes verdicts unretireable in the recovery case the rule exists for.

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 components/managed suite (227/227) plus seven targeted mutants, but did not re-run the CLI integration lane.

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/tsc --build clean.

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.
@wenshao

wenshao commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

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 preserveLoadedPages; definite-4xx reads keep this branch's record-don't-kill policy with the snapshot's own failure ladder; the stall guard fires only when three consecutive resyncs fail to advance; the liveness/delivery reset and the two independent counters are unchanged. Both test suites coexist, normalized onto the shared reactHarness probe shape. M5a (#13167) landed mid-merge — the branch was merged onto it too, and the M5a worker/incarnation asserts verified present afterwards.

The three Criticals.

  • R6-1: snapshot legs are tagged so a transcript-leg definite 4xx records stickily but keeps the bootstrap alive; only a session-leg definite answer is terminal. New spec: 'keeps bootstrapping when only the transcript leg is definitively gone'.
  • R6-2: the summary poller records a definite 4xx and keeps backing off on its ladder instead of dying for the mount; the cadence spec now states this policy by name ('backs off and keeps polling after a definite summary answer instead of stopping'). Rung zero and the retirement-through-snapshot shape are untouched.
  • R6-3: the clean-close summary read has its own try/catch — recorded, never a kill — matching the gap branch it sits next to. New spec: 'survives a definite session-leg answer from the clean-close summary read'.

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, managed-runtime-incarnation, failure counters, stoppedReason, failed()). Full verification on the final tree: web-shell managed 293/293, hook file 50/50, CLI managed suites 954/954 across 6 files, core managed-runtime 1930/1931 with the single failure being managed-session-authority.hook-scale's 15s timeout — a load flake that passes in isolation; tsc --build and eslint/prettier clean. One note: fixing the one rebuilt-splice in the hook spec file produced an order-dependent wall-clock race in the corrupt-run spec, stabilized with a single documented macrotask pump; it is a test-harness concern only.

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]>
@wenshao

wenshao commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Two red lanes, two different causes — one fixed in 07a9756, one investigated below with a rerun requested.

Lint & Static (Check VS Code companion notices are up-to-date) — my mistake, fixed. The merge commit had absorbed a NOTICES.txt regeneration produced from my local worktree, whose store resolves a few transitive versions differently from CI's clean frozen-lockfile env (is-fullwidth-code-point, fdir, picomatch). Restored to main's file in 07a9756 and re-pushed.

Hosted process fault gates / MySQL 8.4 — HostedWorkspaceToolTurnIT.packagedHarnessUsesSavedWorkspacesThroughRealBrokerWorkerAndSqlStore, expected: 0 but was: 1. Investigation so far, with the evidence for calling it not-this-PR:

  1. Named-failure source is unreachable by this PR's diff. The driver state that fails the turn is only thrown at packages/cli/src/serve/hosted-workspace-tool-turn.ts:726 ("Hosted Workspace profile refused a tool call", on the undeclared-tool / duplicate-callId / wasOutputTruncated / hadIncompleteArguments guard). git diff --name-only $(git merge-base 4e6a02ad86 origin/main)..HEAD and the M5a-side diff contain neither that file nor its call sites. The files/rewind 409s right before the turn error are the undo side-effect of the already-failed turn, not the cause.
  2. Cross-branch smoking gun. The identical IT, textbook-identical terminal signature (expected: 0 but was: 1, two turns each ending in the same refusal), fired at 10:26 on run 37115037789 on docs/h3-shell-monitor-design — a branch whose diff is docs-only and which does not contain M5a (checked git merge-base --is-ancestor) — 16 minutes before this PR's run. A docs-only branch cannot break a MySQL E2E, and the M5a theory is excluded by its absence there.
  3. Lane history swings the same lane. The lane was green yesterday (nightly on the pre-M5a base) and at 19m03s on this branch's earlier head (run 36941665147); other branches hit the same test today within a narrow window, consistent with lane/infra (MySQL 8.4 self-hosted service + packaged harness) temperature rather than a code regression: the two runs fail deterministically only at the packaged case while every sibling FG class (FG6A/B/D/E/F) passes, including cases that drive packaged file-tool writes — which also clears this PR's one cli-side worker change (admitsDirectory containment) since the refusal is a different error class and a different throw site.
  4. Honest limitation: I cannot reproduce locally — this MAC has no docker/podman CLI and no MySQL service (only brew MariaDB, un-bootstrapped), so no packaged MySQL E2E run was possible; attribution is from the lane logs and the cross-branch run-set, not a local rerun of the IT.

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.

@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /resolve

wenshao added a commit to wenshao/qwen-code that referenced this pull request Oct 6, 2026
@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Real-stack verification, round 5 — #13179 @ 1780afcd (macOS)

Follow-up to rounds 1, 2–3 and 4. This round covers the two autofix rounds (9eecbc5c0e, 1780afcd3c) and the current main 933dc0a614.

Verdict

  1. Merge blocker — conflict with main. The PR no longer merges: managed-runtime-tool-executor.ts conflicts with feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166, merged into main today. feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166 adds a realpath-based containment in exactly the block item 1 changed, and it supersedes item 1. On the real stack, main's executor refuses all three read escapes from round 1, plus an absolute-path read that item 1 deliberately leaves to the Harness.
    • The resolution I validated: take main's side for the executor, then change the expected message in the PR's two containment tests (two lines). The cli plan suites then pass 935/935.
    • /resolve is already requested; this is the shape it should land.
  2. Panel — the round-4 F1 fix does not take effect on this server. Autofix round 1 landed my round-4 F1 fix and the read_file test. Autofix round 2 (R11-1) then gated the proof-of-life expiry on a delivered frame.
    • Here, resuming an idle Session delivers no frame at all, only :keepalive comments, which the client drops. So on the real stack the expiry never fires: (c) 404, (c′) 502, a real outage and a 401 period all wait for the next event again, exactly like main.
    • This is not a regression against main. But the spec that claims "expires … on an error-free idle reconnect" models the idle reconnect with a replay frame this server never sends.
    • A small candidate counts keep-alives as the answer. It keeps R11-1's black-hole guarantee and clears the red line 20–28 s after recovery on the real server.
  3. Fixed and holding:
    • (a) a 404 on a summary poll clears at 6.1 s; (b) a 404 on the bootstrap's session read loads at 3.1 s;
    • the bootstrap race: 20/20 pages stop at the first attempt;
    • (d) a failed "Older history" fetch is now cleared by the next streamed event (main's behaviour; 9439da72 kept it over the live turn);
    • the deleted-Session line stays up.

What ran

  • Trees. I trial-merged 1780afcd into main 933dc0a614 locally, resolving the one conflict to main's side, so the executor is byte-identical to main. Everything else is the PR's 8 files.
  • Rebuilt from that merge: the Spring jar and the packaged CLI, Harness and worker.
  • Arms, all watching the same Session at the same time:
    • main;
    • the previous head 9439da72 (same tree, only use-managed-session.ts swapped back);
    • the head;
    • the head plus the keep-alive candidate.
  • Static checks. Web-shell managed 332/332; tsc for cli and web-shell, eslint and prettier all clean. The cli plan suites pass 933/935: the 2 failures are the PR's containment tests, which still expect the old message (see §1).
  • CI at 1780afcd. The test and build workflows passed (Qwen Code CI, SDK Java, Serve A/B, web-shell visuals, tui-parity). The two red label / authorize entries are cancelled runs from 2026-10-05 22:25.

1. Merge conflict: main's #13166 supersedes item 1

The conflicting block (main vs PR)

main now resolves file_path (relative or absolute), realpaths the target and the Session directory, refuses targets outside the Session directory unless they stay in the mount and belong to no other Session, and answers Path '…' is not within the Session working directory. The PR side is its original admitsDirectory check on relative paths only, which answers …not within any of the registered workspace directories.

Containment matrix with main's executor (trial merge, real Spring + Broker + packaged worker):

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

round 5
why

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.
    • streamEvents takes an optional onAlive and calls it for each complete heartbeat or comment frame (not for a trailing fragment).
    • The provider passes onAlive through.
    • The hook's onAlive marks 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 capture sse-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。

结论

  1. 合并阻塞项——与 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,它落地的形态应当就是这样。
  2. 面板——第 4 轮 F1 的修复在这个服务端上不生效。 autofix 第 1 轮落地了我第 4 轮 F1 的修复和 read_file 测试;第 2 轮(R11-1)随后把存活过期改为"连接送达过帧才生效"。
    • 在这里,续传空闲会话时一个帧都不送,只有 :keepalive 注释行,而客户端会丢弃它们。所以在真实栈上过期永远不会触发:(c) 404、(c′) 502、真实中断和 401 期间,全部又要等下一个事件,与 main 完全相同。
    • 这相对 main 不算回退。但声称"空闲重连无错时过期"的那个用例,用的是一个回放帧来模拟空闲重连,而这个服务端从不发送这种帧。
    • 一个小候选把 keepalive 计为应答:它保留 R11-1 的黑洞保证,并在真实服务端上于恢复后 20–28 秒清掉红条。
  3. 已修好且保持:
    • (a) summary 轮询吃到 404,6.1 秒清掉;(b) 启动时的 session 读吃到 404,3.1 秒加载;
    • 启动竞态:20/20 页第一次就停;
    • (d) "Older history" 翻页失败的红条,现在由下一个流事件清掉(与 main 一致;9439da72 会把它一直压在实时回合上);
    • 被删除会话的提示一直保持。

运行内容

  • 代码树。 我在本地把 1780afcd 试合并到 main 933dc0a614,唯一的冲突取 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.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 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

feedback.md listed no new reviews, inline comments, issue-level comments, or failed checks — the only work this round was the requested merge of origin/main (the branch had last merged main at 69d5db2, before the managed-agent M5b/H3/glob wave and the #13342 R2 fixes landed). Merged as ae0f9f863f with three conflicted files resolved by reading both sides:

Conflict dispositions

  1. packages/cli/src/serve/managed-runtime-tool-executor.ts — took main's side. Main's run() now contains the file arm's realpath + session-ownership containment and the glob arm (from feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166, feat(managed-agent): H3 background Shell and Monitor runtime #13265, feat(managed-agent): Make local Runtime tool outcomes durable (M5b) #13291), which strictly subsumes this branch's small lexical admitsDirectory check on resolved relative paths (the branch's only change to this file). The branch's two containment regression specs survive against main's stronger scheme; their expected message was updated to main's sanitized refusal (not within the Session working directory). The shell-directory admitsDirectory check both sides share is untouched, and its worker-level pins still pass.

  2. packages/web-shell/client/components/managed/use-managed-session.ts — kept the branch's side. The branch replaces the single error field with per-answer-authority signals (session/transcript/stream legs, terminal-verdict monotonicity, proof-of-life gating), and the merged ManagedSessionsPage renders its stoppedReason/stoppedLeg contract, so main's errorOwnership writers model cannot satisfy the page. Main's hook delta was exclusively that writers model — the same concern the signals model already covers per-leg — so nothing from main's hook was lost.

  3. packages/web-shell/client/components/managed/use-managed-session.test.tsx — union of both suites. Restored the createRoot/root scaffolding main's 24 new specs mount with (the branch had moved its own specs to the reactHarness helpers), then reconciled them with the branch's hook:

    • 7 specs passed unmodified (their pinned behaviors coincide with the signals model).
    • 13 specs retimed: they stepped the old fixed 3000ms poll/retry cadence, but the branch's failure ladder jitters rung 1 to 3000–5999ms; each post-failure recovery advance now steps 6000ms, which covers the whole rung. Assertions unchanged. The hook suite ran 5× green under real jitter.
    • 1 spec partially re-pointed: disarms the stall alert once the stream advances again — its mid-stall beat pinned main's persistent stall slot (a stream failure revealed the still-armed stall again after a clean pass). The branch books the stall in the stream leg's own slot, so the failure takes that slot and the clean pass expires it; the beat now asserts that, and the spec's re-arm beat still pins the stall lifecycle. Its two summary-blip beats were re-phased to the branch's leg semantics (a session-leg success retires the blip).
    • 3 specs dropped (recorded in test-weakening.json with surviving coverage named): ends a dropped stream's failure once the reconnect establishes and idles pins bare onEstablished clearing of a failure on an open-but-silent reconnect — the exact behavior this branch's HEAD commit (1780afc) deliberately gates on an answered attempt, with the same scenario pinned under the branch's polarity by keeps a transient stream failure standing through an unanswered reconnect and expires a transient stream failure on an error-free idle reconnect. reveals the stall alert ahead of booked stream and paging conditions and reveals a still-live stream failure ahead of a paging condition pin main's fixed reveal priority and persistent stall slot, which the branch's newest-live-condition display and per-leg stall re-assertion deliberately replace (surviving pins named in the JSON).

No inline threads existed this round, so no resolved-comments.txt and no comment-replies.json. Nothing was declined or escalated; the one genuinely contested semantic (establishment vs. answered-attempt as success evidence) was already decided and pinned by the branch's most recent commit, so the resolution follows it.

Verification

  • COREPACK_HOME=/tmp/corepack-cache npm run build — passed (the sandbox denies corepack's default $HOME/.cache dir, so the cache was redirected; the build itself is clean)
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/web-shell/client/components/managed/ — 389 passed (15 files), run again post-commit: green
  • vitest packages/web-shell/client/components/managed/use-managed-session.test.tsx ×5 — 94/94 each run (flake check of the retimed specs under real jitter)
  • vitest packages/web-shell (full) — 10899 passed (411 files)
  • vitest packages/cli focused: managed-runtime-file-history, managed-runtime-tool-executor, managed-runtime-tool-worker, managed-runtime-provider-worker, managed-context-worker — 979 passed (5 files); file-history + executor re-run post-commit: 86 passed
  • Mutation probe: no new guard or branch was added this round (a conflict merge; every retained behavior is witnessed by the union suite above), so there was nothing to probe — the three dropped specs are covered by the surviving pins named in test-weakening.json
中文说明

Autofix 本轮处理 — PR #13179(基线冲突解决)

反馈分类

feedback.md 中没有新的评审、行内评论、issue 级评论或失败检查——本轮唯一的工作是按要求合并 origin/main(该分支上次合并 main 停在 69d5db2,早于 managed-agent 的 M5b/H3/glob 一批改动和 #13342 的 R2 修复)。合并提交为 ae0f9f863f,三个冲突文件均在通读两侧后解决:

冲突处置

  1. packages/cli/src/serve/managed-runtime-tool-executor.ts —— 采用 main 侧。 main 的 run() 现在包含 file 分支的 realpath + 会话归属收容检查以及 glob 分支(来自 feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166、feat(managed-agent): H3 background Shell and Monitor runtime #13265、feat(managed-agent): Make local Runtime tool outcomes durable (M5b) #13291),完全覆盖本分支对解析后相对路径所做的词法 admitsDirectory 检查(分支对该文件的唯一改动)。分支的两条收容回归测试在 main 更强的方案下仍然成立;其期望报错文案更新为 main 的脱敏拒绝(not within the Session working directory)。两侧共有的 shell 目录 admitsDirectory 检查未动,其 worker 级测试仍然通过。

  2. packages/web-shell/client/components/managed/use-managed-session.ts —— 保留分支侧。 分支把单一 error 字段替换为按应答权威划分的 signals(session/transcript/stream 三条线、终局判定单调性、proof-of-life 门控),且合并后的 ManagedSessionsPage 渲染其 stoppedReason/stoppedLeg 契约,因此 main 的 errorOwnership writers 模型无法满足页面。main 对该 hook 的改动只有这套 writers 模型——signals 模型已按线覆盖同一关注点——因此 main 的 hook 没有任何内容丢失。

  3. packages/web-shell/client/components/managed/use-managed-session.test.tsx —— 两侧测试套件取并集。 恢复了 main 新增 24 条 spec 挂载所需的 createRoot/root 脚手架(分支自己的 spec 已迁到 reactHarness 辅助函数),再让它们与分支的 hook 对齐:

    • 7 条 spec 原样通过(其锚定的行为与 signals 模型一致)。
    • 13 条 spec 重排时序:它们按旧的固定 3000ms 轮询/重试节奏推进,但分支的失败阶梯把第 1 档抖动到 3000–5999ms;每个失败后的恢复推进改为 6000ms,覆盖整个档位。断言不变。hook 套件在真实抖动下连跑 5 次全绿。
    • 1 条 spec 部分改向:disarms the stall alert once the stream advances again——其中段的节拍锚定的是 main 的持久 stall 槽位(流失败后,干净的完成轮次会再次显示仍武装的 stall)。分支把 stall 记在 stream 线自己的槽位里,因此失败会占用该槽位、完成轮次会将其清除;该节拍现在断言这一行为,且该 spec 的重新武装节拍仍然锚定 stall 生命周期。它的两处摘要抖动节拍按分支的线路义重排(session 线的成功会退役抖动)。
    • 删除 3 条 spec(已记录在 test-weakening.json 并点名存续覆盖):ends a dropped stream's failure once the reconnect establishes and idles 锚定的是开而不答的重连仅凭 onEstablished 就清除失败——这正是本分支 HEAD 提交(1780afcd3c)刻意改为以应答尝试为准的行为,同一场景在分支极性下由 keeps a transient stream failure standing through an unanswered reconnect 和 expires a transient stream failure on an error-free idle reconnect 锚定。reveals the stall alert ahead of booked stream and paging conditions 和 reveals a still-live stream failure ahead of a paging condition 锚定的是 main 的固定揭示优先级和持久 stall 槽位,分支刻意用"显示最新存活条件"和按线重新断言 stall 取而代之(存续锚定点已在 JSON 中点名)。

本轮没有行内讨论串,因此没有 resolved-comments.txt 和 comment-replies.json。没有拒绝或升级任何事项;唯一真正存在分歧的语义(以连接建立还是以应答尝试作为成功证据)已由分支最近一次提交决定并锚定,故解决遵循该决定。

验证

  • COREPACK_HOME=/tmp/corepack-cache npm run build —— 通过(沙箱拒绝 corepack 默认的 $HOME/.cache 目录,故重定向缓存;构建本身干净)
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • vitest packages/web-shell/client/components/managed/ —— 389 通过(15 个文件),提交后再次运行:全绿
  • vitest packages/web-shell/client/components/managed/use-managed-session.test.tsx ×5 —— 每次 94/94(真实抖动下对重排时序 spec 的稳定性复查)
  • vitest packages/web-shell(全量)—— 10899 通过(411 个文件)
  • vitest packages/cli 聚焦:managed-runtime-file-history、managed-runtime-tool-executor、managed-runtime-tool-worker、managed-runtime-provider-worker、managed-context-worker —— 979 通过(5 个文件);提交后重跑 file-history + executor:86 通过
  • 变异探针:本轮未新增任何守卫或分支(这是一次冲突合并;保留的每个行为都有上述并集套件作证),因此没有可探针的对象——被删除的三条 spec 由 test-weakening.json 中点名的存续锚点覆盖

🧪 Gate advisory — this round weakened or removed pre-existing tests (machine-measured, not agent-authored):

  • packages/web-shell/client/components/managed/use-managed-session.test.tsx — net 14 assertion(s) removed

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 逐条审阅。

  • packages/web-shell/client/components/managed/use-managed-session.test.tsx: Merge-resolution removal of three main-side specs that pin the replaced errorOwnership slot machine, which this PR deliberately supersedes with per-answer-authority signals. (1) 'ends a dropped stream's failure once the r

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/模型 kimi-k3 · CLI 0.25.0

@qwen-code-review-bot

qwen-code-review-bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 79d77b2, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

✅ 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.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下:

Autofix round — PR #13179 @ f82bab116e

One commit this round: f82bab116e fix(web-shell): expire stream verdicts on heartbeat-answered idle reconnects (#13179).

Feedback disposition

[ic:6018332618] §1 — Merge conflict: main's #13166 supersedes item 1

Decision: already resolved; verified, no code change needed. The merge commit ae0f9f863f (landed before this round) resolved the conflict in exactly the shape the feedback validated:

  • packages/cli/src/serve/managed-runtime-tool-executor.ts is byte-identical to main (git diff origin/main HEAD on that path is empty) — main's realpath/ownership containment was taken.
  • The PR's two containment regression specs (packages/cli/src/serve/managed-runtime-file-history.test.ts:503,544) now assert main's 'is not within the Session working directory' message — the two-line change the feedback prescribed.
  • That suite passes 33/33 (see Verification), and the PR's remaining diff vs the merge-base no longer touches the executor or its worker tests.

Open note for the maintainer (not actionable by this bot): the feedback also asks that the PR description/title be updated to describe item 1 as "superseded by #13166". PR metadata edits are a network write owned by the workflow; this bot has no GitHub credentials, so the stale description is flagged here rather than changed.

[ic:6018332618] §2 — Proof-of-life: the R11-1 gate never fires on this server

Decision: implemented the feedback's keep-alive candidate. Confirmed the mechanism in code before editing: on an idle-Session resume the server sends only :keepalive comment frames, which decodeFrame dropped (return undefined; // heartbeat or comment), so delivered never became true and the proof-of-life timer's expireAnswered('stream') never ran.

Changes (+28/−3 source, +180 tests across 7 files):

  • java-managed-agent-client.ts — streamEvents takes an optional onAlive and calls it for each complete heartbeat/comment frame; a trailing fragment (mid-frame disconnect) is not reported.
  • managed-agent-provider.ts — subscribeEvents options gain onAlive?(): void (optional; other implementers unaffected).
  • java-managed-agent-provider.ts — passes request.onAlive through to streamEvents.
  • use-managed-session.ts — the hook passes onAlive, which marks the attempt as delivered and, once the proof-of-life point has passed, lands the expiry the timer was holding. The catch-side verdict restore is unchanged, so a keep-alive-credited attempt that then fails still restores its terminal verdict.

Why R11-1's black-hole guarantee holds: a black-holed connection never sends a heartbeat, so onAlive never fires — the existing "keeps a … verdict standing through an unanswered reconnect" specs (unchanged) pin that. The deleted-Session banner also stays up: a deleted Session's reconnect answers 404 within milliseconds, before any keep-alive can arrive, and the deleted-Session specs pass unchanged. The stall warning is likewise unaffected — expireAnswered never expires stall entries, and "keeps the stall warning through an idle connection that never advances" passes unchanged.

New witnesses (all fail on the pre-round head, pass now):

  • Client: each complete keep-alive is reported to onAlive; a trailing fragment is not.
  • Provider: keep-alives are forwarded to the subscribe caller.
  • Hook ×2: a 404 (terminal) and a 502 (transient) followed by a keep-alive-only idle reconnect, with the keep-alive landing after the 3 s proof-of-life point — the standing record expires when the keep-alive lands (t=7000), not at the proof-of-life point (t=6000).

Mutation probes performed (guard removed → focused suite red → restored → green):

  1. Hook: dropped if (proofOfLifePassed) expireAnswered('stream') → both new hook specs failed; restored → green.
  2. Client: dropped if (!trailing) onAlive?.() → the new client spec failed; restored → green.
  3. Provider: dropped request.onAlive pass-through → the new provider spec failed; restored → green.

Verification

  • npm run build (with COREPACK_HOME=/tmp/corepack-cache; the sandbox denies the default corepack cache dir with EACCES) — passed
  • npm run typecheck — passed
  • npm run lint — passed (first run flagged require-yield in the new 502 spec; fixed with the yield* []; idiom the sibling spec uses, re-linted clean)
  • npx prettier --check on all 7 touched files — passed (after one --write on the provider spec)
  • npx vitest run client/components/managed/ in packages/web-shell — 15 files, 393/393 passed
  • npx vitest run src/serve/managed-runtime-file-history.test.ts in packages/cli (item-1 containment suite) — 33/33 passed
  • Mutation probes 1–3 above — each failed red with the guard removed, green restored

Not run: real-stack E2E (requires the Spring/Broker/packaged-worker stack; the feedback's own real-stack rows for this candidate showed the red line clearing 20–28 s after recovery — CI and the next real-stack round remain the final gate).

中文说明

Autofix 本轮报告 — PR #13179 @ f82bab116e

本轮只有一个提交:f82bab116e fix(web-shell): expire stream verdicts on heartbeat-answered idle reconnects (#13179)。

反馈处置

[ic:6018332618] §1 — 合并冲突:main 的 #13166 取代了第 1 项

结论:已解决;本轮已验证,无需代码改动。 合并提交 ae0f9f863f(本轮之前已落地)完全按反馈验证过的形态解决了冲突:

  • packages/cli/src/serve/managed-runtime-tool-executor.ts 与 main 逐字节一致(该路径上 git diff origin/main HEAD 为空)——采用了 main 的 realpath/归属权约束。
  • PR 的两条约束回归测试(packages/cli/src/serve/managed-runtime-file-history.test.ts:503,544)现在断言 main 的 'is not within the Session working directory' 文案——正是反馈开出的两行改动。
  • 该套件 33/33 通过(见"验证"一节),且 PR 相对 merge-base 的剩余 diff 已不再触及执行器及其 worker 测试。

留给维护者的一条说明(本 bot 无法代劳): 反馈还要求把 PR 描述/标题中的第 1 项改述为"已被 #13166 取代"。PR 元数据编辑属于工作流掌管的网络写操作,本 bot 没有 GitHub 凭据,因此在此标记而不是直接修改。

[ic:6018332618] §2 — 存活过期:R11-1 的门槛在该服务端上永远不会触发

结论:实现了反馈给出的 keep-alive 候选方案。 动手前先在代码中确认了机制:空闲会话续传时服务端只发送 :keepalive 注释帧,而 decodeFrame 把它们丢弃(return undefined; // heartbeat or comment),因此 delivered 永远不会为真,proof-of-life 定时器里的 expireAnswered('stream') 永远不会执行。

改动(源码 +28/−3,测试 +180,共 7 个文件):

  • java-managed-agent-client.ts —— streamEvents 新增可选参数 onAlive,每收到一个完整心跳/注释帧就调用一次;流末尾的残帧(半帧断连)不上报。
  • managed-agent-provider.ts —— subscribeEvents 选项新增 onAlive?(): void(可选字段,其他实现不受影响)。
  • java-managed-agent-provider.ts —— 把 request.onAlive 透传给 streamEvents。
  • use-managed-session.ts —— hook 传入 onAlive:把这次连接标记为已送达;若 proof-of-life 检查点已过,就补做定时器一直在等的过期。catch 一侧的裁决恢复逻辑不变——被 keep-alive 记功之后仍然失败的连接,照旧恢复其终局裁决。

为什么 R11-1 的黑洞保证仍然成立: 被黑洞的连接永远不会发心跳,onAlive 就不会触发——既有的"keeps a … verdict standing through an unanswered reconnect"两条用例(未改动)钉住了这一点。被删除会话的红条也依然会保持:已删除会话的每次重连都在毫秒级返回 404,根本活不到收到心跳,相关用例原样通过。stall 警告同样不受影响——expireAnswered 从不过期 stall 记录,"keeps the stall warning through an idle connection that never advances" 原样通过。

新增见证用例(在改动前的 head 上全部失败,现在全部通过):

  • 客户端:每个完整 keepalive 都上报给 onAlive;残帧不上报。
  • provider:keepalive 透传给 subscribe 调用方。
  • hook ×2:一次 404(终局)和一次 502(瞬时)之后,重连空闲且只发 keep-alive,keep-alive 在 3 秒检查点之后到达——站立中的记录在 keep-alive 到达时(t=7000)过期,而不是在 proof-of-life 检查点(t=6000)。

已执行的变异探针(拆掉守卫 → 聚焦套件变红 → 恢复 → 变绿):

  1. hook:删掉 if (proofOfLifePassed) expireAnswered('stream') → 两条新 hook 用例失败;恢复后转绿。
  2. 客户端:删掉 if (!trailing) onAlive?.() → 新客户端用例失败;恢复后转绿。
  3. provider:删掉 request.onAlive 透传 → 新 provider 用例失败;恢复后转绿。

验证

  • npm run build(使用 COREPACK_HOME=/tmp/corepack-cache;沙箱对默认 corepack 缓存目录报 EACCES)—— 通过
  • npm run typecheck —— 通过
  • npm run lint —— 通过(首轮发现新 502 用例违反 require-yield;按同类用例的 yield* []; 写法修复后复检干净)
  • 对全部 7 个改动文件执行 npx prettier --check —— 通过(provider 用例先经一次 --write)
  • 在 packages/web-shell 执行 npx vitest run client/components/managed/ —— 15 个文件,393/393 通过
  • 在 packages/cli 执行 npx vitest run src/serve/managed-runtime-file-history.test.ts(第 1 项的约束套件)—— 33/33 通过
  • 上述变异探针 1–3 —— 拆掉守卫时均变红,恢复后转绿

未运行:真实栈 E2E(需要 Spring/Broker/打包 worker 栈;反馈中该候选的真实栈数据显示红条在恢复后 20–28 秒清除——CI 与下一轮真实栈验证仍是最终关口)。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 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/模型 kimi-k3 · CLI 0.25.0

@qwen-code-review-bot

qwen-code-review-bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 79d77b2. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 1 render-shaping file:

  • packages/web-shell/client/components/managed/ManagedSessionsPage.tsx

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 packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

— Qwen Code · web-shell visuals

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 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 将重新运行。

wenshao added a commit to wenshao/qwen-code that referenced this pull request Oct 6, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code did not run conflict resolution for this request.

PR #13179 does not currently have merge conflicts with main.

@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Real-stack verification, round 6 — #13179 @ 2061be72 (macOS)

Follow-up to rounds 1, 2–3, 4 and 5. This round covers autofix rounds 3–4 (ae0f9f863f conflict merge, f82bab116e keep-alive) and the current main ac497aee.

Verdict: both round-5 items are resolved and hold on the real stack, and the PR now merges cleanly into main. One small new issue comes from how the keep-alive interacts with R11-1's restore; it has a one-line candidate, and I'd take it before merging. Nothing else blocks.

  • Conflict. Resolved exactly as suggested: the executor is byte-identical to main (feat(managed-agent): admit glob in new hosted-workspace /2 profiles #13166), and the two containment tests assert main's message. Item 1 is now tests-only.
  • Keep-alive. It works against the real server. After one 404 / one 502 on a stream reconnect, the red line clears at 21.1 / 20.8 s; after a real outage, 20.1 s after recovery. A 30 s black-holed reconnect still holds the line for the whole hole (R11-1 kept).
  • New (g): suppose one reconnect gets a 404, and a keep-alive on the next, healthy connection then clears it. If that long-lived connection later drops with a network error, the old 404 comes back as a terminal stop. On the real server that means "The Session was not found." over a live Session, for about 18 s, until the next reconnect's keep-alive (2/2 runs).
  • CI. The red Test (ubuntu-latest, Node 22.x) (2 tests in acp-integration/session/Session.test.ts) is inherited from main, not caused by this PR (see notes).

What ran

  • Trees. A local trial merge of 2061be72 into main ac497aee merged cleanly. Its diff against main is the PR's 13 files; the executor diff against main is empty.
  • Rebuilt from that merge: the Spring jar and the packaged CLI, Harness and worker.
  • Arms, all watching the same Session at the same time:
    • main;
    • the previous head (same tree with use-managed-session.ts from 1780afcd, i.e. without the keep-alive commit);
    • the head;
    • the head plus the candidate.
  • Static checks on the trial merge. cli plan suites 935/935, web-shell managed 402/402; tsc for cli and web-shell, eslint and prettier all clean.

1. Real stack

round 6

The keep-alive commit, before vs after. Same tree, only the hook differs:

  • Before (1780afcd's hook): (c), (c′) and (e) all wait for the next event, at 40–70 s.
  • After (the head): (c) clears at 21.1 s, (c′) at 20.8 s, and (e) at 50.7 s, with no gap during the hole.

Against main. Main now clears a stream failure on (re-)establishment (#13342's onEstablished). This server establishes the response on its first keep-alive, so main and the PR now clear at the same moments: (c), (c′), (e), the outage and the 401 case. (a), (b) and (d) are equivalent too.

What the PR still adds over main:

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

resurrect

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, stoppedReason comes back as 'session gone'; with the candidate it stays undefined and error is 'network error'.
  • Tests. The managed suite passes 403/403 (402 + 1), including the R11-1 restore specs.

3. Notes

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 本地试合并到 main ac497aee,无冲突。相对 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. 说明

证据 —— 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 与粗暴断开)。

@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI attribution for the Test (ubuntu-latest, Node 22.x) failure on this PR (run 37495525515), verified byte-by-byte against the checkout the lane compiled.

Root cause: the lane compiled a stale merge ref whose upstream-main half was internally inconsistent on packages/cli/src/acp-integration/session/; the PR side touches none of the involved files.

  • The failing pair — Session > accounts for delegated work when a tool_call tool result returns (validation_error) and routes tool_call through a hidden deferred tool in ACP: build error — compares a refusal string produced by Session.ts with literals in Session.test.ts. The merge ref's parent main was 07e905aa4b, whose Session.ts was already wired to describeBridgedArgumentError (the fix(core): keep nested tool_call arguments open on Responses; name the target in bridged validation errors #12901 "rejected the arguments …" wrapped form) while the same revision's test file still expected the pre-fix(core): keep nested tool_call arguments open on Responses; name the target in bridged validation errors #12901 shapes at exactly two spots (${PREFIX}invalid delegation arguments, and the un-prefixed Deferred tool … rejected the arguments …). Producer and expectation from the same upstream revision did not match, so both assertions red on any tree containing that pair — independent of this branch.
  • Every involved file is untouched by this branch (git diff <merge-base>...HEAD --name-only over packages/cli/src/acp-integration/** and packages/core/src/tools/tool-call.ts is empty; #12901's producer change arrived entirely from upstream after this branch's base).
  • Current origin/main (ac497aeed9) is consistent at both spots (producer wired at 2 call sites, both expectations in the wrapped form), so the failure shape is not constructible on a merge ref computed against it.

Action taken: the branch now merges current upstream (264823fcbc), which is exactly the exception case for updating a running PR's base — main moved the precise failing path. On the thrice-merged tree, both previously failing specs were run explicitly and are green (validation_error 1 passed; build error 1 passed), the web-shell managed suite is 402/402 across 15 files, the cli managed suites are 227/227, and the full build + repo typecheck are clean. This comment carries the evidence; the lane should go green on the rerun the push triggers.

@qwen-code-review-bot qwen-code-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • onEstablished left with no production caller after the hook switched to onAlive — 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)

Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
Comment thread packages/web-shell/client/components/managed/ManagedSessionsPage.tsx Outdated
Comment thread packages/web-shell/client/components/managed/ManagedSessionsPage.tsx Outdated
// 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');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 语义遗留项一起处理。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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++);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 簇足够小而未纳入;下一轮处理。

Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
Comment on lines +559 to +561
const rest = { ...current.signals };
delete rest.transcript;
return rest;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 记录仍然存活),但本轮已经重写了一条较长的判定测试并新增四条;这条需要仔细编排的分页/流交互用例归入下一轮的测试加固批次。

Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
…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.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下:

Autofix round — PR #13179

Commit d8cf0a0aca — fix(web-shell): never resurrect a heartbeat-expired stream verdict (#13179).
8 of 21 posted findings resolved (the Critical, 3 more behavior fixes, 4 coverage pins); the remaining 13 Suggestions are deferred to the next round per the ~8-findings batch cap, each with a recorded reason as a thread reply.

Resolved in code

  • [Critical] R1-1 (use-managed-session.ts, rc:4202164933) — a keep-alive-expired stream verdict was resurrected when the certified connection later dropped. onAlive now disarms the attempt's verdict capture (expiredVerdictMessage = undefined) before the proof-of-life check, and the proof-of-life timer no longer arms the capture for a heartbeat-certified attempt (if (!alive)), which also closes the pre-point heartbeat variant. The restore itself stays: a replay-only attempt (old data, not proof of life) still restores the verdict when it fails past the proof-of-life point, and that branch is now pinned. This is the shape the maintainer's round-6 real-stack verification endorsed (candidate-no-resurrect.patch), extended to the pre-point shape. Witness: new spec does not restore a keep-alive-expired verdict when the certified connection later drops (404 → keep-alive at t=7s → TypeError('network error') at t=68s; asserts stoppedReason undefined, error = 'network error') — red on the pre-round head, green now.
  • R1-18 (rc:4202164960) — a heartbeat no longer resets the stream reconnect ladder. onAlive now marks a separate alive flag (drives the proof-of-life expiry) instead of delivered (drives failures = 0), so a stream that heartbeats and then drops inside 3 s climbs the ladder (0, 4000, 10999, …) instead of retrying at the 3 s floor forever. Rung zero stays exactly BASE_RETRY_DELAY_MS. Witness: new spec keeps climbing the reconnect ladder when heartbeat-only attempts keep failing fast — red when delivered = true is restored to onAlive.
  • R1-6 (ManagedSessionsPage.tsx, rc:4202164948) — the page no longer collapses the hook's stoppedReason/error channels with ||; they render as sibling alerts, so a failed 'Older history' click is shown beside a standing terminal verdict instead of being swallowed. Witness: new page spec shows a failed older-page fetch alongside a sticky terminal stop — red when the channels are collapsed.
  • R1-17 (ManagedSessionsPage.tsx, rc:4202164952) — exact-repeat suppression across the three alert slots (error !== detail.stoppedReason && error !== detail.error), so one server condition is announced once while genuinely different messages keep their own slots (['Not found', 'Turn already active'] still passes). Witness: new page spec renders one alert when the action error repeats the standing verdict — red when the comparison is removed.

Resolved as coverage pins (behavior confirmed correct, now witnessed)

  • R1-12 (rc:4202165052) — the catch-side verdict restore is pinned via the replay door: new spec restores a terminal stream verdict when a replay-only reconnect fails past the proof-of-life point goes red if the restore branch is disabled.
  • R1-28 (rc:4202164971) — retires a definite stream verdict once the stream delivers again was retimed so the new frame lands after the attempt's proof-of-life point, then the attempt throws; goes red if the new-frame disarm (expiredVerdictMessage = undefined on a genuinely new frame) is removed.
  • R1-21 (rc:4202164966) — the same retimed test now continues with a later attempt that parks past its own proof-of-life point and fails; goes red when the three streamVerdictMessageRef mirror resets are deleted together.
  • R1-20 (rc:4202164975) — new spec expires a terminal stream verdict at the proof-of-life point when a keep-alive lands before it: a pre-point heartbeat does not expire early (pins the proofOfLifePassed gate), the timer lands the expiry (pins the heartbeat arm), and a later drop of the certified connection does not resurrect the verdict.

Deferred to the next round (batch cap ~8) — reason recorded on each thread

R1-4 (dead SnapshotLegError.code — should land with the R1-30 403-scope decision), R1-7 (poll failure clears bootstrap loading), R1-8 (paging failure masks a hung stream — needs the displayedLeg tie-break), R1-9 (transient failure evicts an armed stall), R1-10 (re-arm cooldown / no-wipe), R1-11 (inline rule → failed() — lands with R1-13), R1-13 (isAuthFailure pins — behind the R1-30 question), R1-14 (loadOlder clear-scope pin), R1-24 (both-reject drops the transcript answer), R1-25 (fulfilled session read never retires its leg — interacts with R1-10), R1-26 (signals type doc contradicts the new-frame path — a doc-vs-code decision tied to R1-24/25), R1-27 (snapshotFailures never decays), R1-31 (expireFinal → expireAnswered on a non-advancing resync).

Issue-level items

  • The round-6 real-stack verification's item (g) is R1-1 — fixed here in the endorsed shape, with the pre-point variant closed as well.
  • The noted onEstablished plumbing with no production caller (left over from the fix(web-shell): managed session UI correctness from the #12692 R2 review #13342 merge) is deferred to the next round: it spans the provider interface, the pass-through, and the client, and removing it should not ride along with the verdict fix.
  • The stale PR body/title is the workflow's to refresh; not something this round can edit.
  • The Test (ubuntu-latest) failure attribution comment is informational — already resolved by the main merge at 264823fcbc; the serve A/B activeWorkStaleMs 5→4 delta and the web-shell visuals note (no scenario reaches this UI) need no code change this round.

Mutation probes (guard deleted → focused test fails → restored → green)

  1. expiredVerdictMessage = undefined removed from onAlive → keep-alive-drop spec fails. Restored, green.
  2. if (!alive) guard removed from the proof-of-life timer → pre-point keep-alive spec fails. Restored, green.
  3. Timer expiry reverted to if (delivered) → pre-point keep-alive spec fails at the expiry beat. Restored, green.
  4. delivered = true restored to onAlive → heartbeat-only ladder spec fails (opens stay at the 3 s floor). Restored, green.
  5. New-frame disarm removed → retimed retire spec fails. Restored, green.
  6. All three streamVerdictMessageRef resets deleted → retimed retire spec fails at the later-attempt beat. Restored, green.
  7. Catch-side restore disabled (if (false)) → replay-door restore spec fails. Restored, green.
  8. if (proofOfLifePassed) gate removed from onAlive → pre-point keep-alive spec fails at the early-expiry beat. Restored, green.
  9. Page alert channels collapsed back to || → failed-older-page-fetch spec fails. Restored, green.
  10. Page duplicate suppression removed → single-alert spec fails. Restored, green.

Verification

  • vitest run client/components/managed (packages/web-shell) — 15 files, 408/408 passed (402 pre-existing + 6 new), including the two near-miss keep-alive specs and the R11-1 restore spec
  • npm run build — passed (with COREPACK_HOME=/tmp/corepack-cache; the runner's HOME is not writable for corepack's cache)
  • npm run typecheck — passed
  • npm run lint — passed (one require-yield error in a new test found and fixed with the file's yield* [] pattern, then re-run green)
  • npx prettier --check on the four touched files — passed
  • Focused vitest re-runs for each mutation probe above — failed as expected under the mutation, green after restore
  • Not run: integration tests / npm run bundle — the touched behavior (hook + page rendering) is fully exercised by the vitest suites; no bundled-CLI-only path is involved
中文说明

Autofix 本轮总结 — PR #13179

提交 d8cf0a0aca — fix(web-shell): never resurrect a heartbeat-expired stream verdict (#13179)。
本轮解决了 21 条已发布发现中的 8 条(1 条 Critical、3 条行为修复、4 条覆盖钉测试);其余 13 条 Suggestion 按约 8 条的批次上限推迟到下一轮,每条都以线程回复记录了原因。

已在代码中解决

  • [Critical] R1-1(use-managed-session.ts,rc:4202164933)——被 keep-alive 过期的流判定,会在该已认证连接随后断开时被复活。现在 onAlive 在 proof-of-life 判断之前解除本次尝试的判定捕获(expiredVerdictMessage = undefined),且 proof-of-life 计时器不再为已被心跳认证的尝试武装捕获(if (!alive)),同时堵上了“心跳早于存活点”的变体。恢复逻辑本身保留:只重放旧帧的尝试(旧数据,并非存活证据)在越过存活点后失败时仍会恢复判定,且该分支现在有了钉测试。这正是维护者第 6 轮真实环境验证认可的形态(candidate-no-resurrect.patch),并扩展到了心跳早于存活点的情形。见证:新用例 does not restore a keep-alive-expired verdict when the certified connection later drops(404 → t=7 秒 keep-alive → t=68 秒 TypeError('network error');断言 stoppedReason 为 undefined、error 为 'network error')——在本轮之前的 head 上为红,现在为绿。
  • R1-18(rc:4202164960)——心跳不再重置流重连阶梯。onAlive 现在标记独立的 alive 标志(驱动 proof-of-life 过期),而不是 delivered(驱动 failures = 0),因此“发了一个心跳然后在 3 秒内断开”的流会沿阶梯爬升(0, 4000, 10999, …),而不是永远卡在 3 秒地板上。第 0 级仍精确等于 BASE_RETRY_DELAY_MS。见证:新用例 keeps climbing the reconnect ladder when heartbeat-only attempts keep failing fast——把 delivered = true 加回 onAlive 即变红。
  • R1-6(ManagedSessionsPage.tsx,rc:4202164948)——页面不再用 || 合并 hook 的 stoppedReason/error 两条通道;二者作为并列提示渲染,于是一次失败的“加载更早历史”点击会显示在仍立着的终止判定旁边,而不是被吞掉。见证:新页面用例 shows a failed older-page fetch alongside a sticky terminal stop——把两条通道重新合并即变红。
  • R1-17(ManagedSessionsPage.tsx,rc:4202164952)——三个提示槽位之间的完全重复抑制(error !== detail.stoppedReason && error !== detail.error),同一个服务端状况只播报一次,而真正不同的消息仍各占一个槽位(['Not found', 'Turn already active'] 依然通过)。见证:新页面用例 renders one alert when the action error repeats the standing verdict——去掉比较即变红。

作为覆盖钉测试解决(行为确认正确,现在有了见证)

  • R1-12(rc:4202165052)——通过重放路径钉住 catch 侧的判定恢复:新用例 restores a terminal stream verdict when a replay-only reconnect fails past the proof-of-life point 在恢复分支被禁用时变红。
  • R1-28(rc:4202164971)——retires a definite stream verdict once the stream delivers again 重新定时,使新帧落在该次尝试的存活点之后,随后该尝试抛出;删掉新帧解除(真正的新帧上的 expiredVerdictMessage = undefined)即变红。
  • R1-21(rc:4202164966)——同一条重定时用例继续推进:后续某次尝试越过自己的存活点挂住再失败;把三处 streamVerdictMessageRef 镜像清除一起删掉即变红。
  • R1-20(rc:4202164975)——新用例 expires a terminal stream verdict at the proof-of-life point when a keep-alive lands before it:早于存活点的心跳不会提前过期(钉住 proofOfLifePassed 门),计时器落地过期(钉住心跳武装),而这条已认证连接之后的断开不会复活判定。

推迟到下一轮(批次上限约 8 条)——每条线程上已记录原因

R1-4(死字段 SnapshotLegError.code——应与 R1-30 的 403 范围决定同批落地)、R1-7(轮询失败清除启动的 loading)、R1-8(分页失败遮蔽挂住的流——需要 displayedLeg 平局裁决机制)、R1-9(临时失败驱逐已武装的 stall)、R1-10(re-arm 冷却 / 不清状态)、R1-11(内联规则改为 failed()——与 R1-13 同批)、R1-13(isAuthFailure 钉测试——受制于 R1-30 问题)、R1-14(loadOlder 清除范围钉测试)、R1-24(双腿皆拒时 transcript 应答被丢弃)、R1-25(已 fulfil 的 session 读取从不撤销其 leg——与 R1-10 相互影响)、R1-26(signals 类型文档与新帧路径相矛盾——是与 R1-24/25 绑定的文档-vs-代码取舍)、R1-27(snapshotFailures 从不衰减)、R1-31(未推进 resync 上 expireFinal 改为 expireAnswered)。

议题级评论

  • 第 6 轮真实环境验证的 (g) 项即 R1-1——本轮按认可的形态修复,并同时堵上了心跳早于存活点的变体。
  • 提到的 onEstablished 链路没有生产调用方(fix(web-shell): managed session UI correctness from the #12692 R2 review #13342 合并的遗留)推迟到下一轮:它横跨 provider 接口、透传与 client,不应搭判定修复的便车。
  • PR 标题/正文过期由工作流负责更新;本轮无法编辑。
  • Test (ubuntu-latest) 失败归因评论是信息性的——已被 264823fcbc 的 main 合并解决;serve A/B 的 activeWorkStaleMs 5→4 差异与 web-shell 视觉预览说明(没有场景覆盖到该 UI)本轮无需代码改动。

变异探针(删除保护 → 聚焦测试失败 → 恢复 → 转绿)

  1. 从 onAlive 删除 expiredVerdictMessage = undefined → keep-alive 断连用例失败。恢复后转绿。
  2. 从 proof-of-life 计时器删除 if (!alive) 门 → 早于存活点的 keep-alive 用例失败。恢复后转绿。
  3. 计时器过期回退为 if (delivered) → 早于存活点的 keep-alive 用例在过期节拍失败。恢复后转绿。
  4. 把 delivered = true 加回 onAlive → 纯心跳阶梯用例失败(打开时刻停留在 3 秒地板)。恢复后转绿。
  5. 删除新帧解除 → 重定时的撤销用例失败。恢复后转绿。
  6. 三处 streamVerdictMessageRef 重置一起删除 → 重定时的撤销用例在后续尝试节拍失败。恢复后转绿。
  7. 禁用 catch 侧恢复(if (false))→ 重放路径恢复用例失败。恢复后转绿。
  8. 从 onAlive 删除 if (proofOfLifePassed) 门 → 早于存活点的 keep-alive 用例在提前过期节拍失败。恢复后转绿。
  9. 页面提示通道退回 || 合并 → 失败的分页加载用例失败。恢复后转绿。
  10. 删除页面重复抑制 → 单提示用例失败。恢复后转绿。

验证

  • vitest run client/components/managed(packages/web-shell)——15 个文件,408/408 通过(402 既有 + 6 新增),包括两个相邻的 keep-alive 用例与 R11-1 恢复用例
  • npm run build——通过(使用 COREPACK_HOME=/tmp/corepack-cache;runner 的 HOME 不可写,corepack 无法建缓存)
  • npm run typecheck——通过
  • npm run lint——通过(先在一个新测试上发现一处 require-yield 报错,按文件既有的 yield* [] 写法修复后复跑转绿)
  • 对四个改动文件执行 npx prettier --check——通过
  • 上述每个变异探针的聚焦 vitest 复跑——变异下如期失败,恢复后转绿
  • 未运行:集成测试 / npm run bundle——改动行为(hook 与页面渲染)已被 vitest 套件完整覆盖,不涉及只有打包 CLI 才能触达的路径

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/模型 kimi-k3 · CLI 0.25.0

@qwen-code-review-bot qwen-code-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
Comment thread packages/web-shell/client/components/managed/ManagedSessionsPage.tsx Outdated
Comment thread packages/web-shell/client/components/managed/ManagedSessionsPage.tsx Outdated
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下:

Address-review round — PR #13179

Committed as 99dea2d678 fix(web-shell): tighten the signal-leg ledger and alert de-duplication (#13179) — 4 files, +468/−24. Nine of the eleven actionable inline findings are resolved in code; two are deferred to the next round under the per-round batch bound (replies posted on their threads, threads left open).

Addressed

  1. R2-1 (rc:4204452346) — proof-of-life capture armed on the no-expiry branch. Fixed as suggested: the timer now returns early when the attempt answered nothing (if (!delivered && !alive) return;), so the catch can no longer re-stamp a verdict that was never removed with a fresh seq. The expiry gate itself is unchanged — the replay-armed path (delivered === true, alive === false) still captures and expires, pinned by the pre-existing restores a terminal stream verdict when a replay-only reconnect fails past the proof-of-life point. New witness: does not re-stamp a standing verdict when a silent reconnect fails past the proof-of-life point (attempt 1 books a terminal 404, a loadOlder() click then fails, attempt 2 parks silent past +3 s and throws — the paging record keeps the field). Probe: negating the guard turns it red with the exact reported defect (expected undefined to be 'page failed').

  2. R2-2 (rc:4204452356) — no release for a transient stream record on an established-but-silent reconnect. Re-added an establishment signal that expires only transient stream records: onEstablished: () => expireTransient('stream'), where expireTransient deletes the leg's slot only when !entry.final && !entry.stall. No interface change — the plumbing was already declared, forwarded, and invoked. Three witnesses: expires a transient stream failure once the reconnect establishes and idles (cleared at establishment, still clear past the proof-of-life point), keeps a terminal stream verdict standing when the reconnect establishes and idles, and keeps the stall alert standing when a reconnect establishes and idles. Probes: removing the handler reds the first; widening the release to final reds the second; dropping the stall exemption reds the third.

  3. R2-3 (rc:4204452369) — two alert de-duplication guards untested. Added renders one alert when the failed older-page fetch repeats the standing verdict (poll 404 verdict + paged fetch rejected with the same 404 message) and renders one alert when the action error repeats the standing stream failure (stream leg 502 + Send rejected with the same message). Both went red under the exact mutations named in the finding (deleting each !== term from the positional form), and both red again if the Set de-dup is removed from the keyed list that R1-17 introduced.

  4. R2-4 (rc:4204452376) — per-attempt scope of alive unpinned. Added keeps a heartbeat-certified attempt's failure standing through a silent reconnect: attempt 2 heartbeats past the proof-of-life point and then drops (booking network error), attempt 3 stays silent — the record attempt 2 left still stands past attempt 3's +3 s point. Probe: hoisting let alive = false; above the while turns it red (expected undefined to be 'network error').

  5. R1-17 (rc:4204452388) — render-time de-dup re-mounts the suppressed alert. Collapsed the three positional siblings into one keyed list over the distinct messages (insertion order preserves stoppedReason → detail.error → error). Extended renders one alert when the action error repeats the standing verdict with the recovery beat: after the duplicate Send, advancing past the poller's rung-1 delay retires the session leg, and the standing alert keeps its DOM node (toBe(alertBefore)) with texts still ['Not found']. Probe: reverting to positional siblings turns the identity assertion red; the ordered ['Not found', 'Turn already active'] assertion and the :1852 single-alert assertion stay green.

  6. R1-9 (rc:4204452765) — a transient stream failure evicts a standing stall. Extended record()'s guard: if (previous?.stall && !final && !(error instanceof StreamStallError)) return current; — a stall now leaves only through a terminal verdict, its own re-assertion, or an advancing stream, matching the class doc. The two beats in disarms the stall alert once the stream advances again that pinned the old displacement (toBe('Failed to fetch') / toBeUndefined()) now assert /not advancing/ — same assertion count, no test deleted or disabled; the change is the finding's acceptance criterion (the old values pinned the defect). Probe: removing the guard reds that test with exactly the reported blast radius (expected 'Failed to fetch' to match /not advancing/, 1 test).

  7. R1-25 + R1-24 (rc:4204453187, rc:4204453423) — fulfilled session read discarded by the resync's transcript-leg throw. Fixed in one pass as suggested: retire('session') immediately before throw new SnapshotLegError('transcript', …), symmetric with the mirror branch. The endedRef re-arm constraint is satisfied: snapshot() runs only from the bootstrap loop and the gap branch, and endedRef is set solely on the bootstrap's terminal path, which returns immediately — so the added retire is unreachable with the flag set. New witness: retires a standing session-leg verdict when a gap resync reads the session successfully (poll books a terminal 404, gap resync fulfils getSession and rejects getTranscript; the verdict leaves with the resync, a full poll rung before the poller could retire it). Red without the retire (expected 'session gone' to be undefined). Note on R1-24's stated mechanism: the session leg is not booked as failed today — both callers already classify by SnapshotLegError.leg (traced at head; the witness's assertions confirm no session-leg failure is booked). What was missing is the fulfilled read's retirement, which this fix adds — the exact change R1-24 asked for ("retire or record the session leg from sessionRead before throwing").

  8. R1-8 (rc:4204452977) — reveal rule between two concurrent live conditions unpinned. Pinned the rule the PR's ledger model implements: between live records the newest booking wins (documented at the pick). The witness is reinstated in the deleted test's shape by extending reveals a still-live paging failure again once a poll blip clears with a dropped-then-silent stream leg: the stream record never outranks the newer paging record, the poll blip displaces and then re-reveals it. Probe: a stream-over-transcript reordering of the pick — the exact mutant the finding showed was uncaught — now goes red (expected 'Failed to fetch' to be 'Managed Agent request failed (410)'); the pre-existing :4869 blip assertion stays green.

Deferred to the next round (batch bound — 9 findings landed)

  • R1-10 (rc:4204453702) — re-arm wipes paged history. The restart is intended and pinned; the fix is about what the revision bump preserves, and the finding itself notes the interplay with the still-open low-confidence cross-generation endedRef observation — the two should be resolved together.
  • R1-31 (rc:4204453963) — replay guard conflates exact duplicate with strictly older frame. The repo's own contract tests model an overlapping/disordered tail as an identical replay, so distinguishing needs a decided handling for the out-of-order case (or a deliberate documentation-only resolution) — not a tail-of-batch call.

Notes

  • Test-surface change disclosure: two assertion values changed in disarms the stall alert once the stream advances again (see item 6) — no test file deleted, no assertion statement removed, nothing skipped or disabled; the old values pinned the reported defect, and the finding names these beats as the ones that must change.
  • Environment: the runner checkout had no built workspace dists (@qwen-code/sdk, @qwen-code/acp-bridge, @qwen-code/channel-*, @qwen-code/web-templates, @qwen-code/audio-capture), so root npm run typecheck failed identically on pristine HEAD (2324 error lines, verified via stash) — pre-existing, unrelated to this diff. Building those workspaces (all exit 0) made the full root typecheck clean.
  • Diff growth: this round adds ~39 source and ~430 test lines net; every added test is a finding-demanded witness with a recorded mutation probe. The window's test budget will read over budget after this round — flagged here so the next round's audit has the reason up front.

Verification

  • npm run build — passed (exit 0); plus per-workspace builds @qwen-code/sdk, @qwen-code/acp-bridge, @qwen-code/channel-* (12), @qwen-code/web-shell, @qwen-code/web-templates, @qwen-code/audio-capture — all exit 0.
  • npm run typecheck (root, all workspaces + integration) — passed (exit 0, 0 errors).
  • npm run lint (root ESLint, both passes) — passed (exit 0).
  • npx vitest run client/components/managed/ (packages/web-shell) — 15 files, 416 passed (includes 106 hook + 59 page tests).
  • npx vitest run (full packages/web-shell suite) — exit 0.
  • Mutation probes (mutant → focused test result → restored): R2-1 guard negated → red (expected undefined to be 'page failed'); onEstablished removed → red; expireTransient widened to final → red; stall exemption dropped → red; alive hoisted → red (expected undefined to be 'network error'); stall guard removed → red (expected 'Failed to fetch' to match /not advancing/); resync retire('session') removed → red (expected 'session gone' to be undefined); positional alert siblings restored → node-identity assertion red; each positional !== term deleted → respective new test red; keyed-list Set de-dup removed → all three de-dup tests red; stream-over-transcript pick → red (expected 'Failed to fetch' to be 'Managed Agent request failed (410)'). All restored and re-verified green.
  • Integration tests (npm run bundle + integration harness) — not applicable: the changed behavior is exercised through the web-shell jsdom suites, not the bundled CLI.
中文说明

审查处理轮次总结 — PR #13179

已提交为 99dea2d678 fix(web-shell): tighten the signal-leg ledger and alert de-duplication (#13179)——4 个文件,+468/−24。11 条可执行行内发现中的 9 条已在代码中解决;其余 2 条按每轮处理批次上限延后到下一轮(已在各自线程回复说明,线程保持开启)。

已处理

  1. R2-1(rc:4204452346)——proof-of-life 捕获在不过期任何记录的分支上被装填。 按建议修复:当尝试没有任何应答时定时器提前返回(if (!delivered && !alive) return;),catch 因此无法再用新的 seq 重新盖章一个从未被移除的判定。过期门控本身未变——重放装填路径(delivered === true、alive === false)仍会捕获并过期,由既有测试 restores a terminal stream verdict when a replay-only reconnect fails past the proof-of-life point 固定。新增见证:does not re-stamp a standing verdict when a silent reconnect fails past the proof-of-life point(attempt 1 记下终局 404,随后一次 loadOlder() 点击失败,attempt 2 静默越过 +3 秒点并抛错——分页记录保持显示)。探针:否定该守卫会让它变红,且报错与发现所述完全一致(expected undefined to be 'page failed')。

  2. R2-2(rc:4204452356)——重新建立但保持静默的连接没有释放瞬态 stream 记录的路径。 补回了只过期瞬态 stream 记录的建立信号:onEstablished: () => expireTransient('stream'),其中 expireTransient 仅在 !entry.final && !entry.stall 时删除该 leg 的槽位。无接口改动——管道本就已声明、透传并调用。三条见证:expires a transient stream failure once the reconnect establishes and idles(建立时即清除,且越过 proof-of-life 点后仍保持清除)、keeps a terminal stream verdict standing when the reconnect establishes and idles、keeps the stall alert standing when a reconnect establishes and idles。探针:移除该处理函数使第一条变红;把释放放宽到 final 使第二条变红;去掉 stall 豁免使第三条变红。

  3. R2-3(rc:4204452369)——三条告警去重判断中的两条没有测试。 新增 renders one alert when the failed older-page fetch repeats the standing verdict(轮询 404 判定 + 分页读取以相同的 404 消息被拒)与 renders one alert when the action error repeats the standing stream failure(stream leg 502 + Send 以相同消息被拒)。两者在发现所指的确切变异下均变红(在按位置排列的写法中删除对应的 !== 项),且在 R1-17 引入的带 key 列表中移除 Set 去重后也均变红。

  4. R2-4(rc:4204452376)——alive 的逐次尝试作用域未被固定。 新增 keeps a heartbeat-certified attempt's failure standing through a silent reconnect:attempt 2 在 proof-of-life 点之后发出心跳随后断开(记下 network error),attempt 3 保持静默——attempt 2 留下的记录在 attempt 3 的 +3 秒点之后仍然存在。探针:把 let alive = false; 上提到 while 之外会让它变红(expected undefined to be 'network error')。

  5. R1-17(rc:4204452388)——渲染期去重会重新挂载被抑制的告警。 把三个按位置排列的兄弟节点合并为一个按消息去重、带 key 的列表(插入顺序保持 stoppedReason → detail.error → error)。为 renders one alert when the action error repeats the standing verdict 增加了恢复节拍:在重复 Send 之后,推进超过轮询第 1 级延迟使 session leg 退休,并断言已显示的告警保持其 DOM 节点(toBe(alertBefore))且文本仍为 ['Not found']。探针:恢复为按位置排列的兄弟节点会使同一性断言变红;带顺序的 ['Not found', 'Turn already active'] 断言与 :1852 的单告警断言保持绿色。

  6. R1-9(rc:4204452765)——瞬态 stream 失败驱逐已挂起的 stall。 扩展了 record() 的保护:if (previous?.stall && !final && !(error instanceof StreamStallError)) return current;——stall 现在只能通过终局判定、自身重新断言或推进中的 stream 离开,与类文档一致。disarms the stall alert once the stream advances again 中固定旧驱逐行为的两个节拍(toBe('Failed to fetch') / toBeUndefined())现在断言 /not advancing/——断言数量不变,没有删除或禁用任何测试;该改动正是发现的验收标准(旧断言值固定的是缺陷本身)。探针:移除该守卫会使该测试变红,且爆炸半径与发现所述完全一致(expected 'Failed to fetch' to match /not advancing/,1 个测试)。

  7. R1-25 + R1-24(rc:4204453187、rc:4204453423)——resync 的 transcript leg 抛错丢弃了已成功的 session 读取。 按建议一次性修复:在 throw new SnapshotLegError('transcript', …) 之前立即 retire('session'),与镜像分支对称。endedRef 重新拉起的约束已满足:snapshot() 只从 bootstrap 循环与 gap 分支调用,而 endedRef 只在 bootstrap 的终局路径置位且紧随其后就是 return——因此新增的 retire 在该标志置位时不可达。新增见证:retires a standing session-leg verdict when a gap resync reads the session successfully(轮询记下终局 404,gap resync 的 getSession 成功而 getTranscript 被拒;判定随 resync 离开,比轮询能退休它整整早一个轮询级别)。移除该 retire 会变红(expected 'session gone' to be undefined)。关于 R1-24 所述机制的说明:今天 session leg 并不会被记为失败——两个调用方都已按 SnapshotLegError.leg 分类(在 head 上追踪确认;见证的断言也确认没有 session leg 失败被记下)。缺失的是对已成功读取的退休处理,本次修复补上了它——正是 R1-24 要求的改动(“在抛错之前依据 sessionRead 让 session leg 退休或记录”)。

  8. R1-8(rc:4204452977)——两个并存实时状况的展示规则未被固定。 固定了 PR 的账本模型所实现的规则:在实时记录之间由最新记入者赢得展示(已在选取处注释说明)。见证按被删测试的形态恢复——扩展 reveals a still-live paging failure again once a poll blip clears,加入一个断开后静默的 stream leg:stream 记录永远不会压倒更新的分页记录,轮询抖动会先取代再重新揭示它。探针:对选取做 stream 优先于 transcript 的重排——正是发现指出今天无法捕获的那个变异——现在会变红(expected 'Failed to fetch' to be 'Managed Agent request failed (410)');既有的 :4869 抖动断言保持绿色。

延后到下一轮(批次上限——本轮已落地 9 条)

  • R1-10(rc:4204453702)——重新拉起会清掉已分页历史。 重启本身是有意且有测试固定的;修复的关键在于 revision 递增时保留什么,且发现自己也指出它与仍未定论的低置信度跨代 endedRef 观察相互关联——两者应一并解决。
  • R1-31(rc:4204453963)——重放守卫把完全重复与严格更旧的帧混为一谈。 仓库自身的契约测试把重叠/乱序尾部建模为逐字节重放,因此要区分两者需要先确定乱序情形的处理方式(或明确选择仅文档化的解决)——不应在批次末尾仓促决定。

说明

  • 测试表面变化披露:disarms the stall alert once the stream advances again 中有两个断言值被修改(见第 6 条)——没有删除任何测试文件、没有删除断言语句、没有任何跳过或禁用;旧断言值固定的是被报告的缺陷,且发现本身点名这两个节拍必须改变。
  • 环境:本 runner 检出中没有构建好的 workspace dist(@qwen-code/sdk、@qwen-code/acp-bridge、@qwen-code/channel-*、@qwen-code/web-templates、@qwen-code/audio-capture),因此根目录 npm run typecheck 在原始 HEAD 上同样失败(2324 行错误,已通过 stash 验证)——属于预先存在的环境状态,与本 diff 无关。构建这些 workspace(全部 exit 0)后,完整根目录 typecheck 全部通过。
  • diff 增长:本轮净增约 39 行源码与约 430 行测试;每个新增测试都是发现所要求的见证,并配有已记录的变异探针。本轮之后窗口的测试预算将显示超支——在此先行说明,以便下一轮审计直接看到原因。

验证

  • npm run build——通过(exit 0);另有各 workspace 构建 @qwen-code/sdk、@qwen-code/acp-bridge、@qwen-code/channel-*(12 个)、@qwen-code/web-shell、@qwen-code/web-templates、@qwen-code/audio-capture——全部 exit 0。
  • npm run typecheck(根目录,全部 workspace + integration)——通过(exit 0,0 错误)。
  • npm run lint(根目录 ESLint,两个阶段)——通过(exit 0)。
  • npx vitest run client/components/managed/(packages/web-shell)——15 个文件,416 通过(含 106 个 hook + 59 个页面测试)。
  • npx vitest run(packages/web-shell 全量测试套件)——exit 0。
  • 变异探针(变异 → 聚焦测试结果 → 恢复):否定 R2-1 守卫 → 变红(expected undefined to be 'page failed');移除 onEstablished → 变红;expireTransient 放宽到 final → 变红;去掉 stall 豁免 → 变红;上提 alive → 变红(expected undefined to be 'network error');移除 stall 守卫 → 变红(expected 'Failed to fetch' to match /not advancing/);移除 resync 的 retire('session') → 变红(expected 'session gone' to be undefined);恢复按位置排列的告警兄弟节点 → 节点同一性断言变红;逐个删除按位置写法的 !== 项 → 对应新测试变红;移除带 key 列表的 Set 去重 → 三个去重测试全部变红;stream 优先于 transcript 的选取重排 → 变红(expected 'Failed to fetch' to be 'Managed Agent request failed (410)')。全部恢复并重新验证为绿。
  • 集成测试(npm run bundle + 集成测试设施)——不适用:变更行为由 web-shell 的 jsdom 测试套件覆盖,不经过打包后的 CLI。

Deferred non-Critical feedback

Critical-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. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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/模型 kimi-k3 · CLI 0.25.0

@qwen-code-review-bot qwen-code-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 B
  • packages/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)

Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
Comment thread packages/web-shell/client/components/managed/use-managed-session.ts Outdated
Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
…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.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/100 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #13179

Growth audit (required this window)

Verdict: sound (growth-audit.json in the workdir). KISS probe: I assumed the ledger is over-engineered and tried to prove it — the demonstrably simpler shape is the pre-PR single error field with a writer map, and that is exactly what produced the round-1/2 finding cluster this ledger resolves; every accumulated piece (per-leg slots, final monotonicity, stall flag, seq/newest, the four expiry strengths, the verdict mirror, the endedRef re-arm, SnapshotLegError, onAlive plumbing, isAuthFailure, the merge fast path) is load-bearing for a named finding or failure mode. Minimal-change probe: every changed file traces to the PR's original problem or an accepted finding (traced via git history), with one dead operand found and removed this round (SnapshotLegError.code). The window overage is test-side witness accretion the review itself demands; this round is net +222/−18 with the source side nearly flat after the subtractions.

Actionable feedback

  1. [Critical] R3-1 — a frame landing past the proof-of-life point never lands the stream-leg expiry ([rc:4208617735]): Fixed. The frame path now mirrors the heartbeat path: after delivered = true, when the point has passed, the attempt lands the held expiry (expireAnswered('stream')) and disarms the restore capture — a replay counts, and the replay guard no longer swallows it. A stall record is never cleared (expireAnswered refuses entry.stall), and an expiry landed here is never restored by that connection's later drop, matching the onAlive precedent. Witness: new spec expires a terminal stream verdict when a replayed frame lands past the proof-of-life point. Mutation probe: with the added guard removed, the spec fails (expected 'session gone' to be undefined); restored, green.
  2. [Suggestion] R3-2 — the stream-leg record in reveals a still-live paging failure again once a poll blip clears was never observed ([rc:4208617754]): Fixed. The spec gains a closing phase: the dead cursor recovers (pagingDown = false), a successful loadOlder() retires the transcript record, and the final assertion reads the still-booked stream record (error === 'Failed to fetch'). Existing assertions unchanged; the stub's reconnect still never calls onEstablished. Mutation probes: (a) adding request.onEstablished?.() to the stub's reconnect reddens the new assertion (expected undefined to be 'Failed to fetch'); (b) replacing newest with standing's specificity ordering reddens the spec. Both restore to green.
  3. [Suggestion] R3-3 — the stall re-assertion operand is unpinned ([rc:4208617768]): Declined as stated; the operand is removed instead. I wrote the suggested discriminating spec as a probe: it is green both with and without !(error instanceof StreamStallError). Trace: every stall re-assertion runs only after the same gap pass's snapshot(true) succeeded, and that success retires the session and transcript legs before the re-assertion executes — so no competing record can be live when the re-stamp would land, and while resyncs keep failing the re-assertion never runs at all. The operand defends a race that cannot happen; removing it leaves the managed suites at 419/419. The full evidence is posted on the thread, which stays open in case a reachable counterexample exists.
  4. [Suggestion] R1-17 — the node-identity assertion could not distinguish a message key from an index key ([rc:4208617776]): Fixed. shows a fresh action error alongside a sticky terminal stop now covers the 2→1 transition: after the alerts pair is captured, the second node is held, one poll rung retires the session leg, and the spec asserts the surviving alert is ['Turn already active'] and that its node is the captured one. Mutation probe: key={message} → key={index} reddens the identity assertion (node 0 is reused and its text rewritten, so the captured node is detached); restored, green.
  5. [Suggestion] R1-24 — when both snapshot reads reject, the transcript leg's definite answer was dropped ([rc:4208617787]): Fixed. The both-reject branch now records the transcript leg's own answer (failed(transcriptRead.reason, 'transcript')) before throwing the session error — but only when the session leg is transient and the bootstrap will retry; a terminal session answer keeps owning the display, so gives the session leg the bootstrap decision regardless of settle order holds unchanged, and only the session branch may end the bootstrap. Witness: new spec records a definite transcript answer when both bootstrap reads reject and the session blip is transient (standing history pruned/transcript beside the session transient, bootstrap keeps retrying). Mutation probe: with the booking removed the spec fails (expected undefined to be 'history pruned'); restored, green.
  6. [Suggestion] R1-10 — the endedRef re-arm had no cooldown or bound and reset both ladders ([rc:4208618393]): Fixed. A per-session budget (MAX_SESSION_REARMS = 3, rearmRef/rearmSessionRef) bounds the re-arm; the budget resets when a bootstrap actually completes and when the session changes, and past the bound the ended loops stay ended — the poll keeps the summary live and reload() is the way back. Rung zero is untouched. Witness: new spec bounds the session-leg re-arm count when the backend alternates definite and success — the alternating 404/success backend spends exactly 4 transcript reads over 30 s. Mutation probe: with the bound removed the spec fails (expected "spy" to be called 4 times, but got 11 times); restored, green.

Standing findings also resolved in this round

  • R1-4 ([rc:4202164980]) — SnapshotLegError.code was written and never read: removed (verified by grep — no read site anywhere).
  • R1-11 ([rc:4202165030]) — the bootstrap catch hand-inlined failed()'s terminality predicate: the transcript branch now calls failed(error, 'transcript'), and the predicate is shared via the extracted isTerminalAnswer (also used by the R1-24 gate).
  • R1-26 ([rc:4202165039]) — the signals doc stated an authority rule the new-frame path contradicts: the doc now states the cross-leg exception the frame path implements.

Files changed

  • packages/web-shell/client/components/managed/use-managed-session.ts — the R3-1 frame-path expiry, the R1-24 both-reject booking, the R1-10 re-arm bound, the R3-3 operand removal, the R1-4 dead-field removal, the R1-11 predicate dedup, the R1-26 doc correction.
  • packages/web-shell/client/components/managed/use-managed-session.test.tsx — three new specs (R3-1, R1-24, R1-10) and the R3-2 observing phase.
  • packages/web-shell/client/components/managed/ManagedSessionsPage.test.tsx — the R1-17 discriminating identity assertions.

No base-conflict resolution was needed (--conflict false). Integration tests were not run: the touched behavior (the web-shell session hook and the sessions page alert list) is not exercised through the bundled CLI or the integration harness; the managed Vitest suites are the focused coverage.

Verification

  • npm run build — passed (exit 0). Note: this environment's corepack cache is read-only, so the build was run with COREPACK_HOME=/tmp/corepack; the first attempt without it failed with EACCES: permission denied, mkdir '.../.cache/node/corepack/v1', an environment limitation, not a code issue.
  • npm run typecheck — passed (exit 0, 0 errors; same COREPACK_HOME override after building the workspace prerequisites the sandbox lacked).
  • npm run lint — passed (exit 0).
  • npx vitest run client/components/managed (packages/web-shell) — 15 files, 419 passed (includes the pre-existing managed-agent client/provider/messages suites).
  • Mutation probes (each applied, run, then restored and re-run green): R3-1 guard removed → new spec red; R1-24 booking removed → new spec red; R1-10 bound removed → new spec red (11 vs 4 transcript reads); R3-2 (a) stub onEstablished → spec red, (b) newest→specificity ordering → spec red; R1-17 key={index} → identity assertion red; R3-3 operand removed → probe and full suite green both ways (the decline evidence).
中文说明

Autofix 本轮总结 — PR #13179

增长审计(本窗口必需)

结论:sound(见 workdir 中的 growth-audit.json)。KISS 检验:我先假设该账本过度设计并试图证明——明显更简的形态是 PR 之前的单错误字段加写者映射,而那恰恰产生了本轮账本所解决的第 1、2 轮发现簇;每一项累积部件(按 leg 分槽、final 单调性、stall 标志、seq/newest 推导、四种强度的过期操作、判定镜像、endedRef 重新武装、SnapshotLegError、onAlive 管线、isAuthFailure、merge 快路径)都对某个具名发现或故障模式承重。最小改动检验:每个被改文件都可追溯到 PR 的原始问题或已接受的发现(经 git 历史追溯),仅发现一个死操作数并在本轮移除(SnapshotLegError.code)。本窗口的超支在测试侧,是评审本身要求的见证累积;本轮净增 +222/−18,源码侧在扣除删减后基本持平。

可执行反馈

  1. [Critical] R3-1——在存活检查点之后到达的帧从不落实 stream 侧的过期([rc:4208617735]):已修复。 帧路径现在与心跳路径对称:在 delivered = true 之后,若检查点已过,本次尝试落实该点挂起的过期(expireAnswered('stream'))并解除恢复捕获——重放帧也算数,重放过滤器不再吞掉它。stall 记录永不被清除(expireAnswered 拒绝 entry.stall),且在此落实的过期不会被该连接随后的断开恢复,与 onAlive 先例一致。见证:新用例 expires a terminal stream verdict when a replayed frame lands past the proof-of-life point。变异探针:移除新增守卫后该用例变红(expected 'session gone' to be undefined);恢复后转绿。
  2. [Suggestion] R3-2——reveals a still-live paging failure again once a poll blip clears 中的 stream 侧记录从未被观测([rc:4208617754]):已修复。 该用例新增收尾阶段:死游标恢复(pagingDown = false),一次成功的 loadOlder() 撤销 transcript 记录,最终断言读到仍在册的 stream 记录(error === 'Failed to fetch')。既有断言不变;stub 的重连仍然不调用 onEstablished。变异探针:(a) 给 stub 重连加 request.onEstablished?.() 使新断言变红(expected undefined to be 'Failed to fetch');(b) 用 standing 的特异性顺序替换 newest 使用例变红。两者恢复后均转绿。
  3. [Suggestion] R3-3——stall 重新盖章操作数未被钉住([rc:4208617768]):按所提方案拒绝;改为删除该操作数。 我把建议的判别性用例写成探针:保留与删除 !(error instanceof StreamStallError) 两种情况下均为绿色。追踪:每次 stall 重新盖章只发生在同一 gap 趟的 snapshot(true) 成功之后,而该成功会在重新盖章执行前撤销 session 与 transcript 两侧——因此重新盖章落点时刻不可能有竞争记录存活;重同步持续失败时重新盖章根本不会执行。该操作数防守的是一场不可能发生的竞争;删除后 managed 套件保持 419/419 全绿。完整证据已发布在该线程,线程保持开放,以备存在可达的反例。
  4. [Suggestion] R1-17——节点身份断言无法区分消息 key 与索引 key([rc:4208617776]):已修复。 shows a fresh action error alongside a sticky terminal stop 现在覆盖 2→1 迁移:捕获提示对之后,先持有第二个节点,推进一个轮询阶梯使 session 侧撤销,断言存活的提示为 ['Turn already active'] 且其节点即被捕获的节点。变异探针:key={message} 改为 key={index} 使身份断言变红(0 号节点被复用且文本被改写,被捕获节点脱离);恢复后转绿。
  5. [Suggestion] R1-24——两路快照读取都失败时,transcript 侧的确定应答被丢弃([rc:4208617787]):已修复。 双失败分支现在在抛出 session 错误之前先把 transcript 侧自己的应答记入其槽位(failed(transcriptRead.reason, 'transcript'))——但仅当 session 侧是瞬时故障且 bootstrap 将重试时;终止性的 session 应答继续独占显示,因此 gives the session leg the bootstrap decision regardless of settle order 保持不变,且只有 session 分支可以终止 bootstrap。见证:新用例 records a definite transcript answer when both bootstrap reads reject and the session blip is transient(常驻的 history pruned/transcript 与 session 瞬时并存,bootstrap 持续重试)。变异探针:移除该记录后用例变红(expected undefined to be 'history pruned');恢复后转绿。
  6. [Suggestion] R1-10——endedRef 重新武装无冷却无上限且重置两条退避阶梯([rc:4208618393]):已修复。 按会话计数的预算(MAX_SESSION_REARMS = 3,rearmRef/rearmSessionRef)为重新武装设限;预算在 bootstrap 真正完成时以及会话切换时重置,超过预算后已终止的循环保持终止——轮询继续刷新摘要,reload() 是恢复途径。第 0 级阶梯未动。见证:新用例 bounds the session-leg re-arm count when the backend alternates definite and success——404/成功交替的后端在 30 秒内恰好消耗 4 次 transcript 读取。变异探针:移除上限后用例变红(expected "spy" to be called 4 times, but got 11 times);恢复后转绿。

本轮一并解决的存量发现

  • R1-4([rc:4202164980])——SnapshotLegError.code 被写入而从不被读:已删除(经 grep 验证无任何读取点)。
  • R1-11([rc:4202165030])——bootstrap 的 catch 手工内联了 failed() 的终止性谓词:transcript 分支现在调用 failed(error, 'transcript'),谓词通过提取出的 isTerminalAnswer 共享(R1-24 的门控也复用它)。
  • R1-26([rc:4202165039])——signals 文档陈述了一条与新帧路径相矛盾的权威规则:文档现在写明帧路径实际实现的跨 leg 例外。

变更文件

  • packages/web-shell/client/components/managed/use-managed-session.ts——R3-1 帧路径过期、R1-24 双失败记录、R1-10 重新武装上限、R3-3 操作数删除、R1-4 死字段删除、R1-11 谓词去重、R1-26 文档修正。
  • packages/web-shell/client/components/managed/use-managed-session.test.tsx——三个新用例(R3-1、R1-24、R1-10)与 R3-2 的观测阶段。
  • packages/web-shell/client/components/managed/ManagedSessionsPage.test.tsx——R1-17 的判别性身份断言。

无需解决基线冲突(--conflict false)。未运行集成测试:所改行为(web-shell 会话 hook 与会话页提示列表)不会经过打包后的 CLI 或集成测试台;managed Vitest 套件即为聚焦覆盖。

验证

  • npm run build——通过(退出码 0)。注:本环境的 corepack 缓存只读,故构建以 COREPACK_HOME=/tmp/corepack 运行;不带该变量的首次尝试因 EACCES: permission denied, mkdir '.../.cache/node/corepack/v1' 失败,属环境限制而非代码问题。
  • npm run typecheck——通过(退出码 0,0 错误;在补齐沙箱缺失的工作区前置构建后,使用同样的 COREPACK_HOME 覆盖)。
  • npm run lint——通过(退出码 0)。
  • npx vitest run client/components/managed(packages/web-shell)——15 个文件,419 通过(包含既有的 managed-agent client/provider/messages 套件)。
  • 变异探针(每次均为:施加变异、运行、恢复后复跑转绿):移除 R3-1 守卫→新用例红;移除 R1-24 记录→新用例红;移除 R1-10 上限→新用例红(11 对 4 次 transcript 读取);R3-2 (a) stub 加 onEstablished→用例红,(b) newest 换成特异性顺序→用例红;R1-17 改 key={index}→身份断言红;R3-3 删除操作数→探针与全套件双向全绿(即拒绝的证据)。

Deferred non-Critical feedback

Critical-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. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 47 / 测试 735 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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/模型 kimi-k3 · CLI 0.25.0

@qwen-code-review-bot qwen-code-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment thread packages/web-shell/client/components/managed/use-managed-session.ts Outdated
Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
Comment thread packages/web-shell/client/components/managed/use-managed-session.ts
Comment thread packages/web-shell/client/components/managed/use-managed-session.test.tsx Outdated
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.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 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 (c25755dbc8): both Criticals and both Suggestions it posted, the three standing deferral-record items it re-confirmed (N2, N3, N11), and the matched items it re-derived on the R3-1 thread. One round-3 claim was refuted by a probe and is answered with evidence on its thread. One standing deferral (N4) is declined with evidence below.

Findings

  • [Critical] R1-10 fix-induced (comment 4212995670) — fixed. retire() deleted the standing terminal verdict before checking the re-arm budget: once the budget was spent, the poll's next success deleted the verdict, refused the remount, and cleared endedRef, leaving a healthy-looking empty panel with no alert and no path back. Now, past the bound (endedRef set and rearmRef >= MAX_SESSION_REARMS), the session-leg retire skips the delete and keeps endedRef set, so the standing verdict survives every later poll success and reload() remains the way back. The bound's refusal is unchanged (getTranscript stays at 4, subscribeEvents is never called). The bound spec now also reads the rendered state — at t=12000, where the budget has just been spent, it asserts stoppedReason === 'session gone' (this also closes N11, the "spec never reads latest" gap on the same spec).
  • [Critical] R4-1 (comment 4212995679) — fixed. The bootstrap catch's endedRef.current = true was the one cross-run shared-ref write not guarded by abort.signal.aborted; record() is internally guarded, but failed() still returns true on a superseded run, so a stale definite session answer could arm the live run's re-arm path and tear its effect down on the next poll success. The write is now guarded. New witness in the ignores a superseded run family: a parked session-1 bootstrap whose reads settle (definite 404) only after the switch to a healthy session-2 — the successor's transcript reads stay at 2 and its loaded events stay put, instead of the stale re-arm re-spending a third bootstrap read.
  • [Suggestion] R4-2 (comment 4212995685) — fixed. record() now no-ops a final write over a standing final with the identical message, so a repeat terminal answer (e.g. the poll's every-rung 404) can no longer re-stamp its record with a fresh seq and suppress a newer live record on another leg. Not extended to non-final writes. New spec: a session-leg 404 at t=3000, a stream 502 at t=3500, then the identical 404 again at the poll's next rung (t=8999) — the stream's transient keeps the transient field (error === 'Bad gateway') while stoppedReason stays 'session gone'; removing the guard re-stamps and blanks error.
  • [Suggestion] R4-3 (comment 4212995711) — fixed. The both-reads-reject spec's "bootstrap keeps retrying" claim now counts getTranscript (the bootstrap's own read) instead of getSession, which the poll loop also calls — under the mutant the spec was written against, the getSession count passed via the poll. Same beat, same window; the replacement is strictly stronger.
  • R3-1 thread (comment 4208617735) — the round-4 matched items are fixed. The stale proof-of-life comment now names the late-frame disarm alongside the heartbeat's. The disarm rules are now witnessed on both sides: a replay inside the window arms the capture at the proof-of-life point, then a heartbeat (does not restore an armed verdict once a heartbeat certifies the connection) or a replay past the point (does not restore an armed verdict once a late replay lands the expiry) disarms it, and the connection's later drop restores nothing. A replay before the point with no later answer still restores (the existing restores a terminal stream verdict when a replay-only reconnect fails… spec is untouched and green), and replays still count as delivery for the reconnect ladder, as the maintainer confirmed earlier in this thread's history.
  • R3-3 fixed ruling (comment 4212995966) — verified, no code change. record() carries no stall-operand; the guards are the monotonicity guard, the new identical-final guard, and the stall guard. Re-verified against the current tree.
  • N2 (round-3 deferral, onAlive disarm unwitnessed) — fixed. The heartbeat-certifies-then-drop spec above reds when the expiredVerdictMessage reset in onAlive is deleted (probe B4 below).
  • N3 (round-3 deferral, clearing assertions could not distinguish retire from escalate) — fixed. Both stall-clearing beats of disarms the stall alert once the stream advances again now also assert stoppedReason === undefined — an escalated (final) stall record would surface there while leaving error undefined (probe B6 below).
  • N4 (round-2 deferral, stoppedLeg has no production read site) — declined with evidence. Confirmed: 1 write site in the hook, ~54 reads, all in tests, none in the page. Removing the export means deleting exactly the assertions that pin leg attribution and authority precedence — the witnesses the R7–R10 Critical fixes stand on (stoppedLeg is the only observable surface of which authority issued the standing verdict); that is a coverage loss, not a simplification. Wiring it into the page's render (different copy per leg) is a UI/product call no reviewer has specified. Recorded here for a maintainer who wants it gone or wired; the thread was the round-2 deferral record, so there is no inline thread to answer.
  • Round-1 R1-1 (comment 4202164933) — fixed earlier (d8cf0a0aca), re-verified: the no-resurrect family plus this round's two new disarm witnesses are green, and probe B4/B6 mutants red them. Resolved.
  • Round-3 R1-10 bound request (comment 4208618393) — the bound landed in c25755dbc8 and this round's fix closes the exhausted-budget hole its witness missed. Resolved.
  • Round-3 R1-24 narrowed (comment 4208617787) — the both-reject recording landed in c25755dbc8; its spec is the one R4-3 hardened this round. Resolved.

Declined after probing (answered on its thread, left open)

  • Comment 4208617776 (R1-17 node-identity) — the claim was that the keyed-alerts spec's node-identity assertion cannot distinguish a message key from an index key. Probe: switched the alert list to key={index} in ManagedSessionsPage.tsx and re-ran shows a fresh action error alongside a sticky terminal stop — it goes red at exactly that assertion (with index keys the surviving alert's node is unmounted and index 0's node is repurposed, so toBe(actionAlert) fails). The spec already pins message keying; no change made.

Not in this round (recorded, not silently dropped)

  • Round 4's own deferred item (use-managed-session.test.tsx:4530, the 50 ms settle sleep) stays deferred — the reviewer marked it "not requested in this round" and it is a test-isolation question, not a defect.
  • PR body/title staleness (issue comments 6018332618, 6022424066): the "hard stop" sentence and the test counts describe older heads. Updating the PR body is a GitHub write, which this round cannot perform — flagging for the workflow/maintainer.
  • The red Test (ubuntu-latest, Node 22.x) lane was attributed in comment 6024725225 as inherited from main (stale merge ref on packages/cli/src/acp-integration/); this branch already carries the merge of current main (264823fcbc) and the failing specs pass on this tree.

Mutation probes (each guard's witness)

Each probe mutated exactly one line of use-managed-session.ts, ran the hook suite, and restored the file; the restored tree is 113/113 green.

probe mutation result
Pre-round this round's test file against the pre-round source (git show HEAD:…) exactly the 3 Critical-resolving specs red (R1-10, R4-2, R4-1)
B1 budgetSpent forced false in retire() red: bounds the session-leg re-arm count… only
B2 drop the bootstrap catch's abort guard red: ignores a superseded run booking a terminal bootstrap stop only
B3 drop the identical-final guard in record() red: does not re-stamp an identical terminal answer… only
B4 drop the onAlive disarm red: does not restore an armed verdict once a heartbeat certifies… only
B5 drop the late-frame landing-block disarm red: does not restore an armed verdict once a late replay lands… only
B6 escalate instead of retire on the advancing resync red: disarms the stall alert once the stream advances again only

Verification

  • npx vitest run client/components/managed/use-managed-session.test.tsx (packages/web-shell) — 113 passed, 0 failed (109 baseline + 4 new specs)
  • npx vitest run client/components/managed (packages/web-shell) — 15 files, 423 passed, 0 failed
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx eslint --max-warnings 0 on both touched files — passed (the pre-commit hook caught one require-yield in the new R4-2 spec's park-only generator; fixed with the house yield* [] pattern and re-verified)
  • npx prettier --check on both touched files — passed
  • Pre-round and mutation probes — table above; every new guard is red-witnessed
  • Integration tests — not run: the touched behavior is the web-shell hook's failure lifecycle, fully exercised by the unit suite; nothing here is only reachable through the bundled CLI or the integration harness
  • npm run generate:settings-schema — not applicable: no settings source changed
中文说明

轮次总结 —— 第 4 轮评审(head 79d77b2)

本轮处理第 4 轮评审(c25755dbc8):它发布的两条 Critical 与两条 Suggestion、它重新确认的三条挂起缓议项(N2、N3、N11),以及它在 R3-1 线程上重新推导出的匹配项。第 3 轮的一条主张经探测证伪,已在原线程带证据回复。一条挂起的缓议项(N4)按证据婉拒,理由见下。

发现逐条处理

  • [Critical] R1-10 修复引入(评论 4212995670)——已修复。 retire() 在检查重新挂载预算之前就无条件删除仍在生效的终止判定:预算用尽后,轮询的下一次成功会删掉判定、拒绝重挂载并清掉 endedRef,留下一个看似健康却空白、无告警、无回归路径的面板。现在,越过上限后(endedRef 已置位且 rearmRef >= MAX_SESSION_REARMS),session 腿的 retire 跳过删除并保持 endedRef,于是判定在之后的每次轮询成功中保持,reload() 仍是回归路径。上限的拒绝行为不变(getTranscript 恒为 4,subscribeEvents 从不调用)。上限用例现在还读取渲染态——在预算刚好用尽的 t=12000 断言 stoppedReason === 'session gone'(同时闭合了 N11,即同一用例"从不读取 latest"的缺口)。
  • [Critical] R4-1(评论 4212995679)——已修复。 bootstrap catch 里的 endedRef.current = true 是唯一一处没有 abort.signal.aborted 保护的跨 run 共享 ref 写入;record() 内部有保护,但 failed() 在被取代的 run 上仍返回 true,于是一个迟到的确定性 session 应答可以武装存活 run 的重挂载路径,并在下一次轮询成功时拆掉它的 effect。该写入现在已加保护。在 ignores a superseded run 家族中新增见证:session-1 的 bootstrap 两条读取都被挂起,直到切换到健康的 session-2 之后才以确定性 404 落定——后继 run 的 transcript 读取保持 2 次、已加载事件不变,而不是被陈旧的 re-arm 再花掉一次 bootstrap 读取。
  • [Suggestion] R4-2(评论 4212995685)——已修复。 record() 现在对"在相同消息的终止记录上再写一条终止记录"直接短路,因此重复出现的同一终止应答(例如轮询每个梯级都 404)不会再用新 seq 重盖记录、压制另一腿上更新的存活记录。没有扩展到非 final 写入。新用例:t=3000 session 腿 404、t=3500 流 502、轮询下一梯级(t=8999)又是同一个 404——流的 transient 保持 transient 字段(error === 'Bad gateway'),stoppedReason 保持 'session gone';去掉该守卫会重盖并把 error 清空。
  • [Suggestion] R4-3(评论 4212995711)——已修复。 双腿都拒绝用例中"bootstrap 持续重试"的断言,现在数 getTranscript(bootstrap 自己的读取),不再数 getSession(轮询循环也会调用它)——在该用例针对的变异体下,getSession 计数靠轮询就能通过。同一节拍、同一窗口;替换后的断言严格更强。
  • R3-1 线程(评论 4208617735)——第 4 轮匹配到的两项已修复。 过时的 proof-of-life 注释现在把"晚到帧的解除武装"与心跳并列写出。解除武装规则两侧现在都有见证:窗口内的重放帧在 proof-of-life 时点武装捕获,随后心跳(does not restore an armed verdict once a heartbeat certifies the connection)或越过时点的重放帧(does not restore an armed verdict once a late replay lands the expiry)解除武装,连接其后断开不再恢复任何东西。时点之前的重放且之后没有应答时仍然恢复(既有的 restores a terminal stream verdict when a replay-only reconnect fails… 用例未动、保持绿色),重放帧仍按维护者此前确认的那样计入重连阶梯的送达。
  • R3-3 已修复裁定(评论 4212995966)——已复核,无代码改动。 record() 不含 stall 操作数;守卫为单调性守卫、新增的相同终止守卫与 stall 守卫。已对当前树复核。
  • N2(第 3 轮缓议,onAlive 解除武装无见证)——已修复。 上面的"心跳证明后断开"用例在删除 onAlive 中 expiredVerdictMessage 复位时变红(下方探测 B4)。
  • N3(第 3 轮缓议,清除断言无法区分"退休"与"升级为终止")——已修复。 disarms the stall alert once the stream advances again 的两个清除节拍现在同时断言 stoppedReason === undefined——被升级为终止的 stall 记录会在这里暴露,而 error 两种情况下都是 undefined(下方探测 B6)。
  • N4(第 2 轮缓议,stoppedLeg 没有生产读点)——按证据婉拒。 已确认:hook 里 1 处写入,约 54 处读取全部在测试中,页面没有读。删除该导出意味着删掉恰好钉住腿归属与权限优先级的那些断言——它们是 R7–R10 各 Critical 修复所依赖的见证(stoppedLeg 是"哪条腿给出了当前判定"的唯一可观察面);这是覆盖损失,不是简化。把它接进页面渲染(按腿区分文案)是 UI/产品决定,没有评审者提出过。记录在此,交由维护者决定删除或接线;该意见源自第 2 轮缓议记录,没有可回复的行内线程。
  • 第 1 轮 R1-1(评论 4202164933)——此前已修复(d8cf0a0aca),本轮复核:不复活家族加本轮两个新的解除武装见证均为绿,探测 B4/B6 的变异体使它们变红。标记为解决。
  • 第 3 轮 R1-10 上限请求(评论 4208618393)——上限在 c25755dbc8 落地,本轮修复堵上了其见证漏掉的"预算用尽"空洞。标记为解决。
  • 第 3 轮 R1-24 收窄重裁(评论 4208617787)——双拒记录已在 c25755dbc8 落地;其用例正是本轮 R4-3 加固的那一个。标记为解决。

探测后婉拒(已在原线程回复,保持打开)

  • 评论 4208617776(R1-17 节点同一性)——其主张是:告警 key 化用例的节点同一性断言无法区分"消息做 key"与"索引做 key"。探测:把 ManagedSessionsPage.tsx 的告警列表改成 key={index} 后重跑 shows a fresh action error alongside a sticky terminal stop,恰好红在该断言上(索引 key 下存活告警的节点被卸载、索引 0 的节点被改写复用,toBe(actionAlert) 失败)。该用例按现有写法已经钉住消息 key;未做改动。

本轮未处理(已记录,非静默丢弃)

  • 第 4 轮自己的缓议项(use-managed-session.test.tsx:4530,50ms 落定 sleep)保持缓议——评审者标注"本轮不要求处理",且那是测试隔离问题,不是缺陷。
  • PR 标题/正文过时(issue 评论 6018332618、6022424066):"hard stop"一句和测试计数描述的是旧 head。更新 PR 正文属于 GitHub 写操作,本轮无权执行——标记给工作流/维护者。
  • 红色的 Test (ubuntu-latest, Node 22.x) 已在评论 6024725225 中归因为继承自 main(packages/cli/src/acp-integration/ 上的陈旧 merge ref);本分支已合入当前 main(264823fcbc),失败用例在本树通过。

变异探测(每个守卫的见证)

每个探测只改 use-managed-session.ts 的一行,跑 hook 套件后还原文件;还原后的树为 113/113 全绿。

探测 变异 结果
前置基线 本轮测试文件对轮前源码(git show HEAD:…) 恰好 3 条对应 Critical 修复的用例变红(R1-10、R4-2、R4-1)
B1 retire() 中 budgetSpent 恒为 false 仅 bounds the session-leg re-arm count… 变红
B2 去掉 bootstrap catch 的 abort 守卫 仅 ignores a superseded run booking a terminal bootstrap stop 变红
B3 去掉 record() 的相同终止守卫 仅 does not re-stamp an identical terminal answer… 变红
B4 去掉 onAlive 的解除武装 仅 does not restore an armed verdict once a heartbeat certifies… 变红
B5 去掉晚到帧落地块的解除武装 仅 does not restore an armed verdict once a late replay lands… 变红
B6 推进式重同步改为升级而非退休 仅 disarms the stall alert once the stream advances again 变红

验证

  • npx vitest run client/components/managed/use-managed-session.test.tsx(packages/web-shell)——113 通过,0 失败(基线 109 + 新增 4 条)
  • npx vitest run client/components/managed(packages/web-shell)——15 个文件,423 通过,0 失败
  • npm run build ——通过
  • npm run typecheck ——通过
  • npm run lint ——通过
  • 对两个改动文件 npx eslint --max-warnings 0 ——通过(pre-commit 钩子曾发现新 R4-2 用例的"只挂起"生成器缺 yield,已按本仓库的 yield* [] 惯例修复并复验)
  • 对两个改动文件 npx prettier --check ——通过
  • 前置基线与变异探测 ——见上表;每个新守卫都有变红见证
  • 集成测试 ——未运行:改动是 web-shell hook 的失败生命周期,单测套件已完整覆盖;没有任何行为只能通过打包后的 CLI 或集成装置触达
  • npm run generate:settings-schema ——不适用:未改动 settings 源

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/模型 kimi-k3 · CLI 0.25.0

@yiliang114

yiliang114 commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Closeout status at 79d77b243b31344a3a4c6522f1a00c1b44b6f599: the branch is mergeable and all current checks have completed successfully or been skipped (28 successful, 45 skipped). There are no pending or failed checks.

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 qwen-code-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants