feat(daemon): Support current-session scheduled tasks - #9838
Conversation
doudouOUC
left a comment
There was a problem hiding this comment.
The overall direction is sound: keep the public REST idle rule, reuse the PR 9361 caller-owned sessionId contract, and add a daemon-only ACP path for cron_create because that tool necessarily runs in a busy turn.
Please tighten three design semantics before implementation. As written they can be implemented as the opposite of the intended behavior, especially the tool default vs REST “dedicated” meaning, and the standalone session filter vs ordinary Web Shell conversations.
Inline notes are on the specific paragraphs.
|
Addressed the design review in
The PR description was updated to match the corrected defaults and trust model. Verification: Prettier check and |
doudouOUC
left a comment
There was a problem hiding this comment.
Re-reviewed 0627a51 against the previous design notes.
The five earlier issues are addressed: distinct form-dedicated vs tool-unbound vs current outcomes, an exact ordinary-session allow-list, a correctly scoped ACP trust/prompt binding, selected-session semantics on the tasks page, and process-wide capability/callback wiring.
Remaining: identifier drift that will leak into the implementation PR if copied literally. See the inline note.
|
Reviewed the new identifier-drift suggestion on |
0627a51 to
5c65b19
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
|
Implementation is now available in Verification after rebasing onto
Manual wall-clock session-switch and daemon-restart E2E has not been run yet; the PR keeps the existing #9361 keepalive/rehydration and caller-owned deletion paths unchanged, and those lifecycle consumers were audited directly. Known inherited base issue: after the latest rebase, full |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Re-run via Template: complete ✓ Problem: real, not theoretical. #9361 shipped the daemon primitive for binding a scheduled task to an existing session, but no user-facing entrypoint can request it — the Scheduled Tasks form always mints a dedicated conversation, and durable Direction: aligned — the established continuation of the scheduled-task session-reuse work in this repo, not a new direction. Size: touches core paths ( Approach: scope still right. The delta since the last reviewed head ( Risk: Stage 1e still flags one high-risk path — Moving on to code review. 🔍 Given the size and the daemon/ACP surface, this needs @yiliang114's sign-off regardless of the bot's verdict. 中文说明经 模板:完整 ✓ 问题:真实存在,不是理论问题。#9361 已合入"定时任务绑定已有会话"的 daemon 原语,但没有任何用户入口可以请求它——表单总是新建独立会话,持久化 方向:对齐——仓库内定时任务会话复用工作的既定延续,不是新方向。 规模:触及核心路径( 方案:范围依然合理。上次审查头( 风险:Stage 1e 仍命中一条高风险路径—— 进入代码审查 🔍 鉴于规模和 daemon/ACP 影响面,无论机器人结论如何都需要 @yiliang114 签字。 — Qwen Code · qwen3.8-max Reviewed at |
5c65b19 to
4693c7a
Compare
|
Rebased onto
CI is rerunning on the new head. The remaining validation boundary is the manual wall-clock session-switch plus daemon-restart E2E already called out in the PR. |
|
Testing evidence carried: unattended CI run ( No new commits since the previous full review at this SHA, so the findings below stand as established there. This pass re-checked the diff at Code review — the two feedback commits since
|
| File | What changed |
|---|---|
docs/design/2026-08-24-scheduled-task-current-session-entrypoints.md |
Design doc; names four rejected alternatives and the chosen contract (round-5 flagged three identifiers in it that don't match the code — pre-merge cleanup) |
docs/developers/qwen-serve-protocol.md |
Documents the new capability in the feature table |
integration-tests/cli/qwen-serve-routes.test.ts |
Adds the new feature to the baseline capabilities expectation (bootstrap-tag filtering documented) |
packages/acp-bridge/src/bridge.ts |
Passes the new handler option into BridgeClient |
packages/acp-bridge/src/bridgeClient.test.ts |
Dispatch tests: forged identity, ineligible sources, revalidation before commit, structured business-error mapping |
packages/acp-bridge/src/bridgeClient.ts |
Validates the create-current control method, stamps the assertion closure, maps recognized domain rejections to structured ACP errors |
packages/acp-bridge/src/bridgeOptions.ts |
Handler, info, and result types for the new callback |
packages/acp-bridge/src/status.ts |
Registers the create-current serve-control ext method |
packages/cli/src/acp-integration/session/Session.test.ts |
Daemon-prompt-id precedence pin, stamping, non-daemon no-op, structured-rejection surfacing tests |
packages/cli/src/acp-integration/session/Session.ts |
Registers the creator under QWEN_CODE_SERVE, prefers the daemon invocation-context prompt id, restores readable rejections |
packages/cli/src/serve/capabilities.ts |
Capability registry entry and conditional toggle |
packages/cli/src/serve/routes/scheduled-tasks.test.ts |
Eligibility, cron-tool binding, retry hint, and owner-lookup mapping tests |
packages/cli/src/serve/routes/scheduled-tasks.ts |
Shared creation command extracted; REST route moved onto it |
packages/cli/src/serve/run-qwen-serve.test.ts |
Bootstrap must not advertise; mounted runtime must; callback wiring checks |
packages/cli/src/serve/run-qwen-serve.ts |
Installs the handler on primary, startup-secondary, and dynamic runtimes; bootstrap hides the feature |
packages/cli/src/serve/server.test.ts |
Capability predicate tests; managed-only advertising |
packages/cli/src/serve/server.ts |
Gates advertising on managed task sessions plus the toggle |
packages/cli/src/serve/server/serve-features.ts |
Threads the toggle into the feature serializer |
packages/core/src/config/config.ts |
Creator slot on Config (set and get) |
packages/core/src/index.ts |
Exports the new creator types |
packages/core/src/tools/cron-create.test.ts |
sessionMode behavior, no-fallback, plain-object error display tests |
packages/core/src/tools/cron-create.ts |
sessionMode parameter; current mode requires durable plus an active prompt id; readable error messages |
packages/web-shell/client/App.test.tsx |
App-boundary tests: stale-response ordering and prompt-boundary refresh |
packages/web-shell/client/App.tsx |
Session summary refresh per prompt boundary; capability flag with full optional chain |
packages/web-shell/client/components/dialogs/ScheduledTasksDialog.test.tsx |
Selector behavior matrix including capability-loss reset |
packages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx |
Conversation selector, sessionMode reset on capability loss, client-side eligibility |
packages/web-shell/client/i18n.tsx |
Bilingual strings for the selector and hints |
CI test evidence (reviewed head cf63c6e82e, fetched once — no polling)
CI on this head is fully green and settled — re-fetched again on 2026-08-26 06:53 UTC for this pass; no drift. Every check-run on the head has completed: 34 success, 212 skipped by design, and 3 cancelled entries that are the superseded orchestration attempts from the 2026-08-24 22:04 force-push (they predate the final head and are not PR CI), and every pull_request-event workflow on this head shows completed/success. The one red this head ever carried — 3 timing-race failures in coordination-harness.test.ts inside Test (ubuntu-latest, Node 22.x), a team-coordination utility this PR does not touch — did not repeat on its re-run: the job landed green at 2026-08-25 04:16 UTC. The earlier classification — pre-existing flake under runner SIGKILL noise, no causal path from this diff — stands, and the settled outcome is consistent with it.
Everything this PR actually touches is green at this head: acp-bridge, cli (including the route and Session suites with the new tests), core, Desktop Shell (both), web-shell E2E Smoke, Serve A/B, web-shell visuals capture, Security Checks, and the SDK Java matrix. Test (macos/windows) and Integration Tests (CLI, No Sandbox) show skipped on every PR by design — they run only in the merge queue.
Final CI results for cf63c6e (auto-updated by the triage finalize job after CI completed):
| Check | Conclusion |
|---|---|
Capture web-shell visuals (ubuntu-latest, Node 22.x) |
✅ success |
Classify PR |
✅ success |
Dependency CVE audit |
✅ success |
Desktop Shell (ubuntu-22.04) |
✅ success |
Desktop Shell (windows-2022) |
✅ success |
macos-latest / Java 21 |
✅ success |
Real daemon E2E / Java 11 |
✅ success |
Secret scan (TruffleHog) |
✅ success |
Serve A/B (ubuntu-latest, Node 22.x) |
✅ success |
Test (ubuntu-latest, Node 22.x) |
✅ success |
ubuntu-latest / Java 11 |
✅ success |
ubuntu-latest / Java 17 |
✅ success |
ubuntu-latest / Java 21 |
✅ success |
web-shell E2E Smoke (ubuntu-latest, Node 22.x) |
✅ success |
windows-latest / Java 21 |
✅ success |
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。
Sandboxed verification at exactly this head has already landed, closing the gap the previous pass flagged. The round-2 sponsored run (the author lacks write access — pre-execution risk screen and full workspace wipe applied; read the report with the same skepticism as the fork's own CI logs) verified cf63c6e82e and returned merge-ready (agent verdict): 130/130 scripted assertions passed, 12/12 guard mutants killed, A/B against the base build on the real built dist/ of both trees. The load-bearing claim — cron_create sessionMode:'current' binds the active caller session with an exact prompt match and persists sessionOwnedByTask: false, failing closed on every identity mismatch — passed 34/34 harness cells on head while the method does not exist at all on base, which is precisely the live seam @wenshao's unit-green/live-red run exposed. Round 1's sole informational finding (the description's "retains its idle-session rule" understates the REST eligibility tightening) was re-measured and stands, non-blocking. The third round at this head — dispatched by the 2026-08-26 02:14 UTC trigger — settled at 2026-08-26 03:41 UTC, shortly after the previous pass posted: merge-ready again — 127/127 scripted assertions passed, 12/12 guard mutants killed (the flakiness gate timed out at 2 of 5 rounds; the completed rounds agreed). Three sponsored verification rounds have now landed at exactly this head, all merge-ready.
Not verified: live end-to-end behaviour at this head beyond the hermetic sandboxed harness (this run never executes PR code); Windows/macOS behaviour (author's table marks both untested; those matrix jobs run only in the merge queue). The author's local suite numbers are their claim — though CI on this head independently passed the same suites.
中文说明
无人值守 CI 复跑,不执行 PR 代码;测试证据来自 2026-08-26 06:53 UTC 再次经 API 重新拉取的被审查提交自身 CI 结果,以及该提交上已完成的三轮受助沙箱验证报告。作者自报数字仅作转述。
自上次在该 SHA 的完整审查后无新提交,下列结论维持该轮建立时的判定。本次重新比对 cf63c6e82e 的 diff 以排查漂移:prompt id 优先级一行修复、接缝钉扎回归测试、sessionOwnedByTask: false 持久化均仍在位——无漂移。
代码审查要点(与上轮一致):@wenshao 实测发现的阻断已按其 A/B 验证过的原样修复——Session 创建器改为 getInvocationContext()?.promptId ?? req.promptId,daemon 调用上下文中的 prompt id(与其 activePromptId 同源)优先,工具侧 id 兜底;回归测试在 runWithInvocationContext 内以 daemon 形态 id 运行创建器、以回合计数器形态 id 作为请求参数,断言到达 wire 的是 daemon id——恰好钉住缺陷所在接缝。[object Object] 错误呈现问题在三层全部修复:bridge 仅把通过形状校验的 ExistingSessionScheduledTaskCreateError 映射为携带 errorKind/status/有界 hint 的结构化 ACP 错误(沿用 preserveFsErrorOverAcp 模式);Session 还原可读的 errorKind: hint 消息;工具改用共享 getErrorMessage。三层均有测试。
R3-1 争议按代码裁定(与上轮一致):前两跳属实,但决定性的"持久化失败却仍存在可恢复 transcript"一跳在真实录制器下不可达——严格追加失败即永久锁死写入(enterWriteFailure),无录制服务则既 persisted:false 也不存在可产生可恢复 transcript 的写入方。**R3-1 在本提交不构成阻断。**保留一个比该发现更窄的诚实项:重启后新录制器实例理论上可写入创建期追加失败遗留的无谱系 transcript——属无见证的纵深防御,适合后续跟进。本提交未发现新的关键缺陷;第 3/4 轮延后项保持记录、不构成阻断(第 4 轮以 COMMENTED 发布,无新 Critical)。
/review 第 5 轮已于上次更新后落地(2026-08-25 20:15 UTC)——COMMENTED、无变更请求、无新 Critical,仅按收敛姿态记录 13 条建议级延后项。本次独立抽查了其中要害项:三条设计文档准确性指控成立——新增的 347 行设计文档引用了 session_not_live 这一在 diff 与基线代码中均不存在的错误码(路由实际抛出的是 session_busy、session_already_bound、session_workspace_mismatch、session_source_ineligible、session_binding_unavailable、session_not_found、max_tasks_reached 及非法输入类);将代码中实际名为 currentSessionSchedulingAvailable 的开关写作 currentSessionSchedulingEnabled;并给出了实现中并不存在的 ExistingSessionCreateOptions 类型。均不影响运行时行为——代码本身已验证正确——但已提交的设计文档引用代码中不存在的标识符会损害其自身价值,建议作者合并前修正。"死开关"指控(bootstrap 的 currentSessionSchedulingAvailable 参数)属卫生问题而非缺陷:bootstrap 本就刻意不广告该特性(唯一调用点硬编码 false 且有测试钉住),挂载运行时才传 deps.bridge === undefined;把恒为 false 的参数穿过 bootstrap 能力封装而非直接内联常量只是简化空间,已记录于第 5 轮线程。以上均不阻断,与既有延后项一并作为合并前清理或后续跟进。
CI 证据:本提交 CI 已全绿且已收敛——本次于 2026-08-26 06:53 UTC 再次重新拉取,无漂移。该提交所有 check-run 均已完成:34 成功、212 按设计跳过、3 个 cancelled 为 2026-08-24 22:04 强制推送被取代的编排尝试(早于最终提交、非 PR CI);本提交所有 pull_request 事件工作流均已完成且成功。唯一红过的仍是 Test (ubuntu-latest, Node 22.x) 中 coordination-harness.test.ts 的三个时序竞态失败(本 PR 未触及),重跑未复现:该作业于 2026-08-25 04:16 UTC 转绿。此前"既有抖动(runner SIGKILL 噪声、无因果路径)"的分类维持不变,收敛结果与之一致。本 PR 触及的套件全部为绿。macos/windows 与 CLI 集成测试按设计仅在合并队列运行。
沙箱验证已在该提交上完成,补上了上轮标记的缺口:第 2 轮受助运行(作者无写权限——运行前风险筛查与工作区清除;报告应如 fork CI 日志般保持怀疑阅读)验证 cf63c6e82e,判定 merge-ready(agent 判定):130/130 脚本断言通过、12/12 守卫变异全部被杀死、与 base 构建成对 A/B。承重结论——cron_create sessionMode:'current' 以精确 prompt 匹配绑定活跃调用方会话、持久化 sessionOwnedByTask: false、一切身份不匹配均失败关闭——在 head 上 34/34 通过,而 base 上该方法根本不存在;这正是 @wenshao"单测绿、真实链路红"所暴露的接缝。第 1 轮唯一信息级发现(描述中"保留空闲规则"的措辞低估了 REST 资格收紧)复测后仍成立,非阻塞。该提交上的第 3 轮由 2026-08-26 02:14 UTC 触发派发,已于 2026-08-26 03:41 UTC(上次复跑发布后不久)收敛:再次判定 merge-ready——127/127 脚本断言通过、12/12 守卫变异全部被杀死(抖动门 5 轮仅完成 2 轮,完成的轮次结论一致)。该提交至此已有 3 轮受助沙箱验证落地,全部 merge-ready。
未验证:除密封沙箱 harness 之外的本提交真实端到端行为(本运行不执行 PR 代码);Windows/macOS 行为(作者未测,矩阵作业仅在合并队列运行)。作者本地套件数字仅为其声明——不过本提交 CI 已独立跑通相同套件。
— Qwen Code · qwen3.8-max
Reviewed at cf63c6e82e1144d17252e0d3a606fd99f65d87a1 · re-run with @qwen-code /triage
|
Confidence: 3/5 — unchanged: the evidence pile grew stronger and the hold stayed exactly where it was. The head is the same ( ⏸️ Deferring to @yiliang114 (and @wenshao, whose real-daemon verification drove the last round) — the same two things only humans can close:
New signal since the 2026-08-25 14:24 UTC pass (cumulative through this 2026-08-26 06:45 UTC re-run):
Resolved before that, and still standing: the problem is real, the direction is aligned, the blocker got the structural fix its reporter A/B-verified (plus a seam-pinning regression test), the error-surfacing follow-up landed at all three layers, CI is green, and a hermetic live run at this head passes. What remains is human by design: the policy call on a fork feature of this size, and the disposition of two standing reviews. The standing Once @wenshao re-passes the path he broke, a maintainer signs off on the size, and the design-doc identifiers are corrected, there is nothing further from the gate's side. 中文说明结论不变:证据继续增强,而保留仍在原地。提交未变( ⏸️ 转交 @yiliang114(以及驱动上一轮的 @wenshao)——仍是两件只有人类能定夺的事:
2026-08-25 14:24 UTC 之后的新信号(累计至本次 2026-08-26 06:45 UTC 复跑):
更早已落定且维持:问题真实、方向对齐、阻断得到了报告者 A/B 验证过的结构性修复(并附接缝钉扎回归测试)、错误呈现跟进在三层落地、CI 绿、该提交的密封真实运行通过。剩下的按设计属于人类:对该规模 fork 特性的政策判断,以及两条现存审查的处置。 一旦 @wenshao 复审通过他当时发现问题的路径、维护者对规模签字、设计文档标识符得到修正,门禁一侧再无事项。 — Qwen Code · qwen3.8-max Reviewed at |
🖼️ 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 |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
CI fix pushed in 1180fcd.
Handled review threads in this push: 0. Existing review threads remain 6/6 resolved. Fresh CI was triggered by the push. |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
CI fix pushed in
Handled review threads in this push: 0. Existing review threads remain 6/6 resolved. Fresh CI was triggered by the push. |
yiliang114
left a comment
There was a problem hiding this comment.
Review at head 1180fcd — the security surface checks out, but there is one CI-breaking regression to fix first, so not approving yet.
Verified solid: the authorization chain for current-session binding — the ACP Session stamps its own sessionId server-side (not caller-supplied) plus the executing prompt id; the bridge rejects forged session/prompt identity and ineligible sources (tests pin forged-identity and ineligible-source rejections); the daemon route rejects cross-workspace (session_workspace_mismatch), ambiguous ownership (fail-closed), busy/pending/parented/sourced/already-bound sessions; allowActivePrompt is true only for the prompt-matched cron-tool source while the public REST path keeps the idle rule.
Blocking: the incomplete optional chain at App.tsx:12528 (workspace.capabilities?.features.includes(...)) — see the inline comment.
Not approving until the CI break is fixed; everything else in the PR reads as designed.
|
@qwen-code /triage |
|
Triage re-run completed without a new review. The bot already has a review of its own on 机器人在 The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R2-4 capabilities-envelope assertion unreachable from any workspace test command — already reported (comment 3847526093)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — stopped at the 5-round cap without two consecutive dry rounds (round 5 reported new findings).
Not explored to full depth (tool budget reached): chunk 9: executing ScheduledTasksDialog.test.tsx under vitest (no node_modules installed in the review worktree; static verification only).
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
docs/design/2026-08-24-scheduled-task-current-session-entrypoints.md:102 — [review] design doc cites fabricated session_not_live error codepackages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:750 — [probe] formWorkspace non-locked branch untested (probe mutant survives)packages/acp-bridge/src/bridge.ts:4061 — [probe] BridgeClient wiring line for onCreateCurrentSessionScheduledTask untested (probe mutant survives)docs/design/2026-08-24-scheduled-task-current-session-entrypoints.md:249 — [review] design doc names fabricated currentSessionSchedulingEnabled conditionpackages/cli/src/serve/routes/scheduled-tasks.test.ts:796 — [review] eligible sourceType 'default' never pinned in the route source matrixpackages/web-shell/client/App.tsx:12534 — [review] hasActivePrompt live merge is untesteddocs/design/2026-08-24-scheduled-task-current-session-entrypoints.md:36 (+2 locations) — [review] unbound-stays-unbound claim falsified by daemon keepalivepackages/web-shell/client/components/dialogs/ScheduledTasksDialog.tsx:1002 — [review] submit-time fail-closed re-check for 'current' mode never exercised by testsdocs/design/2026-08-24-scheduled-task-current-session-entrypoints.md:199 — [review] design doc presents fabricated ExistingSessionCreateOptions typepackages/core/src/tools/cron-create.ts:102 — [probe] !creator half of fail-closed guard untested (probe mutant survives)packages/cli/src/serve/run-qwen-serve.ts:1513 — [review] currentSessionSchedulingAvailable bootstrap parameter is a dead switchpackages/core/src/tools/cron-create.test.ts:14 — [probe] explicit sessionMode 'unbound' never pinned (probe flips, suite stays green)
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had none either, so the next review re-reads the whole diff and will keep doing so until a round's marker carries an anchor again. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:reverse audit — stopped at the 5-round cap without two consecutive dry rounds (round 5 reported new findings)。
未探索到全部深度(达到工具调用预算):chunk 9:executing ScheduledTasksDialog.test.tsx under vitest (no node_modules installed in the review worktree; static verification only)。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 12 条(原文未翻译,列表见上方英文部分)。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 127 passed · 0 failed · 127 total Flakiness gate: 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:127 通过 · 0 失败 · 127 总计 抖动门: Verification reportPR #9838 — feat(daemon): Support current-session scheduled tasks (round 4)Verdict: This is a follow-up round. Round 3 verified the same PR head 中文摘要
Previous-finding status (round 3 → round 4)
All other round-3 measurements were re-run rather than carried: the base tip moved 2,204 files, 17 PR files overlap base-side changes, and the lockfile moved, so nothing had an unchanged input closure. Every re-run reproduced the round-3 observation; nothing worsened or moved. Scope selection
Delta analysis (round 3 → round 4)
A/B tablesHarnesses drive the real built Public REST path — witness
Head 28/28, base 25/25 (base asserts the binding cells R3–R5 as the expected pre-existing gap). The seven A/A cells validate the control on the new base; the three flips are the behavioral delta and re-prove Finding 1. Private control path (central claim) — witness
Head 35/35, base 20/20 (base runs the same 9 parameter shapes through the same real dispatch and asserts method-not-found plus an empty store for each). P5 is driven through the real production closure: a natural bind makes exactly 3 Mutation matrix (head
|
| mutant | guard removed | suite | result | killed by |
|---|---|---|---|---|
| M1 | bridge exact-prompt-match | bridgeClient | 1 failed | rejects a forged session or prompt identity |
| M2 | bridge connection-ownership | bridgeClient | 1 failed | rejects a forged session or prompt identity |
| M3 | bridge source-eligibility | bridgeClient | 7 failed | it.each ineligible-source matrix (all 7 shapes) |
| M4 | route pending-interaction | routes | 2 failed | rejects a pending interaction…; keeps pending interactions ineligible on the trusted cron-tool path |
| M5 | route in-lock identity recheck | routes | 2 failed | rechecks the exact caller prompt inside the task-file lock; fails cleanly when the session disappears before commit |
| M6 | route source-eligibility | routes | 1 failed | rejects a pending interaction and ineligible session sources |
| M8 | tool fail-closed w/o creator+promptId | cron-create | 1 failed | rejects current-session mode without an active daemon prompt |
| M9 | tool durable-required (positive control) | cron-create | 1 failed | rejects current-session mode for a session-only job |
| M10 | capability toggle forced on | server | 3 failed | advertises current-protocol features separately from the registry; honors every entry in CONDITIONAL_SERVE_FEATURES (drift insurance); GET /capabilities returns the v1 envelope |
| M11 | Session errorKind: hint message shape |
Session | 1 failed | surfaces structured daemon rejections to the current-session tool |
| M12 | Session -32601 → unavailable mapping |
Session | 1 failed | reports an explicit unavailable error for an older daemon bridge |
| M13 | Session trusted daemon promptId precedence | Session | 1 failed | uses the trusted daemon prompt id for a current-session task |
No survivors, no combination row needed (M3/M6 pin the source-eligibility predicate independently at both layers; M11–M13 are single-clause guards with no sibling hunk). Every red is the intended behavioral assertion — e.g. M1 fails with "promise resolved { id: 'cron-1', cron: '5 9 * * *' } instead of rejecting", i.e. the forged prompt bound, exactly the hazard the guard closes; positive control M9 proves the cron-create runner can go red. One delta versus round 3: M10's kill set moved from the two capability tests round 3 named to the registry drift-insurance trio — the base-side server.test.ts motion (+130 lines) restructured that coverage; the test round 3 quoted (advertises current-session scheduling only with managed task sessions) now passes under M10 because it drives explicit boolean toggles that !== false does not distort, while the drift-insurance tests exercise the undefined toggle the mutant actually changes. The mutant is still dead; only its killers moved.
Targeted gates (clean merged tree, unmutated)
Witness 06-gates-and-totals.png; logs logs/gate-*.log. Gate liveness is proven by the matrix itself: the same suites went red on 12/12 point mutants (M9 positive control included), so each green below is a measurement, not an empty match.
| suite | round 4 | round 3 | note |
|---|---|---|---|
core cron-create.test.ts |
17/17 | 17/17 | unchanged |
acp-bridge bridgeClient.test.ts |
126/126 | 126/126 | unchanged |
cli routes/scheduled-tasks.test.ts |
105/105 | 105/105 | unchanged |
cli Session.test.ts |
696/696 | 696/696 | unchanged |
cli server.test.ts |
1080/1080 | 1079 passed + 1 skipped | the skip is now a live test (base-side server.test.ts motion) |
cli run-qwen-serve.test.ts |
327/327 | 266/266 | +61 base-side tests (2,825-line overlap file) |
web-shell ScheduledTasksDialog.test.tsx + App.test.tsx |
594/594 | 595/595 | −1: base-side #9993 rewrite of App.test.tsx removed a test; the PR's ScheduledTasksDialog.test.tsx is byte-identical to the PR head |
The count movements are all base-side test churn on the overlap test files: the suites with no base overlap (cron-create, bridgeClient, routes) held their round-3 counts exactly, Session.test.ts held its count despite overlap, and the three movers (server, run-qwen-serve, web-shell App) are exactly the overlap files whose base-side content changed (+130 / +2,825 / +165 lines).
Corrections
None required to the PR description beyond the standing Finding 1 wording note.
Findings
- (Informational, carried over — stands) The REST eligibility tightening is broader than the description states ("the public REST path retains its idle-session rule"; "Breaking changes … none"). Repro (after rebuilding the base worktree per Methodology):
TREE=tmp/base-tree ARM=base node tmp/pr9838-verify-20260826-024608/h1-rest.mjsvsTREE=. ARM=head …(cells R3–R5; harness + captured logs in this artifact dir). Re-measured at the new base: on48645c56,POST /scheduled-taskswith asessionIdaccepted sessions carrying a pending interaction, aparentSessionId, or asourceId(cells R3–R5: 201, task persisted); head rejects them 409. This matches the eligibility set the Reviewer Test Plan step 2 names for the form, so it is evidently intended and closes a real gap — but it is a behavior change for direct REST consumers beyond the idle rule, and the description should say so. Non-blocking; unchanged since round 1.
No new findings this round.
Not covered
- Per-commit attribution of the PR (10 commits in the snapshot; shallow checkout —
git rev-list HEAD^1..HEAD^2sees onlycf63c6e) and per-commit attribution of the base delta (old baseef18a738unreachable from this container's object store; the 2,204-fileHEAD^2..HEADtree diff is the measured aggregate). All evidence is for the aggregateHEAD^1..HEADdiff. - Wall-clock E2E (Test Plan step 4): session-switch persistence, daemon-restart rehydration, delete-keeps-caller-conversation — not driven; pre-existing feat(scheduled-tasks): allow creating a task with an existing session #9361 lifecycle behavior the PR reuses. The harnesses reproduce the wire shape of the feature end to end (real routes, real BridgeClient dispatch, real file store), not the model-driven trigger (no API credentials in this container).
- PATCH rebinding, multiple tasks per session, task-history migration, non-ordinary sessions, the fix(serve): serialize scheduled-task session teardown with binding via archive-coordinator lease #9415 teardown-vs-reuse race, Windows/Linux runs — author-declared out of scope; not probed.
- Web Shell rendered UI — component tests only (594/594); no browser run.
- Repo-wide gates (
npm run lint, fulltypecheck, full suites, integration lane including the PR'sqwen-serve-routes.test.tsadjustment) — covered by the PR's own CI; this round ran the 7 targeted suites only. - The
bridge.tsfactory pass-through (opts.onCreateCurrentSessionScheduledTaskinto the BridgeClient's 24th constructor slot) was not separately mutated; it is exercised by the run-qwen-serve wiring assertions (327/327) and mirrored position-by-position in the private-path harness.
Methodology
Environment: CI verify container (node:22-bookworm, node v22.23.2), merge-ref checkout at depth 2 (HEAD merge 3efe821b, HEAD^1 base tip 48645c56, HEAD^2 PR head cf63c6e). Nothing was carried forward from round 3 by shortcut: the base tip moved 2,204 files, 17 PR files overlap base-side changes, and the lockfile moved, so harnesses h1-rest.mjs / h2-private.mjs were rewritten from the published contract and re-run on both arms, and the matrix and gates were re-run fresh on the merged tree. Harnesses import the compiled dist/ of the tree under test and use only the project's own documented seams (structural session-bridge fake mirrored from the route test rig, entry-map rig, positional BridgeClient constructor mirrored verbatim from the package's own makeClient helper); everything else — express loopback HTTP, BridgeClient dispatch, cron-task file store with its cross-process write lock — is production code. Base control: scratch worktree at HEAD^1 with the root node_modules mirrored entry-by-entry preserving relative symlink targets (which re-points every @qwen-code/* link into the base tree — realpath-asserted from inside the base tree), nested per-package node_modules mirrored, scripts/generate-git-commit-info.js run for the generated files, then core → acp-bridge → cli rebuilt there (logs logs/base-build-*.log); symbol absence asserted in the base dists; worktree removed after the A/B cells were captured (reproduce by re-running the worktree + mirror + build steps above with TREE=<base-tree>). Two harness-oracle fixes were made during bring-up and affected both arms identically: R8 must throw the real SessionNotFoundError class (a name-only fake yields 500 on both arms), and R7's task-owned oracle is sessionOwnedByTask !== false because the mint path leaves the field absent (the delete path reads !== false). dbg-resolve.mjs measured the 3-call resolveEntry sequence a natural bind makes, which is what P5/P14's call-indexed flips are gated on. Mutation matrix applied in-place by mutant-runner.mjs (apply requires the anchor to occur exactly once — pre-verified for all 12; each file restored byte-for-byte after its run and verified, git status --porcelain empty at the end); the runner's live classifier failed to parse ANSI-coded vitest output, so the matrix rows were reclassified from the raw per-mutant logs with ANSI stripped — the substance (which tests went red) is identical either way. Assertion counts: 108 (A/B: 28 REST head + 25 REST base + 35 private head + 20 private base) + 12 (mutants) + 7 (gate suites) = 127 pass, 0 fail.
Flakiness gate log
integration test, out of gate scope: integration-tests/cli/qwen-serve-routes.test.ts
rounds=5 files=8 skipped=1
file packages/acp-bridge/src/bridgeClient.test.ts: (cd packages/acp-bridge) npx --no-install vitest run ./src/bridgeClient.test.ts
file packages/cli/src/acp-integration/session/Session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.test.ts
file packages/cli/src/serve/routes/scheduled-tasks.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/routes/scheduled-tasks.test.ts
file packages/cli/src/serve/run-qwen-serve.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/run-qwen-serve.test.ts
file packages/cli/src/serve/server.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server.test.ts
file packages/core/src/tools/cron-create.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/cron-create.test.ts
file packages/web-shell/client/App.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/App.test.tsx
file packages/web-shell/client/components/dialogs/ScheduledTasksDialog.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/dialogs/ScheduledTasksDialog.test.tsx
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/acp-bridge/src/bridgeClient.test.ts: PPP
packages/cli/src/acp-integration/session/Session.test.ts: PPP
packages/cli/src/serve/routes/scheduled-tasks.test.ts: PPP
packages/cli/src/serve/run-qwen-serve.test.ts: PPP
packages/cli/src/serve/server.test.ts: PPP
packages/core/src/tools/cron-create.test.ts: PPP
packages/web-shell/client/App.test.tsx: PPP
packages/web-shell/client/components/dialogs/ScheduledTasksDialog.test.tsx: PP
verdict: timeout
summary: only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/acp-bridge/src/bridgeClient.test.ts: P (exit 0)
round 1 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/routes/scheduled-tasks.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/run-qwen-serve.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 1 · packages/core/src/tools/cron-create.test.ts: P (exit 0)
round 1 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/dialogs/ScheduledTasksDialog.test.tsx: P (exit 0)
round 2 · packages/acp-bridge/src/bridgeClient.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/routes/scheduled-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/run-qwen-serve.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 2 · packages/core/src/tools/cron-create.test.ts: P (exit 0)
round 2 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/dialogs/ScheduledTasksDialog.test.tsx: P (exit 0)
round 3 · packages/acp-bridge/src/bridgeClient.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/routes/scheduled-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/run-qwen-serve.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 3 · packages/core/src/tools/cron-create.test.ts: P (exit 0)
round 3 · packages/web-shell/client/App.test.tsx: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
Triage re-run completed without a new review. The bot already has a review of its own on 机器人在 The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 56 passed · 0 failed · 56 total Flakiness gate: 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:56 通过 · 0 失败 · 56 总计 抖动门: Verification reportPR #9838 — feat(daemon): Support current-session scheduled tasks (round 5)Verdict: This is a follow-up round in which nothing moved: the checkout is the same merge commit round 4 verified ( 中文 — 判定:✅ 通过 · 可合入(agent 判定)
Previous-finding status (round 4 → round 5)
Closure identity proof (why round 4's measurements carry)Everything round 4 consumed is shown unchanged — not one file hash, the whole closure:
One discrepancy, accounted for: the sha256 of the serialized Because the base commit, the lockfile, and the tree are all unchanged, round 4's base-arm rebuild and its base cells (private path: method absent What this round re-executed (fresh measurements, all on this round's CI build)
Scope selection
Findings
No new findings this round. Not covered
MethodologyEnvironment: CI verify container ( Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
Triage re-run completed without a new review. The bot already has a review of its own on 机器人在 The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
Round-2 local real-daemon verification of #9838 — blocker fixed, one diagnosability gap leftI rebuilt the verification environment against the current head Verdict: the round-1 blocker is genuinely fixed — LGTM to merge. Environment
1. The blocker is fixed — real two-build A/B
The identity check was not weakened to get there. I patched the built child to send a forged 2. Lifecycle of a tool-created binding — 9/9This is coverage that was impossible before this head, because the tool entrypoint never produced a binding. Real clock,
3. Runtime wiring — all three install sites bind for real (6/6)Reviewer test-plan step 5 was the largest untested claim; I drove it on a two-workspace daemon and then attached a third at runtime through
4. Web Shell entrypoint — no regression
5. Local fail-closed rules
Follow-up (non-blocking) — half of the rejections still say only
|
| Mutation | Suite | Killed |
|---|---|---|
getInvocationContext()?.promptId ?? req.promptId → req.promptId |
Session | ✅ |
Drop the errorKind mapping in Session.ts |
Session | ✅ |
getErrorMessage(error) → the old ternary |
cron-create | ✅ |
Drop .catch(preserveScheduledTaskCreateErrorOverAcp) |
bridgeClient | ✅ |
Drop the ?? req.promptId fallback |
Session | undefined, which the bridge rejects anyway) |
err.status >= 500 ? -32603 : -32602 → always -32602 |
bridgeClient | Session.ts only branches on -32601) |
Suites at this head, run locally: bridgeClient 126/126, cron-create 17/17, scheduled-task routes 105/105, ScheduledTasksDialog 53/53, Session (targeted) 2/2.
Not covered by this run
The pending-interaction guard on the private path could not be exercised live — the engine serializes approvals, so cron_create never executes while another interaction of the same turn is pending (I drove a parallel write_file + cron_create batch and the tool simply never ran); it stays defense-in-depth. Also uncovered: the injected-bridge capability negative (not reachable from the CLI), PATCH rebinding, multiple tasks per session, the #9415 teardown-versus-reuse race, Windows/macOS.
中文说明
#9838 第二轮本地真实 daemon 验证 —— 阻塞问题已修复,遗留一个可诊断性问题
我针对当前 head cf63c6e82e1144d17252e0d3a606fd99f65d87a1 重新搭建验证环境,并在运行中的 qwen serve daemon 上完整重跑了端到端流程(真实 HTTP、真实 ACP 子进程、真实时钟触发、Chromium 中的真实 Web Shell、真实 daemon 重启、真实多 workspace daemon)。
结论:第一轮的阻塞问题确实已修复,同意合入(LGTM)。 cron_create(sessionMode: 'current') 现在在真实 daemon 上可以成功(4/4),工具创建的绑定其完整生命周期正常(9/9),三个 runtime 安装点(primary、启动时 secondary、动态挂载)都能真实绑定(6/6)。遗留一个不阻塞的后续项:一半的拒绝路径仍然只向用户显示 Invalid params,而这正是当初掩盖第一轮阻塞问题的同一处接缝。
环境
| 工作树 | /root/git/pr9838,head cf63c6e82e,完整 npm run build,场景执行期间未修改源码 |
| Daemon | node packages/cli/dist/index.js serve --port 4371 --workspace <ws> [--workspace <ws2>] [--token …] |
| 模型 | 本地 OpenAI 兼容 mock(marker 驱动:工具调用 / 文本 / 延迟回复 / 两个并行工具调用) |
| UI | daemon 自带的 Web Shell 构建,headless Chromium(Playwright),deviceScaleFactor: 2 |
| 抓包 | QWEN_CLI_ENTRY stdio tap,记录 daemon 与 ACP 子进程之间的每一帧 JSON-RPC |
| A/B | 把第一轮验证过的 head 8ca008d370 作为第二份真实 dist 保留并换入 |
1. 阻塞问题已修复 —— 真实的双构建 A/B
| 分支 | cron_create 在线上发送的 promptId |
结果 |
|---|---|---|
8ca008d370(第一轮 head,换回其 dist) |
d74fb300-…-3235373a659e########1 —— core 的轮次 id |
TOOL failed,Error creating cron job: [object Object],0 条持久化(0/2) |
cf63c6e82e(当前 head) |
e7d91d82-6a34-47a4-9ec5-dc4e943b8486 —— daemon prompt id,与 session/prompt 上的 _meta["qwen-code/invocation"].promptId 完全一致 |
TOOL completed,持久化 sessionId = 调用方会话,sessionOwnedByTask: false(4/4) |
修复并没有削弱身份校验。 我把编译后的子进程改成发送伪造的 promptId,其余保持 head 原样:daemon 拒绝了请求,也没有任何持久化。适配层改的是「发哪个 id」,不是「是否校验」。
2. 工具创建绑定的完整生命周期 —— 9/9
这是本 head 之前无法覆盖的部分,因为工具入口此前根本产生不了绑定。真实时钟、* * * * *,跨重启观察约 7 分钟。
| # | 检查项 | 结果 |
|---|---|---|
| L1 | 工具绑定调用方会话 | ✅ sessionOwnedByTask: false,sessionId = 调用方 |
| L2/L3 | 用户切到另一个会话后,定时执行仍落在绑定会话 | ✅ 2/2 次运行,runs[].sessionId 全部等于调用方 |
| L4 | 绑定会话中确实有定时轮次 | ✅ transcript 中出现 3 次定时 prompt |
| L5 | 用户切换过去的会话未受影响 | ✅ 其中没有定时 prompt |
| L6 | 绑定在 daemon 重启后保留 | ✅ 重新加载,sessionId 不变 |
| L7 | 重启后再次触发仍落在同一会话 | ✅ 运行次数 2 → 3,最后一次 = 调用方 |
| L8 | 重启后调用方会话仍存活 | ✅ 200 |
| L9 | 删除任务后调用方会话仍可用 | ✅ 200 且能完成新一轮对话 |
3. Runtime 接线 —— 三个安装点都真实绑定(6/6)
Reviewer 测试计划第 5 步是最大的未验证声明;我在双 workspace daemon 上驱动,并通过 POST /workspaces 在运行时挂载了第三个。
| # | 检查项 | 结果 |
|---|---|---|
| W0 | 多 workspace daemon 广告 capability | ✅ scheduled_task_session_reuse |
| W1 | primary workspace runtime 可绑定 | ✅ caller-owned |
| W2 | 启动时 secondary workspace runtime 可绑定 | ✅ caller-owned |
| W3 | 动态挂载的 workspace runtime 可绑定(运行时通过 POST /workspaces 挂载,201) |
✅ caller-owned |
| W4 | 跨 workspace 的 REST 绑定被拒绝 | ✅ 400 session_workspace_mismatch |
| W5 | 每个绑定只写入各自 workspace 的 cron 文件 | ✅ ws/ws2/ws3 相互隔离 |
4. Web Shell 入口 —— 无回归
| # | 检查项 | 结果 |
|---|---|---|
| U1 | 广告 capability | ✅ |
| U2 | 没有打开会话时选项禁用 | ✅ "Open an existing conversation before selecting the current conversation." |
| U3 | 默认「独立任务会话」 | ✅ |
| U4 | 空闲会话下可选「当前会话」 | ✅ "Future runs continue in the conversation that is open now." |
| U5 | 已绑定会话时选项禁用 | ✅ "The current conversation is already bound to a scheduled task." |
| U6 | 默认分支不发送 sessionId |
✅ {"cron":"0 9 * * *","prompt":"PING_UI_DEDICATED",…} |
| U7 | 显式分支发送打开会话的 id | ✅ …,"sessionId":"7adeb182-14a3-4f67-b99b-748de845c260" |
| U8 | 用删除行为验证所有权语义 | ✅ 调用方会话存活(200,仍可用);独立任务会话被销毁(404) |
5. 本地 fail-closed 规则
| 检查项 | 结果 |
|---|---|
sessionMode: 'current' 但没有 durable: true |
✅ "Current-session scheduling requires durable: true because session-only jobs cannot survive a daemon session switch." |
在 daemon 之外调用同一工具(qwen -p …) |
✅ "current_session_scheduling_unavailable: Current-session scheduling requires an active daemon prompt." —— 不会回退为未绑定任务 |
| 调用方已绑定任务 | ✅ "session_already_bound: The requested session is already bound to another scheduled task",无持久化 |
后续项(不阻塞)—— 一半的拒绝仍然只显示 Invalid params
[object Object] 的修复对 daemon route 的领域错误 是有效的:它们现在携带 data.errorKind/status/hint,session_already_bound 能以完整句子送达模型。但 bridge 自身的守卫 —— 也就是身份或来源不符时触发的那些 —— 送到用户手里时消息仍然被抹掉。
真实复现,无需插桩,且是用户真的会遇到的场景:一个定时任务的运行请求把后续运行留在它当前所在的会话中。调用方是任务自有会话,来源守卫正确拒绝 —— 而用户和模型看到的全部内容就是:
Error creating cron job: Invalid params
同一守卫的抓包(为便于隔离帧,改用一个 scheduled_task 来源的会话驱动)显示 daemon 确实没有发送更多信息:
OUT {"jsonrpc":"2.0","id":0,"method":"qwen/control/scheduled-task/create-current",
"params":{"callerSessionId":"a23482c7-…","promptId":"e7d91d82-…","cron":"*/13 * * * *","prompt":"R4_SOURCED","recurring":true}}
IN {"jsonrpc":"2.0","id":0,"error":{"code":-32602,"message":"Invalid params"}}
根因。 withLogSafeAcpError(packages/acp-bridge/src/bridge.ts:531)会把每个向外的 RequestError 消息替换为 logSafeRequestErrorMessage(code),并丢弃所有不含 errorKind 的 data。preserveScheduledTaskCreateErrorOverAcp 正是为了绕过这层过滤而加的 —— 但只覆盖 ExistingSessionScheduledTaskCreateError。handleCreateCurrentSessionScheduledTask 中那 8 个 RequestError.invalidParams(undefined, …) 守卫都没有传 data,因此全部塌缩成同样的两个词:
`callerSessionId` must name a session owned by this connection`promptId` must be a non-empty string、`cron` …、`prompt` …、`recurring` …The caller session does not own the active prompt← 第一轮阻塞问题的确切失败点The caller session no longer owns the active promptThe caller session source cannot own a scheduled task
我也通过伪造 id 验证了 prompt 不匹配这条路径:同样是 Invalid params。也就是说,如果这个问题再次回归,现场信号并不会比第一轮更好。
建议修复(改动很小): 给这些守卫与领域错误相同的待遇,例如 RequestError.invalidParams({ errorKind: 'caller_prompt_mismatch', hint: '…' }, '…') —— logSafeRequestErrorData 本就会透传 errorKind/hint,Session.ts 也已经会渲染 errorKind: hint。
关于测试真实性: cron-create.test.ts 断言的是 { code: -32602, message: 'The caller session does not own the active prompt' }。这个消息在该场景下永远不会到达子进程 —— 线上值是 Invalid params。断言对 getErrorMessage 的验证是对的,但读起来像是守卫文案对用户可见,实际并不可见。
作为对比,REST / Web Shell 这一侧处理得很好 —— 同类拒绝的原文会原样显示在表单里。
关于被推迟的 UI 陈旧态问题:推迟是合理的
在当前 head 仍可复现(打开表单 → 退出 → 另一个客户端开始一轮对话 → 重新打开:选项仍然可选),并且是 fail-closed 且带有良好提示:POST /scheduled-tasks 返回 409 session_busy,表单直接渲染服务端的原文,没有任何持久化。忙碌状态下重新加载页面则正确禁用该选项。这一项不需要阻塞合入。
测试牙齿 —— 对三处修复做了 6 个变异
| 变异 | 套件 | 是否被杀死 |
|---|---|---|
getInvocationContext()?.promptId ?? req.promptId → req.promptId |
Session | ✅ |
删除 Session.ts 中的 errorKind 映射 |
Session | ✅ |
getErrorMessage(error) → 旧的三元表达式 |
cron-create | ✅ |
删除 .catch(preserveScheduledTaskCreateErrorOverAcp) |
bridgeClient | ✅ |
删除 ?? req.promptId 兜底 |
Session | undefined,bridge 同样会拒绝) |
err.status >= 500 ? -32603 : -32602 → 恒为 -32602 |
bridgeClient | Session.ts 只对 -32601 分支) |
本地在该 head 上跑的套件:bridgeClient 126/126、cron-create 17/17、scheduled-task routes 105/105、ScheduledTasksDialog 53/53、Session(定向)2/2。
本轮未覆盖
私有路径上的 pending interaction 守卫无法在真实环境触发 —— 引擎会串行处理审批,因此同一轮中另一个交互处于 pending 时 cron_create 根本不会执行(我驱动了 write_file + cron_create 的并行批次,工具压根没有运行);它仍属于纵深防御。另外未覆盖:注入 bridge 时不广告 capability 的反例(从 CLI 无法触达)、PATCH 改绑、单会话绑定多个任务、#9415 的 teardown-versus-reuse 竞态、Windows/macOS。
|
Thanks for the thorough round-2 verification. I independently checked the exact head and agree with the diagnosability finding: these bridge guards still call I am deferring this as a follow-up rather than widening #9838 again. The paths fail closed, the issue is explicitly non-blocking, and this PR has already gone through more than the repository threshold of roughly five review rounds where only Critical fixes should continue landing. The current lifecycle fix remains merge-ready; the structured error taxonomy and wire-realistic coverage should be handled separately. |
|
@qwen-code /triage |
|
Triage re-run completed without a new review. The bot already has a review of its own on 机器人在 The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
chiga0
left a comment
There was a problem hiding this comment.
No blocking findings.
CI: Test (ubuntu-latest, Node 22.x) ✅ · Real daemon E2E ✅ · Serve A/B ✅ · web-shell E2E Smoke ✅ · Desktop Shell (ubuntu + windows) ✅ · Integration Tests (CLI, No Sandbox) Test (macos-latest / windows-latest, Node 22.x) qwen-serve-routes.test.ts:306 assertion lives outside every npm workspace so it is not exercised by CI.
Authorization chain verified:
- ACP Session stamps its own
sessionId+ executingpromptId(not caller-supplied). Bridge validatescallerSessionIdis owned by this connection ANDentry.activePromptId === promptId. Both hold at the initial check and at write-lock recheck viaassertCallerPromptActive— the closure captures the originalentryobject so object-identity change or prompt-end between calls is caught. assertReusableScheduledTaskSessionis called twice: once before the task object is built, and once inside theupdateCronTaskswrite-lock callback. Both calls invokeassertCallerPromptActive?.()first, serializing eligibility + prompt-active with the atomic write.- REST path keeps the idle rule (
allowActivePrompt = false);cron-toolpath allows an active prompt and rejectspendingInteractionCount > 0at both call sites. - Cross-workspace sessions rejected before session lookup;
ambiguousowner resolution fails closed with 500. preserveScheduledTaskCreateErrorOverAcpconvertsExistingSessionScheduledTaskCreateErrorto typedRequestErrorwitherrorKind— structured rejections surface to the tool caller.Retry-After: 1header is set on theworkspace_runtime_unavailablepath ✓- Optional chain fixed to
workspace.capabilities?.features?.includes(...)✓ - Bootstrap advertises
scheduled_task_session_reuse: false; capability appears only after the managed runtime mounts ✓
One minor finding (R1-1, inline): the source-eligibility check in handleCreateCurrentSessionScheduledTask reads parentSessionId/sourceType/sourceId from the in-memory BridgeClientSessionEntry. After a daemon restart, a reconnected session's bridge entry may not carry this lineage metadata. The fast-path check would pass for a parented sub-session whose entry fields are zero-valued on reconnect. Defense-in-depth is provided by the second authoritative check via bridge.getSessionSummary(), which reads from persisted session data. Not a blocker given the fallback.
Cross-check against existing reviews:
- yiliang114 (head 1180fcd): optional-chain crash → fixed in current head
- wenshao (CHANGES_REQUESTED → APPROVED 2026-08-26): bootstrap capability timing, Retry-After header, promptId threading, pendingInteractionCount staleness, sessionMode reset on capability loss → addressed in current head per wenshao's approval
- qwen-code-ci-bot deferred Suggestion-level (D2-2 through D3-8): missing test pins for source-matrix, control-method payload-validation, classifier projection, submit-time fail-closed — recorded; none are blockers under the convergence posture
- R1-1 → same concern as qwen-code-ci-bot bridgeClient.ts:660 finding — confirmed, mitigated by
getSessionSummaryfallback
Not covered: execution rungs 1-3 not run (no local toolchain). Windows/macOS platform behavior (SKIPPED CI). Integration test suite (SKIPPED CI). Design doc reviewed for algorithmic consistency only.
Reviewed with AI assistance.
| ); | ||
| } | ||
| if ( | ||
| entry.parentSessionId !== undefined || |
There was a problem hiding this comment.
Source-eligibility from in-memory bridge entry may be incomplete after restart (minor): The check here reads parentSessionId, sourceType, and sourceId from the live BridgeClientSessionEntry. If a session reconnects after a daemon restart and the bridge entry is rebuilt without restoring these lineage fields, the guard passes for a parented sub-session or scheduled-task-source session whose entry fields are undefined.
The assertReusableScheduledTaskSession call downstream — both the pre-validation pass and the write-lock recheck — repeats an equivalent check via bridge.getSessionSummary(), which reads from persisted session data. That fallback provides defense-in-depth. If entries are always rehydrated with lineage fields on reconnect, the fast-path check here is redundant but correct; if they are not, only getSessionSummary catches the case.
Not a blocker given the defense-in-depth path, but worth confirming that BridgeClientSessionEntry entries are always populated with parentSessionId/sourceType/sourceId during session rehydration.
Already have 2 approves,3ks.
|
Sandboxed verification: Skipped because the PR is not open for verification (state=MERGED, draft=false). 中文 — 判定:
|
|
Released in v0.22.2. |















What this PR does
This PR adds explicit current-conversation binding to both scheduled-task creation entrypoints while preserving their existing defaults. The Scheduled Tasks form keeps Dedicated task conversation as the default and sends the outer selected conversation's
sessionIdonly after the user selects Current conversation.cron_createkeeps durable tasks unbound by default and addssessionMode: 'current'for an explicitly requested durable current-conversation task.Current-mode tool creation uses a daemon-only ACP control request. The ACP Session stamps its own session id and the executing prompt id, the bridge verifies that the connection owns that session and that the prompt matches its active prompt, and the daemon persists the task with
sessionOwnedByTask: false. The public REST path retains its idle-session rule; only the prompt-matched private path may bind its active caller, and pending interactions remain ineligible.The implementation reuses the existing #9361 lifecycle contract and durable schema. It adds conditional capability advertising, installs the callback on primary, startup-secondary, and dynamically attached workspace runtimes, keeps injected/partial bridges from advertising support, and relies on the existing keepalive, rehydration, deletion, and session scheduler behavior after persistence.
Why it's needed
#9361 added the daemon primitive for reusing an existing session, but users still could not request it from the Scheduled Tasks form or from an active chat. The form always minted a dedicated task conversation, while durable
cron_createstayed unbound and could fire through a different shared per-project owner. These entrypoints let users explicitly keep future scheduled runs in the conversation where the task was requested without changing behavior for existing callers.Reviewer Test Plan
How to verify
scheduled_task_session_reuse. Create a task without changing the conversation selector and confirm a dedicated task conversation is created and the request omitssessionId.sessionOwnedByTask: false. Confirm busy, pending, parented, sourced, cross-workspace, and already-bound sessions cannot be selected.cron_createwithdurable: trueandsessionMode: 'current'. Confirm creation succeeds during that exact prompt, while missing prompt identity, a sibling session, an ineligible source, a pending interaction, or an older bridge fails without an unbound fallback.scheduled_task_session_reuse, and that primary, startup-secondary, and dynamically attached runtimes all receive the host callback when support is enabled.Evidence (Before & After)
sessionIdonly after explicit selection. The component suite passes 51/51 tests.cron_createhad no current-session mode; durable tasks were always unbound.sessionMode: 'current'commits a caller-owned binding through an exact-prompt daemon path, while omitted/unboundbehavior is unchanged. The core suite passes 16/16 tests.Tested on
Environment (optional)
macOS, Node.js 22.22.3, npm 10.9.8.
npm run build,npm run typecheck, andnpm run lintpassed after rebasing ontoorigin/main@3892ca32ca. Focused verification passed for Core (16), ACP bridge (124), scheduled-task routes (102), Web Shell (51), ACP Session (3 targeted), capability registry/server (1 targeted), primary/static-secondary/dynamic runtime wiring (2 targeted), and the formerly failing lazy content-generator test (27).Risk & Scope
Linked Issues
Follow-up to #8906 and #9361. Related to #9415.
中文说明
这个 PR 做了什么
这个 PR 为两个定时任务创建入口增加了显式绑定当前会话的能力,同时保留各自原有的默认行为。Scheduled Tasks 表单仍默认选择“独立任务会话”,只有用户选择“当前会话”后才发送外层选中会话的
sessionId。cron_create的持久化任务默认仍保持未绑定,并新增sessionMode: 'current',用于用户明确要求创建绑定当前会话的持久化任务。工具的 current 模式通过 daemon-only ACP 控制请求创建任务。ACP Session 注入自身 session id 和当前执行中的 prompt id,bridge 校验该连接拥有这个会话且 prompt 与其 active prompt 匹配,daemon 随后以
sessionOwnedByTask: false持久化任务。公开 REST 路径继续要求会话空闲;只有 prompt 精确匹配的私有路径可以绑定正在执行的调用方会话,存在待处理交互时仍然拒绝。实现复用了 #9361 已有的生命周期约定和持久化 schema。它新增条件化 capability 广告,在 primary、启动时 secondary 和动态挂载的 workspace runtime 上安装 callback,禁止注入或仅部分支持的 bridge 广告能力,并在持久化之后继续复用现有 keepalive、rehydration、删除和 session scheduler 行为。
为什么需要
#9361 已经提供了复用已有会话的 daemon 基础能力,但用户仍无法从 Scheduled Tasks 表单或正在执行的聊天中请求该行为。表单始终创建独立任务会话,而持久化
cron_create始终保持未绑定,可能由另一个共享的 per-project owner 执行。新增入口允许用户显式要求后续定时运行留在提出任务的会话中,同时不改变已有调用方的行为。Reviewer 测试计划
如何验证
scheduled_task_session_reuse的 daemon 上打开 Scheduled Tasks。不修改会话选择器创建任务,确认系统创建独立任务会话,并且请求省略sessionId。sessionOwnedByTask: false。确认忙碌、存在待处理交互、有 parent、有特殊来源、跨 workspace 或已绑定的会话无法选择。durable: true和sessionMode: 'current'调用cron_create。确认任务可在该精确 prompt 执行期间创建;缺少 prompt 身份、指向 sibling 会话、来源不符合要求、存在待处理交互或使用旧 bridge 时都会失败,且不会回退为未绑定任务。scheduled_task_session_reuse;启用支持时,primary、启动时 secondary 和动态挂载 runtime 都收到 host callback。证据(Before & After)
sessionId。组件测试 51/51 通过。cron_create没有当前会话模式;持久化任务始终未绑定。sessionMode: 'current'通过精确 prompt 的 daemon 路径提交调用方所有的绑定;省略/unbound行为保持不变。Core 测试 16/16 通过。测试平台
环境(可选)
macOS,Node.js 22.22.3,npm 10.9.8。rebase 到
origin/main@3892ca32ca后,npm run build、npm run typecheck和npm run lint均通过。定向验证全部通过:Core(16)、ACP bridge(124)、scheduled-task routes(102)、Web Shell(51)、ACP Session(定向 3 个)、capability registry/server(定向 1 个)、primary/static-secondary/dynamic runtime 接线(定向 2 个),以及此前失败的 lazy content-generator 测试(27)。风险与范围
关联问题
#8906 和 #9361 的后续工作。与 #9415 相关。