Repository navigation
fix(web-shell): follow an accepted approval answer to its terminal operation - #13609
Conversation
…eration actions/respond answers 202 once the answer is admitted, but delivery to the Harness can still fail (a revoked Workspace grant), and the operation stays running while the Session is blocked. The Java provider treated any non-failed 202 as applied, so a later failure left the approval silently unanswered. Read the same operation through operations/query until it is terminal, keeping the original attempt and idempotency key; report an answer still unsettled after ~30s as unconfirmed so the card returns and a second click replays the same operation.
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
…sertions Both assertions are awaited only after the fake timers advance, so the expect chain has to be stored in a variable. Mark them with the same eslint-disable convention this repo already uses for that deferred-await pattern (responses-pipeline.test.ts, acp-bridge bridge.test.ts) instead of restructuring the tests, which would leave the rejection unhandled while the timers run. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmuy7yk8zcm
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template looks good ✓ — all nine sections present, and the "Not validated / out of scope" list is unusually honest about what this does not cover. Problem: real, and I verified it in the base code rather than taking the description's word. Direction: aligned. This closes a documented gap in an active workstream instead of inventing one, and it is client-side only — Size: the core-module gate does not apply — all three files sit under Approach: scope feels right and I would not cut anything. Reading the same operation back is the minimal way to learn the outcome, and refusing to re-send the answer is what keeps the idempotency key and the original attempt intact — a retry-loop that re-answered would have been the obvious wrong turn here. Related D6 items (fresh-attempt policy after a definitive failure, input previews, resuming across a reload) are explicitly left in the tracker rather than folded in, which is the right call. The one alternative I considered was settling over the existing event stream instead of polling; that avoids the poll budget entirely but needs stream correlation and is well past the bounded slice #12867 asked for, so polling is the better trade. Risk: no elevated risk signals — none of the changed files match the high-revert-correlation paths. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 九个章节齐全,"未验证/范围外"一节对没有覆盖的内容交代得相当诚实。 问题: 真实存在,我在基线代码里核实过,没有只采信描述。 方向: 对齐。这是在补齐一个有文档记录的缺口,而不是新造需求;且纯客户端改动 —— 规模: 核心模块门禁不适用 —— 三个文件都在 方案: 范围合理,我不会砍任何东西。回读同一个 operation 是获知结果的最小做法;而不重发答复正是保住幂等键与原始那次尝试的关键 —— 这里最容易走错的方向就是做一个重新作答的重试循环。相关的 D6 事项(确定性失败后的新尝试策略、输入预览、刷新后恢复跟踪)被明确留在跟踪表里而非塞进本 PR,这个取舍是对的。我唯一考虑过的替代方案是走已有事件流结算而不是轮询;那能完全省掉轮询预算,但需要流关联,远超 #12867 要求的有界切片,所以轮询是更好的权衡。 风险: 无升级风险信号 —— 改动文件都不匹配高回滚相关性路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewI wrote down my own fix before reading the diff: add a
Three things worth raising, none of them blocking: 1. A definitive end-state can now reach the user as "retry the same option", and the classifier that exists to prevent that will not catch it. §6.2 says an operation "completes … with its end state ( 2. Nothing tests the 3. Abort can lag by up to 8 s. Nit: the pre-existing approval test that gained a third mock sits outside the new fake-timers One thing I checked because it looked like a UX regression and turned out not to be: on a recovery-blocked Session the operation can legitimately stay The reshaped handshake, since the "admitted ≠ applied" distinction is the whole change: sequenceDiagram
participant P1 as WebShell approval card
participant P2 as provider respond
participant P3 as Java service
P1->>P2: user picks an option (card hides immediately)
P2->>P3: POST actions respond
P3-->>P2: 202 with operation op-1, status running
Note over P2: admitted, not applied - the old code stopped here
loop bounded reads, 9 attempts, about 30s total
P2->>P3: POST operations query (same sessionId and operationId)
P3-->>P2: latest durable status
end
alt completed
P2-->>P1: resolve, card stays hidden
else failed, cancelled or recovery_blocked
P2-->>P1: reject, card returns with the reason
else still pending or running at budget end
P2-->>P1: reject as not confirmed yet, same key replays on retry
end
Testing evidenceThis is an unattended CI run, so I did not build or execute anything from this PR — the evidence below is the PR's own CI on the reviewed commit, read through the API.
The two Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified: why
I am deliberately not calling this pre-existing infra noise, because the concurrency evidence argues against that, and not calling it PR-caused, because the diff gives no mechanism. It needs a human eye on the log once the run finishes. Sandboxed verification would settle the behavioural claim: 中文说明代码审查我在读 diff 之前先写下了自己的方案:给 client 加
三点值得提出,都不构成阻塞: 1. 确定性终态现在可能以"重试同一选项"呈现给用户,而为此存在的分类器抓不到它。 §6.2 写明 operation "以其结束态( 2. 3. abort 最多滞后 8 秒。 小问题:那个新增了第三个 mock 的既有审批用例位于新的假定时器 有一处我因为看起来像 UX 回归而专门核查、结果发现不是:在被恢复阻塞的 Session 上,operation 可以合法地在预算耗尽后仍为 改动重塑后的握手流程如下,因为"已接纳 ≠ 已生效"这个区分正是整个变更的核心: (时序图见上,中文不重复) 测试证据这是无人值守的 CI 运行,所以我没有构建或执行本 PR 的任何代码 —— 下面的证据是通过 API 读到的、该 PR 自己在被审查 commit 上的 CI 结果。
两个 未验证:
我刻意不把它称为既有基础设施噪声,因为并发证据不支持;也不称其为本 PR 引起,因为 diff 给不出机制。run 结束后需要人看一眼日志。 沙箱验证可以坐实这个行为主张: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — the code is solid and I found no blocker; the missing fifth is a CI signal I could not read, not a doubt about the change. Stepping back: this is what a well-scoped slice of a tracked workstream looks like. I went looking for the usual problems and did not find them. The diff is 68 production lines against 113 test lines, every one of which serves the stated goal — no drive-by refactor, no formatting churn, no opportunistic "while I was in here". The adjacent D6 items that would have been tempting to bundle (fresh-attempt policy, input previews, reload-resume) are explicitly left in #12867 instead. The new code reuses what exists rather than standing up a parallel mechanism: same client method shape as its two neighbours, same I also checked the part I was most suspicious of, because "poll until settled" is where a change like this usually goes wrong. It does not. The status check sits at the top of the loop body, so an already-terminal admission response returns without a single sleep — the common case pays nothing. The budget is nine reads totalling 29,750 ms, so "about 30 s" is a measured claim rather than a round number. And the thing that makes the whole approach safe — never re-sending the answer, so the original attempt and its idempotency key survive — is exactly what the tests pin, via a URL sequence that proves one answer and nothing but reads afterwards. The bug itself is real, and I confirmed it in the base code rather than accepting the framing: What keeps this at 4 rather than 5 is honest uncertainty, not a defect I am waving through:
Would I maintain this in six months without cursing the author? Yes. The polling budget is a named constant with a comment explaining why it is shaped that way, the doc comment on Verdict: approve, deferred until CI lands green on 中文说明信心度:4/5 —— 代码扎实,我没有发现阻塞项;少的那一分来自一个我读不到的 CI 信号,而不是对这个改动本身的怀疑。 退一步看:这是一个被跟踪的工作流里、切片划分得当的 PR 该有的样子。我按常见的毛病逐条去找,都没有找到。diff 是 68 行生产代码对 113 行测试,每一行都服务于既定目标 —— 没有顺手重构,没有格式抖动,没有"既然改到这里就顺便"。邻近那些很容易被捆绑进来的 D6 事项(确定性失败后的新尝试策略、输入预览、刷新后恢复)被明确留在 #12867 里。新代码复用既有实现而不是另起一套机制:client 方法与它的两个邻居形态一致,走同一个 我最怀疑的部分也专门查了,因为"轮询到结算"正是这类改动最容易出错的地方。它没有出错。状态检查位于循环体开头,所以入场响应本身已是终态时不会有任何 sleep —— 常见路径零成本。预算是九次读取合计 29,750 ms,因此"约 30 秒"是实测说法而非凑整。而让整个做法成立的那一点 —— 绝不重发答复,从而保住原始尝试及其幂等键 —— 正是测试钉住的东西:URL 序列证明只有一次答复,其后全是读取。 bug 本身是真实的,我在基线代码里确认过,没有直接采信 PR 的表述: 让它停在 4 分而不是 5 分的,是诚实的不确定性,而不是我放水的缺陷:
六个月后我来维护这份代码,会骂作者还是会谢作者?会谢。轮询预算是一个具名常量,带注释解释了它为何是这个形状; 结论:批准,但延迟到 CI 在 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
chiga0
left a comment
There was a problem hiding this comment.
Review — round 1
Tier: Standard (feature — new polling loop in a web-shell client path; no persisted format, no trust-boundary change, no auth handling).
Verdict: No blocking findings. Approval blockers: none.
Scope
Reviewed: (new settleActionResponse function + respond changes), java-managed-agent-client.ts (new queryOperation method), java-managed-agent-provider.test.ts (updated existing test + two new tests). Cross-file: managed-agent-provider.ts (interface contract), use-managed-actions.ts (only caller of respond), managed-approval.ts, package.json exports.
Not reviewed: managed-agent-api.ts schema (reviewed only the relevant WebShellOperation, WebShellCommandOperation, WebShellOperationRequest entries). Execution rungs: not run locally; no working tree available. See disclosure below.
Checks performed
Class 1 — writer/reader contract: respondAction returns WebShellCommandOperation; queryOperation returns WebShellOperation (the union WebShellCwdOperation | WebShellCommandOperation). The read.type !== 'action_response' guard at provider.ts line 72 correctly narrows the union and rejects a cwd_change operation being accepted as an action-response. Downstream in respond, after settleActionResponse returns, only terminal statuses can have been returned; the failed | cancelled | recovery_blocked check is complete for the error path; completed falls through silently, which is the correct success path.
Class 2 — API compatibility: queryOperation is an additive method on the already-exported JavaManagedAgentClient. No barrel re-export was changed, no existing method signature was altered. No breaking change to any external consumer.
Class 3 — error handling: settleActionResponse propagates errors from queryOperation without swallowing. No init-promise cached on failure. Abort signal is checked (signal?.throwIfAborted()) immediately after each timer fires and before each network request.
Class 6 — resource lifecycle: setTimeout is not retained; no listener is attached. No leak path.
Class 7 — sibling entrances: Only one site in the provider calls respondAction (the respond function on line 111). settleActionResponse is only reachable from there.
Stated intent vs. code (class 10):
- PR says "never re-sends the answer": confirmed —
settleActionResponsecalls onlyqueryOperation, neverrespondActionagain. - PR says "bounded backoff ~30 s": delays 250+500+1000+2000+2000+4000+4000+8000+8000 = 29,750 ms. ✓
- PR says "refuses a read-back whose type is not action_response": confirmed at provider.ts line 72.
Test validity:
- Reverted-fix probe (static): without
settleActionResponse,respondActionreturns{status:'pending'}→respondreturns void → the new tests'.rejects.toThrow(…)assertions fail because the promise resolves. Both new tests are efficacious. - Existing modified test: the
actions/respondmock now returns{status:'running'}, requiring one real 250 ms poll cycle beforecompletedis returned. No fake-timer setup in that test path; the 250 ms real-time wait is within normal test-suite tolerance.
Cross-check: One prior review from qwen-code-review-bot (APPROVED, "LGTM, looks ready to ship — CI landed green"). No findings to reconcile; their conclusion agrees with mine.
Unreviewed dimensions
- Local execution was not run (no working tree). Rung 1–3 not run. Not added to
approvalBlockersbecause no open finding required execution to resolve, and the PR author noted CI as the verification path.
Reviewed with AI assistance.

What this PR does
The WebShell's Java Managed Agent provider now follows an accepted approval answer until its operation settles.
actions/respondanswers202with anaction_responsecommand operation as soon as Java admits the answer. Until now the provider treated any non-failed202(pendingorrunning) as applied, so the approval card disappeared at that point.The provider now reads that same operation through
operations/querywith a bounded backoff (about 30 s in total) until it is terminal. It never re-sends the answer, so the original attempt and its idempotency key are preserved. A terminalfailed,cancelledorrecovery_blockedtakes the existing error path, and the card comes back with the failure. An operation stillpending/runningwhen the budget runs out is reported as "not confirmed yet". The card returns, and answering again uses the same key (<actionId>:<optionId>), so Java replays the same operation and tracking resumes.The client gains
queryOperation. The provider refuses a read-back whosetypeis notaction_response.Why it's needed
This is the D6 gap the #12380 tracker lists as the clearest uncovered bounded slice. Java already returns the operation and serves
/operations/query, but the client only looked at the immediate response. After admission an answer can still fail: R1-5 in #13163 is a revokedcan_createduring delivery backoff, which ends the operationFAILED workspace_unavailable. The design also keeps the operationrunningwhile the Action stays requested on a blocked Session (docs/design/2026-09-30-managed-agent-actions.md§6.2). In both cases the user saw the card vanish as if the answer had applied.Reviewer Test Plan
How to verify
npx vitest run packages/web-shell/client/components/managed/java-managed-agent-provider.test.ts:runninganswer to be followed by anoperations/queryfor the same{sessionId, operationId}, with no secondactions/respond.pending → running → failed(workspace_unavailable)rejects with that code after two reads.runningrejects as "not confirmed yet (operation op-1 is running)", and every call after the single answer is a read of that operation.Evidence (Before & After)
N/A, no visual change. Before: a
202 runninganswer resolved immediately, and a later failure was invisible. After: the card stays hidden while the answer settles, and returns with the error if it fails or stays unconfirmed.Tested on
Environment (optional)
Not run locally. Verification is the CI
Test (ubuntu-latest)web-shell suite. Prettier (--experimental-cli, 3.6.1) is clean on the changed files.Risk & Scope
Linked Issues
Part of #12867 (D6 accepted-response operation tracking, from the #12380 snapshot).
中文说明
本 PR 做了什么
WebShell 的 Java Managed Agent provider 现在会一直跟踪一个已被接受的审批答复,直到它的 operation 结束。
actions/respond在 Java 接纳答复后就返回202和一个action_response命令 operation。此前 provider 把所有未失败的202(pending或running)都当作已生效,审批卡片在这一刻就消失了。现在 provider 会通过
operations/query读取同一个 operation,带有有界退避(总共约 30 秒),直到它进入终态。它不会重发答复,因此原始那次尝试及其幂等键都被保留。终态为failed、cancelled或recovery_blocked时走已有的报错路径,卡片带着失败原因重新出现。预算耗尽时如果 operation 仍是pending/running,会报告"尚未确认",卡片重新出现;再次作答使用同一个键(<actionId>:<optionId>),Java 会重放同一个 operation,跟踪随之继续。客户端新增
queryOperation;provider 会拒绝type不是action_response的读回结果。为什么需要
这是 #12380 跟踪表列出的、最清楚且尚无实现的有界 D6 切片。Java 已经返回 operation,也提供了
/operations/query,但客户端只看即时回复。答复被接纳之后仍然可能失败:#13163 的 R1-5 就是在投递退避期间撤销can_create,operation 最终以FAILED workspace_unavailable结束。设计中还规定,Session 被阻塞、Action 仍处于 requested 时,operation 保持running(docs/design/2026-09-30-managed-agent-actions.md§6.2)。这两种情况下,用户看到的都是卡片消失,好像答复已经生效。评审测试计划
如何验证
npx vitest run packages/web-shell/client/components/managed/java-managed-agent-provider.test.ts:running后,接着用同一个{sessionId, operationId}调用operations/query,并且不会再发第二次actions/respond。pending → running → failed(workspace_unavailable)在两次读取后以该错误码拒绝。running的 operation 会以"not confirmed yet (operation op-1 is running)"拒绝,并且在唯一一次答复之后,每次调用都是对该 operation 的读取。前后对比
N/A,无界面变化。修改前:答复返回
202 running就立即视为完成,之后的失败无从得知。修改后:答复结算期间卡片保持隐藏;如果失败或一直未确认,卡片会带着错误重新出现。已测试平台
本地未运行,以 CI 的
Test (ubuntu-latest)web-shell 测试为准。改动文件已用 Prettier(--experimental-cli,3.6.1)检查通过。风险与范围
关联 Issue
#12867 的一部分(D6 已接受答复的 operation 跟踪,来自 #12380 的快照)。