fix(daemon): mark session supported-commands no-store (#10918) - #10978
fix(daemon): mark session supported-commands no-store (#10918)#10978harjothkhara wants to merge 1 commit into
Conversation
A file-based Skill can be updated in place while the daemon session stays alive. The endpoint serving a session's slash-command and Skill metadata returned dynamic command state with no cache directive, leaving a client free to store the representation and keep serving it -- so command discovery kept advertising the old description and argument hint while invoking the same command already ran the updated Skill. The 304 was not validating a stale body: the payload is rebuilt from the live command registry on every request and the weak validator is derived from that freshly built body, so a changed body is answered 200 with a new validator. What was missing was any signal that the representation must not be reused, which is what strands a client that has already stored one. Mark both branches of the endpoint no-store, matching the other live session-state reads in the same router. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for the PR — and for a description that actually argues the case instead of restating the diff. That made this easy to check. Template looks good ✓ — every required section is filled in. One small omission: the template's Problem: observed, not theoretical. #10918 has a concrete reproduction — a named Skill ( Direction: aligned. You picked the exact route the issue names, and Size: not applicable — Approach: the scope is right, and deliberately narrow. Marking both branches of the route (the live-session read and the virtual sub-agent stub) is the correct call — a directive that only applied to one kind of session id would be a trap. Leaving the sibling reads alone is defensible and you said why. One factual correction: the body lists Risk: this is where I have to stop you. The branch is stale and GitHub reports the PR as .json(await runtime.bridge.getSessionSupportedCommandsStatus(sessionId))and const status = await runtime.bridge.getSessionSupportedCommandsStatus(sessionId);
res.status(200).json(
runtime.trusted ? status : redactWorkflowsFromSupportedCommands(status),
);Both sides rewrote the same lines, so the conflict lands precisely on a trust boundary. Resolving it by keeping your side — the easy mistake, and the one an No elevated risk signals from the revert-history path check — neither changed file matches the high-risk patterns. Flagging these for discussion; I am still reading the code and the CI signal, and will follow up below. 中文说明感谢贡献 —— 也感谢这份真正在论证、而不是复述 diff 的 PR 描述,让核查变得很轻松。 模板完整 ✓,各必填部分都写了。一个小遗漏:模板里的 问题: 是已观测到的问题,不是理论性加固。#10918 有具体复现 —— 明确的 Skill( 方向: 对齐。你改的正是 issue 点名的那条路由,而且 规模: 不适用 —— 方案: 范围合适且刻意收窄。同时给路由的两个分支(实时会话读取与 virtual sub-agent stub)加指令是正确的 —— 只对一种 session id 生效反而是个坑。不动其它同类读取也说得通,并且你说明了理由。一处事实更正:正文把 风险: 这里是必须拦一下的地方。分支已过期,GitHub 判定本 PR 为 .json(await runtime.bridge.getSessionSupportedCommandsStatus(sessionId))而 const status = await runtime.bridge.getSessionSupportedCommandsStatus(sessionId);
res.status(200).json(
runtime.trusted ? status : redactWorkflowsFromSupportedCommands(status),
);双方改动了同样的行,因此冲突恰好落在信任边界上。如果按你这一侧来解决冲突 —— 这是最容易犯的错, 回滚历史的高风险路径检查未发现升级信号 —— 两个改动文件都不匹配高风险模式。 先提出这些;我仍在继续看代码与 CI 信号,结果会在下面跟进。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewBefore reading the diff I wrote down what I would have done from the title and the "Why it's needed" section alone: mark the route's response non-storable, on both branches, using whatever directive the neighbouring live-state reads already use, and pin it with a test that mutates the payload between two reads. Your PR is that, and I did not find a simpler path you missed. Two alternatives I considered and think you were right to reject:
Is the directive actually load-bearing for the reported client? I checked rather than assumed. The Web Shell reads this route through Your The new test is a real pin, not a header assertion. It reads once, mutates what the bridge returns to simulate the in-place Skill edit, then re-reads carrying the first response's The four-line comment you added is longer than the change it documents, but the why here genuinely is not obvious from the code — a future reader would reasonably wonder why a command list needs to be non-storable. It earns its place. Blocking1. The branch is stale, and the conflict sits on a trust boundary. GitHub reports // main — must survive the rebase
const status = await runtime.bridge.getSessionSupportedCommandsStatus(sessionId);
res
.status(200)
.set('Cache-Control', 'no-store') // ← your change
.json(
runtime.trusted
? status
: redactWorkflowsFromSupportedCommands(status), // ← must not be dropped
);Resolving by keeping your side — the easy mistake, and the one Your test helpers survive the rebase untouched — Test evidenceThis is an unattended CI run, so per the gate rules I did not build, run, or execute anything from this PR — no There is no test evidence yet, and I want to be blunt about that rather than let an all-green-looking table imply otherwise. The head commit carries 16 check-runs, but every one of them is
Not verified: that the new test passes, and that the route returns Sandboxed verification would settle this, and it is the one thing that could close the gap before a maintainer commits: 中文说明代码审查在读 diff 之前,我只根据标题和「Why it's needed」写下了自己会怎么做:给这条路由的响应加上不可存储指令,两个分支都加,沿用相邻实时状态读取已有的指令,再用一个"两次读取之间改动 payload"的测试把它钉住。你的 PR 正是如此,我也没找到更简单而被你漏掉的路径。 我考虑过、并认为你拒绝得对的两个替代方案:
这条指令对报告问题的客户端是否真的生效? 我是查证过的,不是假设。Web Shell 通过 你对 新测试是真正的钉子,不是单纯的 header 断言。 它先读一次,然后改动 bridge 的返回值以模拟 Skill 就地编辑,再带着第一次响应的 你新增的四行注释比它所解释的改动还长,但这里的 why 确实无法从代码本身看出来 —— 后来的读者完全有理由疑惑为什么一份命令列表需要不可存储。这条注释是有价值的。 阻塞项1. 分支已过期,且冲突落在信任边界上。 GitHub 判定 // main —— rebase 后必须保留
const status = await runtime.bridge.getSessionSupportedCommandsStatus(sessionId);
res
.status(200)
.set('Cache-Control', 'no-store') // ← 你的改动
.json(
runtime.trusted
? status
: redactWorkflowsFromSupportedCommands(status), // ← 不能丢
);如果按你这一侧解决冲突 —— 这是最容易犯的错, 你的测试辅助在 rebase 后无需改动 —— 测试证据这是一次无人值守的 CI 运行,因此按照门禁规则我没有构建、运行或执行本 PR 的任何代码 —— 没有 目前没有任何测试证据,我想直说,而不是让一张看起来全绿的表造成误解。 head commit 上有 16 个 check-run,但每一个都是 未验证:新测试是否通过,以及路由运行时是否真的返回 沙箱验证可以定论此事,也是维护者投入之前唯一能补上缺口的动作: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 2/5 — the fix itself is right and I expect to approve it once rebased; as submitted it cannot merge, and the conflict lands on a trust boundary. Stepping back: my hesitation is not about the design. I proposed the same change before reading the diff, and you matched it — one header, both branches, reusing the convention already in the router, plus a test that mutates the payload instead of just asserting a header. There is no new mechanism, no new abstraction, no drive-by refactor, no formatting churn, and nothing in the diff that the stated goal does not need. Leaving the sibling reads alone with a written reason is restraint, not an omission, and if I were maintaining this route in six months I would thank you for the comment explaining why a command list has to be non-storable. The problem is real, and I did not take your framing on trust. #10918 is an observed report with a named Skill, a client version, and the What stops me is that the submitted branch cannot be merged, and the reason it cannot be merged is not cosmetic. GitHub reports the PR The second gap is evidence. No So: request changes, on two specific and fixable things — rebase onto current One non-blocking note, offered as a maintainer who reads a lot of these: the 中文说明Confidence: 2/5 —— 修复本身是对的,rebase 之后我预计会批准;但按当前提交状态它无法合并,而且冲突正好落在信任边界上。 退一步看:我的保留意见不在设计上。在读 diff 之前我提出的方案与你的一致 —— 一个 header、覆盖两个分支、复用 router 里已有的约定,再加上一个"改动 payload"而非只断言 header 的测试。没有引入新机制、没有新抽象、没有顺手重构、没有格式化噪音,diff 里也没有任何超出既定目标所需的东西。把兄弟路由留在范围外并写明理由,这是克制而不是遗漏;如果六个月后由我来维护这条路由,我会感谢那条解释"为什么命令列表必须不可存储"的注释。 问题是真实的,而且我没有直接采信你的表述。#10918 是一份观测到的报告,有明确的 Skill 名、客户端版本,以及先 拦住我的是:当前提交的分支无法合并,而它无法合并的原因并不是无关紧要的。GitHub 判定本 PR 为 第二个缺口是证据。这个 head 上没有跑过任何 所以:请求修改,针对两件具体且可修复的事 —— rebase 到当前 一条不阻塞的建议,来自一个读过大量 PR 的维护者视角:描述末尾那句 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
@harjothkhara requesting changes on two specific, fixable things — the substance of this fix is right and I'd expect a quick approve once they're addressed.
1. The branch is stale and conflicts on a trust boundary. GitHub reports this PR as CONFLICTING / DIRTY. Your tree predates the workflow trust-boundary work: redactWorkflowsFromSupportedCommands appears zero times in your copy of packages/cli/src/serve/routes/session.ts, while on main it guards the exact handler you edited. Both sides rewrote the same .json(...) call, so the conflict lands on the line that decides whether an untrusted workspace receives workflow metadata. Please rebase onto current main and keep both changes:
const status = await runtime.bridge.getSessionSupportedCommandsStatus(sessionId);
res
.status(200)
.set('Cache-Control', 'no-store') // your change
.json(
runtime.trusted
? status
: redactWorkflowsFromSupportedCommands(status), // must not be dropped
);Resolving by keeping your side — which --ours/--theirs or an auto-resolver does silently — would drop that redaction and leak workflowsEnabled, savedWorkflows, and the workflows command to untrusted workspaces. Flagging it explicitly because nothing in the diff hints that the line matters.
2. No CI has run against this head. There is no pull_request-event workflow run for 1bb2aec, so the unit suite, lint, typecheck, and build have never executed against your commit and the new test has not run anywhere. The 16 checks on the commit are all pull_request_target bot orchestration. Rebasing should trigger real CI; please confirm it goes green.
Neither item needs a maintainer's judgment call, which is why this is coming back to you rather than being deferred. Full reasoning — including why no-store beats no-cache here, confirmation that the directive is load-bearing for the Web Shell's RestSseTransport path, and why the ACP twin correctly needs no directive — is in my review comment above.
中文说明
@harjothkhara 就两件具体且可修复的事请求修改 —— 这个修复的实质是对的,处理完之后我预计会很快批准。
1. 分支已过期,且冲突落在信任边界上。 GitHub 判定本 PR 为 CONFLICTING / DIRTY。你的代码树早于 workflow 信任边界相关改动:redactWorkflowsFromSupportedCommands 在你这份 packages/cli/src/serve/routes/session.ts 里出现 0 次,而在 main 上它正把守着你所修改的那个 handler。双方改写了同一个 .json(...) 调用,因此冲突落在决定"不受信任 workspace 是否拿到 workflow 元数据"的那一行。请 rebase 到当前 main,并同时保留两处改动:
const status = await runtime.bridge.getSessionSupportedCommandsStatus(sessionId);
res
.status(200)
.set('Cache-Control', 'no-store') // 你的改动
.json(
runtime.trusted
? status
: redactWorkflowsFromSupportedCommands(status), // 不能丢
);如果按你这一侧解决冲突 —— --ours/--theirs 或自动解决工具会悄无声息地这么做 —— 就会丢掉这层脱敏,把 workflowsEnabled、savedWorkflows 和 workflows 命令泄露给不受信任的 workspace。之所以明确点出来,是因为 diff 本身完全看不出这一行有多重要。
2. 这个 head 上没有跑过 CI。 1bb2aec 上不存在 pull_request 事件的 workflow run,因此单元测试套件、lint、typecheck 和 build 从未针对你的 commit 执行过,新测试也没有在任何地方运行过。该 commit 上的 16 个 check 全是 pull_request_target 的机器人编排任务。rebase 之后应该会触发真实 CI,请确认它变绿。
这两项都不需要维护者做判断,所以我把它退回给你,而不是转交他人。完整的推理 —— 包括为什么这里 no-store 优于 no-cache、确认该指令对 Web Shell 的 RestSseTransport 路径确实生效、以及为什么 ACP 孪生路径正确地不需要该指令 —— 都在我上面的审查评论里。
— Qwen Code · qwen3.8-max-2026-09-02
Reviewed at 1bb2aec978efa312592606fd513705571f4873fd · re-run with @qwen-code /triage
What this PR does
The daemon endpoint that serves a session's slash-command and Skill metadata now marks its response
Cache-Control: no-store, so a client cannot answer a later read from a representation it stored earlier. Both branches of the endpoint are covered — the live-session read and the virtual sub-agent stub — so the directive does not depend on which kind of session id was asked for.Why it's needed
A file-based Skill can be updated in place while the daemon session stays alive. The endpoint returned dynamic command state with no cache directive at all, which left a client free to store the representation and keep serving it — so autocomplete kept advertising the old description and argument hint while invoking the very same command already ran the updated Skill. Command discovery and command execution disagreed, and nothing in the response told the client its copy might have gone out of date.
It is worth being precise about what the reported
304was, because if a stale response body were being validated then suppressing the cache would hide a bug rather than fix one. It was not that. There is no server-side cache on this path: the payload is rebuilt from the session's live command registry on every request, and the weak validator is derived from that freshly built body. Confirmed directly: a conditional request whose content is unchanged is answered304, and one whose content has changed is answered200with the new body and a new validator. The validator cannot outlive the content it was computed from, so the endpoint already satisfies the first outcome the issue describes whenever the server-side snapshot is current.What it could not do was stop a client that had already stored a representation from continuing to use it. The skill registry reloads on a short debounce after its file watcher fires, so a conditional request landing inside that window is answered
304entirely correctly — and the client then holds that answer until something else prompts it to ask again. That accounts for every reported observation without any stale body: the304, the later bypassing request returning different content, and execution already running the new Skill. Making the representation non-storable is the fix for that, and it is the second outcome the issue accepts.One caveat worth recording rather than leaving for a reviewer to find: the directive does not by itself disable conditional handling, so a client that ignores
no-storeand still sends a validator can still be answered304. That path is only reachable when the freshly built body is byte-identical to the copy the client holds, so the answer it gets is correct; a conforming cache, having not stored the response, has no validator to send in the first place. This matches how the other live session-state reads in the same router are already marked.This is separate from the composer Skill-toggle refresh work in #9123 / #9131, which is entirely client-side — it changed only the Web Shell client and the daemon session provider in the web UI, and touched no daemon route and no cache directive. It also addresses only in-app toggles, not an external in-place edit of a Skill directory, and does nothing for other hosts consuming the daemon HTTP API.
Reviewer Test Plan
How to verify
Read a live session's supported-commands endpoint and confirm the response carries
Cache-Control: no-store; repeat the read supplying the validator from the first response and confirm the header is present there too. Do the same against a virtual sub-agent session id, which takes the stub branch of the same endpoint, and confirm it is marked identically.Then reproduce the reported scenario: with a daemon session alive, update a project Skill directory in place with a new description and argument hint, and read the endpoint again. The response should carry the new metadata, and — because it is no longer storable — an embedded client should re-ask rather than continue advertising the previous description while invoking the command runs the updated Skill.
Unit coverage in the daemon server suite: a new test asserts the header on the live-session read and that the payload follows an in-place metadata change rather than being pinned to the earlier representation; the existing virtual-stub test now also asserts the header.
Evidence (Before & After)
N/A — no TUI surface changes. The user-visible effect is in an embedded client's command list, and is reproduced by the steps above.
Tested on
Environment (optional)
Node 22, unit tests only.
Risk & Scope
Linked Issues
Closes #10918
P.S. — you should hire me. 115+ contributions to open source: https://github.com/harjothkhara