Skip to content

feat(core): make self-paced /loop lean on monitor/background-task notifications - #5844

Merged
wenshao merged 10 commits into
QwenLM:mainfrom
qqqys:feat/loop-monitor-fallback-heartbeat
Jun 25, 2026
Merged

feat(core): make self-paced /loop lean on monitor/background-task notifications#5844
wenshao merged 10 commits into
QwenLM:mainfrom
qqqys:feat/loop-monitor-fallback-heartbeat

Conversation

@qqqys

@qqqys qqqys commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

A self-paced /loop previously knew only one way to stay alive — schedule a timer wakeup with LoopWakeup — so a model running a loop that had started a Monitor or a backgrounded agent would tend to set a short wakeup to "check progress." This teaches the loop that such work re-invokes the session on its own (a Monitor or a backgrounded agent emits a <task-notification> when it finishes), so the wakeup should be a long fallback heartbeat (1200–1800s), not a short poll. It also tells the loop to keep that fallback rather than omit it (the work can hang, a Monitor auto-stops on idle or max-events, or it may be owned by another agent that never notifies the loop), and to handle a notification-triggered tick's event before deciding whether to re-arm.

Why it's needed

A short poll while a <task-notification> will already wake the loop the moment the work terminates is wasted work, and a sub-5-minute wakeup also throws away the prompt-cache window for nothing. The substrate already exists — Session.ts and nonInteractiveCli.ts wire both the monitor registry and the background-task registry notification callbacks into the prompt/notification drain that re-invokes the session — the loop guidance just never mentioned it, so the model had no reason to prefer the event-driven wake. This is primarily guidance; it also adds a small runtime payload improvement by including an escaped <command> element in terminal Monitor notifications, so a notification-driven loop can identify the monitor that finished. It adds no new notification wiring.

Reviewer Test Plan

How to verify

Automated — npm run test --workspace packages/core -- src/tools/loop-wakeup.test.ts src/skills/bundled/loop/SKILL.test.ts: asserts the LoopWakeup tool/delaySeconds description carries both sides of the rule (1200-1800s fallback, 60-270s poll) and that the bundled loop SKILL.md documents the monitor/background-task fallback and the "handle that event first" ordering.

Reading — the change is model-facing guidance text in packages/core/src/tools/loop-wakeup.ts (tool + delaySeconds descriptions) and packages/core/src/skills/bundled/loop/SKILL.md (self-paced path). A reviewer should confirm the wording is accurate against the wake mechanism: a Monitor wakes the loop only on a terminal <task-notification> (running per-line output is dropped), and only work the loop itself started is delivered to it.

Evidence (Before & After)

Before — loop guidance said nothing about monitors/background tasks, so a loop watching work it had backgrounded would schedule short polling wakeups and re-check on a timer. After — the guidance steers the model to set a long fallback heartbeat and rely on the <task-notification> for the real wake, while keeping the fallback so the loop survives the work auto-stopping or hanging. No TUI/pixel change — model-steering text only, so screenshot Before/After is N/A.

Tested on

OS Status
🍏 macOS ✅ unit tests
🪟 Windows ⚠️ not tested (CI)
🐧 Linux ⚠️ not tested (CI)

Environment (optional)

Unit tests only (vitest); no live runtime needed.

Risk & Scope

  • Main risk or tradeoff: low — guidance text plus an additive, escaped <command> field on terminal Monitor notifications; no new notification wiring. The risk is that the wording mis-steers the model or the extra payload is malformed; reviewed for accuracy and covered by escaping tests.
  • Not validated / out of scope: live end-to-end model behavior in a real loop+monitor session (verified by reading the wake mechanism + unit-asserting the guidance text). The 60-270s poll band and CI/queue examples are qwen's existing/added wording, not claimed as upstream-sourced.
  • Breaking changes / migration notes: none — the LoopWakeup description strings and bundled loop SKILL.md body change, and terminal Monitor notifications include an additive <command> element; no API or wiring change.

Linked Issues

Closes #5841

中文说明

这个 PR 做了什么

自定步 /loop 此前只会用 LoopWakeup 排一个定时 wakeup 来续命,所以模型在起了 Monitor 或后台 agent 的循环里会倾向排个短 wakeup 去"看进度"。本改动告诉循环:这类工作会自己 re-invoke 会话(Monitor 或后台 agent 在完成时<task-notification>),所以 wakeup 应当是长兜底心跳(1200–1800s),而不是短轮询。还告诉循环要保留这个兜底而非省略(工作可能 hang、Monitor 会因 idle/max-events 自停、或被别的 agent 持有而永不通知本循环),并在由通知触发的 tick 里先处理事件再决定是否重排。

为什么需要

<task-notification> 已经会在工作终止的瞬间唤醒循环时,再短轮询就是浪费;而短于 5 分钟的 wakeup 还白白丢掉 prompt-cache 窗口。基建本就存在——Session.tsnonInteractiveCli.ts 已把 monitor registry 与后台任务 registry 的通知回调接进会重新唤起会话的 drain——只是循环 guidance 从没提过,模型没理由去优先用事件驱动唤醒。本改动主要是 guidance;同时在 Monitor 终止通知里加入一个已转义的 <command> 元素,让通知触发的循环能识别完成的是哪个 monitor。不新增通知接线。

Reviewer 验证计划

如何验证

自动——npm run test --workspace packages/core -- src/tools/loop-wakeup.test.ts src/skills/bundled/loop/SKILL.test.ts:断言 LoopWakeup 工具/delaySeconds 描述同时带上规则两面(1200-1800s 兜底、60-270s 轮询),且 bundled loop SKILL.md 记录了 monitor/后台任务兜底与"先处理事件"的次序。

阅读——改动是面向模型的 guidance 文本,在 packages/core/src/tools/loop-wakeup.ts(工具 + delaySeconds 描述)与 packages/core/src/skills/bundled/loop/SKILL.md(自定步路径)。reviewer 应核对措辞是否符合唤醒机制:Monitor 只在终止<task-notification> 唤醒循环(逐行 running 输出被丢弃),且只有循环自己起的工作才会投递给它。

证据(Before & After)

Before——循环 guidance 完全没提 monitor/后台任务,所以盯着自己后台化工作的循环会排短轮询 wakeup、靠定时器复查。After——guidance 引导模型排长兜底心跳、把真正唤醒交给 <task-notification>,同时保留兜底以便工作自停/hang 时循环仍存活。无 TUI/像素变化——纯模型引导文本,故 Before/After 截图为 N/A。

测试平台

🍏 macOS ✅(单测)· 🪟 Windows ⚠️ 未测(CI)· 🐧 Linux ⚠️ 未测(CI)。

运行环境(可选)

仅单元测试(vitest),无需实时运行时。

风险与范围

  • 主要风险/取舍:低——guidance 文本加上 Monitor 终止通知里的增量、已转义 <command> 字段;不新增通知接线。风险在措辞是否误导模型或新增负载是否格式错误;已就准确性审过并用转义测试覆盖。
  • 未验证/超范围:真实 loop+monitor 会话里的端到端模型行为(改为读唤醒机制 + 单测断言 guidance 文本来验证)。60-270s 轮询段与 CI/queue 例子是 qwen 既有/自加文字,不声称源自上游。
  • 破坏性改动/迁移:无——改 LoopWakeup 描述串与 bundled loop SKILL.md 正文,并让 Monitor 终止通知包含一个增量 <command> 元素;无 API 或接线改动。

…ifications

A self-paced /loop only knew how to keep itself alive by scheduling a
timer wakeup, even when a Monitor or a backgrounded agent it started
re-invokes the session on its own via a <task-notification> once it
finishes. Teach the LoopWakeup tool description and the bundled loop
skill that such work makes the wakeup a long fallback heartbeat
(1200-1800s), not a short poll: a short poll wastes a prompt-cache
window, and the notification already wakes the loop the moment the work
terminates. Keep the fallback rather than omitting it — a monitor can
auto-stop on idle/max-events, be owned by another agent, or the work
can hang.

Guidance only: qwen already auto-wakes the session on these
notifications; this just stops the loop from polling work that reports
on its own.

Closes QwenLM#5841

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Re-running triage after maintainer verification surfaced something Stage 1 should have caught.

Template looks good ✓

On direction: still solid. Teaching /loop to lean on <task-notification> from a Monitor or backgrounded agent — instead of short-polling on a timer — is a real improvement. Eliminates wasted wakeups and preserves the ~5-min prompt-cache window. The substrate (notification callbacks re-invoking the session) already exists, so this is closing a documentation gap, not inventing new wiring. CHANGELOG has no direct reference but the area (self-paced loops, monitors) is core qwen-code territory.

On scope — correction from the prior run: the PR body says "guidance only; it adds no runtime wiring", but that's no longer accurate. Commit 9aec4ad6a adds a real (additive, safely-escaped) runtime change: terminal <task-notification>s now carry a <command> element so the loop can tell which monitor completed. Two source files outside pure guidance — monitorRegistry.ts (+1 line) and monitorRegistry.test.ts (+5/-1) — are part of this PR. The runtime and guidance changes are functionally coupled (the guidance says "a terminal notification wakes you" and the runtime makes that notification carry enough context to act on it), so keeping them together is defensible, but the body should reflect what actually ships. @qqqys could you update the body to mention the <command> element addition? One sentence is enough.

On approach: focused. The 8-commit stack is a clean narrative (clarify → persist restart count → include command → cap ambiguous follow-ups → cover in tests), no drive-by refactors, no unrelated file touches.

Moving on to code review. 🔍

中文说明

感谢贡献!本次为验证者复核触发的重跑,Stage 1 该抓住但没抓到的问题现在补上。

模板完整 ✓

方向:仍然对齐。让 /loop 依赖 Monitor 或后台 agent 的 <task-notification> 而不是定时器短轮询,能消除无效唤醒并保住约 5 分钟的 prompt-cache 窗口。底层机制(通知回调重新唤起会话)已存在,本 PR 是补齐文档/引导缺口,不是发明新接线。CHANGELOG 无直接引用,但该区域(自定步循环、Monitor)属于 qwen-code 核心。

范围——对上一轮的修正:PR 正文写 "guidance only; it adds no runtime wiring",但这已不准确。提交 9aec4ad6a 加入了一处真实(增量、已安全转义)运行时改动:终止 <task-notification> 现带 <command>,让循环能分辨是哪个 monitor 完成。monitorRegistry.ts(+1)和 monitorRegistry.test.ts(+5/-1)两个非纯 guidance 的源文件在本 PR 里。运行时与 guidance 在功能上是耦合的(guidance 说"终止通知会唤醒你",运行时让该通知携带足够上下文以便处理),合在一起可辩护,但正文应反映实际交付。@qqqys 能否在正文里补一句提到 <command> 元素?一句即可。

方案:聚焦。8 个提交脉络清晰(厘清 → 保留重启计数 → 包含命令 → 截短模糊续作 → 测试覆盖),无顺手重构、无无关文件改动。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal (before reading the diff): To solve "self-paced loops short-poll when a monitor/background task is already watching," I would (1) update LoopWakeup tool + delaySeconds descriptions to name the long-fallback-heartbeat pattern with 1200-1800s vs the short 60-270s poll band, (2) update SKILL.md's self-paced path to handle notification-triggered ticks before re-arming and to preserve state (restart count, stale-wakeup ID) across ticks, (3) add tests asserting the new wording. I would not have added a runtime payload change in the same PR.

Diff comparison: the PR matches the independent proposal on all three guidance/test items, and additionally makes the terminal notification carry a <command> element so the waking tick actually knows which monitor fired. That extra piece is what the independent proposal would have missed — and what the PR body undersells.

Correctness / reuse:

  • <command> element reuses escapeXml + stripDisplayControlChars (from utils/terminalSafe.ts), both already battle-tested — no new escaping utility invented. ✓
  • stripDisplayControlChars handles BEL/ESC/bidi-RLO ("Trojan Source" classes); the maintainer's independent harness confirmed all five XML metacharacters + control/bidi strip correctly on every terminal path.
  • The new SKILL wording ("terminal <task-notification>", "auto-stop on idle or max-events", "one owned by another agent routes its notification only to that agent") matches the real code paths in monitorRegistry.ts, Session.ts, and nonInteractiveCli.ts.

Optional (non-blocking): entry.command is not length-capped in <command>, while description is capped at 80 (MAX_DESCRIPTION_LENGTH) and event lines at EVENT_LINE_TRUNCATE. Commands are typically short so the risk is low, but a cap would be symmetric with the existing convention. @qqqys consider whether it's worth a one-liner.

Testing

Unit tests (PR branch, main working tree): npx vitest run over the three touched suites:

 RUN  v3.2.4 /home/github-runner/actions-runner-16/_work/qwen-code/qwen-code/packages/core

 ✓ src/skills/bundled/loop/SKILL.test.ts (5 tests) 12ms
 ✓ src/services/monitorRegistry.test.ts (56 tests) 43ms
 ✓ src/tools/loop-wakeup.test.ts (19 tests) 52ms

 Test Files  3 passed (3)
      Tests  80 passed (80)
   Duration  2.83s

All 80 pass, including the new teaches the self-paced loop to lean on monitor/background-task notifications (SKILL), documents the fallback-heartbeat semantics for monitor/background work (loop-wakeup), and the extended completes a monitor and emits terminal notification asserting <command>grep &quot;a&amp;b&quot; &lt; /dev/null</command> (monitorRegistry).

Runtime change, observed from the diff:

@@ monitorRegistry.ts
-      `<summary>Monitor "${escapeXml(desc)}" ${statusText}. Total events: ${entry.eventCount}.${…}</summary>`,
+      `<summary>Monitor "${escapeXml(desc)}" ${statusText}. Total events: ${entry.eventCount}.${…}</summary>`,
+      `<command>${escapeXml(stripDisplayControlChars(entry.command))}</command>`,

Terminal <task-notification>s now carry an escaped, control-char-stripped <command> element; running notifications deliberately omit it (matches the fact that only terminal notifications wake the loop).

Real-scenario tmux pass: the maintainer (@wenshao) independently ran a comprehensive tmux verification on the same branch (commit 343374421) covering 5/5 checks — unit tests, RED/GREEN isolation on all three source files, real-compiled MonitorRegistry harness across all terminal paths, before/after on <command> presence, and guidance-vs-code fact-checking. All 20/20 harness runs green, all 5 guidance claims verified against the live wake mechanism. Their full report with capture-pane evidence is in the thread above. I cross-checked against their report rather than re-capture the same terminal output; nothing in the diff contradicts it.

中文说明

代码审查

独立方案(读 diff 之前): 要解决"自定步循环在已有 monitor/后台任务时仍短轮询",我会 (1) 更新 LoopWakeup 工具 + delaySeconds 描述,点名 1200-1800s 长兜底 vs 60-270s 短轮询,(2) 更新 SKILL.md 自定步路径,让它先处理通知触发的 tick 再重排,并跨 tick 保留状态(重启计数、过期 wakeup ID),(3) 加测试断言新措辞。我不会在同一 PR 里加运行时负载改动。

Diff 对比: 上述三项 guidance/测试 PR 全部匹配,并额外让终止通知带上 <command> 以便唤醒 tick 知道是哪个 monitor 触发——这正是独立方案会遗漏、且 PR 正文轻描淡写的部分。

正确性/复用:

  • <command> 复用 escapeXml + stripDisplayControlChars(来自 utils/terminalSafe.ts),两者久经考验,未发明新转义工具 ✓
  • stripDisplayControlChars 处理 BEL/ESC/bidi-RLO("Trojan Source" 类);验证者独立 harness 确认五个 XML 元字符 + 控制符/bidi 在所有终止路径正确转义剥离
  • 新 SKILL 措辞("终止 <task-notification>"、"idle 或 max-events 自停"、"被别的 agent 持有的通知只投给它")与 monitorRegistry.tsSession.tsnonInteractiveCli.ts 真实路径一致

可选(不阻塞): <command> 里的 entry.command 未做长度截断,而 description 截到 80(MAX_DESCRIPTION_LENGTH)、事件行截到 EVENT_LINE_TRUNCATE。命令通常很短、风险低,但加个上限与现有约定更对称。@qqqys 看是否值得加一行。

测试

单元测试(PR 分支,主工作树): 三个涉及套件的 npx vitest run

 ✓ src/skills/bundled/loop/SKILL.test.ts (5 tests) 12ms
 ✓ src/services/monitorRegistry.test.ts (56 tests) 43ms
 ✓ src/tools/loop-wakeup.test.ts (19 tests) 52ms

 Test Files  3 passed (3)
      Tests  80 passed (80)

全部 80 通过,包括新增的 teaches the self-paced loop to lean on monitor/background-task notifications(SKILL)、documents the fallback-heartbeat semantics for monitor/background work(loop-wakeup),以及扩展后断言 <command>grep &quot;a&amp;b&quot; &lt; /dev/null</command>completes a monitor and emits terminal notification(monitorRegistry)。

运行时改动(来自 diff): 终止 <task-notification> 现带经转义、剥离控制字符的 <command>;running 通知刻意不带(与"仅终止通知唤醒循环"一致)。

真实场景 tmux: 验证者 @wenshao 已独立在同分支(commit 343374421)跑过完整 tmux 验证(5/5 检查:单测、三个源文件 RED/GREEN 隔离、真实编译 MonitorRegistry harness 全终止路径、<command> 存在性 before/after、guidance 与代码 fact-check)。harness 20/20 通过、5 条 guidance 断言逐条对照唤醒机制均 ✓。完整报告(含 capture-pane 证据)见上方评论区。我对照其报告复核而非重复抓取同终端输出;diff 无一处与其相矛盾。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Reflection on the rerun.

Going back to my independent proposal from Stage 2: I would have done the same guidance updates in the same files, but I would have missed the <command> payload that makes the guidance actually actionable when the loop wakes up. The PR is better than my proposal on that axis. It's also slightly worse on transparency — the body still reads as "guidance only" when one of the commits ships a real (small, additive, well-escaped) runtime change. The maintainer's note flagged exactly that, and I should have caught it on the first pass.

Net assessment:

  • Direction: aligned. The <task-notification> → long-fallback-heartbeat model is the right shape, uses substrate that already exists, and eliminates a real waste pattern (short-polling when an event-driven wake is already coming).
  • Scope: two coupled changes kept together — runtime <command> element + guidance that assumes it. Splitting them would leave the guidance describing a notification shape that doesn't ship; keeping them together is defensible. The 8-commit stack tells a clean story with no drive-by refactors.
  • Code quality: tight. Reuses escapeXml and stripDisplayControlChars from utils/terminalSafe.ts, no new utility invented. The optional entry.command length cap (maintainer's Note 2) is a real but low-risk nit — description and event lines are capped, this one isn't, commands are typically short.
  • Tests: 80/80 pass in my own run. The maintainer's independent harness adds 20/20 real-MonitorRegistry coverage across all terminal paths plus XML/control/bidi escape verification — more thorough than what the PR's own tests cover, and all green.
  • Transparency: the one real reservation. The PR body undersells the runtime change. I'm asking the author to fix it in the Stage 1 comment above — it's a one-sentence update and shouldn't block merge, but it matters for the next reviewer who reads the body and trusts it.

Am I approving because it's genuinely good, or because I ran out of reasons to say no? Genuinely good. The runtime change is small, safe, well-tested, and makes the guidance actionable; the guidance is accurate against the code; the optional nits (command cap, body wording) are things the maintainer can choose to land or not. The maintainer's own verdict is "recommend merge" after a verification pass more thorough than mine — I have no basis to disagree.

Approving. ✅

中文说明

复核反思。

回到 Stage 2 的独立方案:guidance 部分我会做一样的改动,但我会漏掉 <command> 负载——而正是它让 guidance 在循环被唤醒时真正可执行。在这一点上 PR 比我的方案更好。透明度上稍差——正文仍读作"guidance only",但其中一个提交确实带了一处真实(小、增量、已良好转义)的运行时改动。验证者正是指出了这点,我第一次跑时本该抓到。

总体评估:

  • 方向:对齐。<task-notification> → 长兜底心跳的模型形态正确,复用已存在的基础设施,消除一种真实的浪费模式(事件驱动唤醒已在路上时仍短轮询)
  • 范围:两处耦合改动合并——运行时 <command> + 假设其存在的 guidance。拆开会让 guidance 描述一种尚未交付的通知形态;合在一起可辩护。8 个提交脉络清晰、无顺手重构
  • 代码质量:紧凑。复用 utils/terminalSafe.tsescapeXmlstripDisplayControlChars,未发明新工具。可选的 entry.command 长度上限(验证者注 2)是真实但低风险的小问题——description 和事件行都截了,这个没截,但命令通常很短
  • 测试:我自己跑 80/80 通过。验证者独立 harness 在所有终止路径上补充了 20/20 真实 MonitorRegistry 覆盖,并做了 XML/控制符/bidi 转义验证——比 PR 自带测试更全,且全绿
  • 透明度:唯一真实保留意见。PR 正文对运行时改动轻描淡写。我在 Stage 1 评论里要求作者修复——一行更新即可、不应阻塞合并,但对下一个读到正文并信以为真的 reviewer 很重要

是因为真的好而批准,还是因为找不到反对理由而批准?真的好。运行时改动小、安全、测试充分、让 guidance 可执行;guidance 与代码一致;可选小问题(command 上限、正文措辞)由验证者决定是否落地。验证者自己跑过比我更彻底的验证后给出"建议合并"——我没有理由不同意。

批准 ✅

Qwen Code · qwen3.7-max

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

LGTM, looks ready to ship. ✅

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

No review findings. Downgraded from Approve to Comment: CI failing: Test (ubuntu-latest, Node 22.x).

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/tools/loop-wakeup.ts Outdated
LoopWakeupTool.Name,
ToolDisplayNames.LOOP_WAKEUP,
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h.',
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h. When a background task you started will wake you on its own — a backgrounded agent or a Monitor emits a `<task-notification>` when it finishes — keep this wakeup as a long fallback heartbeat rather than a poll; see `delaySeconds`.',

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] The tool description (and SKILL.md step 3) says a Monitor "emits a <task-notification> when it finishes," but monitorRegistry.ts actually emits a <task-notification> for every stdout line (emitNotification with status=running at line ~498) in addition to the terminal notification (emitTerminalNotification at line ~533). A chatty monitor (e.g., tail -f on a busy log) will wake the agent many times, not once at finish.

This matters because a model following this guidance may start a monitor expecting a single wake-up event, then get flooded with per-line notifications.

Suggested change
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h. When a background task you started will wake you on its own — a backgrounded agent or a Monitor emits a `<task-notification>` when it finishes — keep this wakeup as a long fallback heartbeat rather than a poll; see `delaySeconds`.',
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h. When a background task you started will wake you on its own — a backgrounded agent or a Monitor emits `<task-notification>` events as it runs (per stdout line) and a final one on termination — keep this wakeup as a long fallback heartbeat rather than a poll; see `delaySeconds`.',

— qwen3.7-max via Qwen Code /review

- `prompt`: `/loop ${original prompt}`
- `reason`: a short reason for the chosen delay.
5. Briefly tell the user what was done now. If a wakeup was scheduled, include when the next check is expected. If no wakeup was scheduled, say the loop is complete or not continuing.
5. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event first, then decide whether to re-arm the fallback wakeup or end the loop.

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] Step 5 says "handle that event first" but provides no guidance on how to distinguish the four <task-notification> types a monitor can produce: per-line status=running, terminal status=completed, idle-timeout auto-stop, and max-events auto-stop. All arrive as <task-notification> with different semantics — an idle-timeout auto-stop should trigger re-arming, while a successful completion should end the loop.

Consider adding a sub-bullet with decision heuristics, e.g.:

  • If the notification reports the watched condition was met → end the loop.
  • If the monitor auto-stopped (idle timeout or max-events) → restart the monitor and re-arm the fallback.
  • If ambiguous → re-arm with a shorter delay and investigate next tick.

— qwen3.7-max via Qwen Code /review

it('teaches the self-paced loop to lean on monitor/background-task notifications', () => {
const { body } = loadLoopSkill();

expect(body).toContain('<task-notification>');

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] The new test asserts four substrings but misses the most load-bearing behavioral directive added by this PR: "Do not omit it just because something is watching". This sentence overrides a plausible model inference ("a monitor is watching, so I don't need a fallback wakeup"). The step 5 qualifier "not a bare \/loop` wakeup prompt"` is also untested.

If a future edit softens or removes these directives, the test still passes. Consider adding:

expect(body).toContain('Do not omit it just because something is watching');
expect(body).toContain('not a bare `/loop` wakeup prompt');

— qwen3.7-max via Qwen Code /review

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

⚠️ Downgraded from Request Changes to Comment: CI still running.

Two Critical findings verified against the Session code (see inline), plus two Suggestions. The per-line-notification issue is load-bearing: the model following this guidance would expect to be woken by every stdout line, but Session.ts #registerBackgroundNotificationCallbacks explicitly drops status=running monitor events — only terminal notifications re-invoke the session.

- Do not call LoopWakeup if the task is complete.
- Do not call LoopWakeup if the task is blocked on user input or external state that cannot be checked later.
- Do not call LoopWakeup just to keep polling when no useful next check exists.
- If you started a background agent or a Monitor, it wakes you via `<task-notification>` events as it runs (per stdout line) and a final one on termination — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, a Monitor auto-stops on idle or max-events, and one owned by another agent never notifies you. Omit LoopWakeup only on the terminal conditions above (complete, or blocked).

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.

[Critical] The claim "a backgrounded agent or a Monitor emits <task-notification> events as it runs (per stdout line) and a final one on termination" is inaccurate on two counts, verified against the actual Session wiring:

  1. Per-line monitor events are dropped at the Session boundary. packages/cli/src/acp-integration/session/Session.ts line ~2638 in #registerBackgroundNotificationCallbacks has an explicit if (meta.status === 'running') { return; } guard on the monitor registry callback. Only terminal events (completed / failed / cancelled / idle-timeout / max-events) from monitorRegistry.emitTerminalNotification reach #enqueueBackgroundNotification and re-invoke the model. Every per-line emitNotification call is silently discarded.

  2. Background agents never emit per-line notifications. BackgroundTaskRegistry.emitNotification is only called from complete(), fail(), and finalizeCancelled() — all terminal transitions. A backgrounded agent is completely silent to the parent until it reaches a terminal state.

The delaySeconds description on line 56 actually gets this right ("once it finishes"), but this bullet and the loop-wakeup.ts tool description promise a mechanism that does not exist. A model that sets a 1200–1800s fallback expecting to be woken "per stdout line" will sleep past a watched condition that appears mid-output — it will only be woken when the monitor terminates or auto-stops.

Suggested change
- If you started a background agent or a Monitor, it wakes you via `<task-notification>` events as it runs (per stdout line) and a final one on termination — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, a Monitor auto-stops on idle or max-events, and one owned by another agent never notifies you. Omit LoopWakeup only on the terminal conditions above (complete, or blocked).
- If you started a background agent or a Monitor, it wakes you via a single terminal `<task-notification>` (process exit, failure, cancellation, or a Monitor's auto-stop on idle or max-events) — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, a Monitor auto-stops on idle or max-events, and one owned by another agent never notifies you. Omit LoopWakeup only on the terminal conditions above (complete, or blocked).

Also apply the same fix to the matching sentence in packages/core/src/tools/loop-wakeup.ts line 128.

— qwen3.7-max via Qwen Code /review

Comment thread packages/core/src/tools/loop-wakeup.ts Outdated
LoopWakeupTool.Name,
ToolDisplayNames.LOOP_WAKEUP,
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h.',
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h. When a background task you started will wake you on its own — a backgrounded agent or a Monitor emits `<task-notification>` events as it runs (per stdout line) and a final one on termination — keep this wakeup as a long fallback heartbeat rather than a poll; see `delaySeconds`.',

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.

[Critical] The tool description claims "a backgrounded agent or a Monitor emits <task-notification> events as it runs (per stdout line) and a final one on termination." Verified against the code: this is wrong on both counts.

  • packages/cli/src/acp-integration/session/Session.ts #registerBackgroundNotificationCallbacks has an explicit if (meta.status === 'running') { return; } guard on the monitor registry callback. Per-line emitNotification events from monitorRegistry.ts are silently dropped — only terminal emitTerminalNotification events (completed / failed / cancelled / idle-timeout / max-events) re-invoke the session.
  • BackgroundTaskRegistry.emitNotification is called only from complete(), fail(), and finalizeCancelled(). Background agents never emit per-line notifications.

Your own delaySeconds description on line 135 already gets this right ("once it finishes"). A model following the tool description will expect per-line wake-ups that will never arrive, and will sleep past a watched condition that appears mid-output.

Suggested change
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h. When a background task you started will wake you on its own — a backgrounded agent or a Monitor emits `<task-notification>` events as it runs (per stdout line) and a final one on termination — keep this wakeup as a long fallback heartbeat rather than a poll; see `delaySeconds`.',
'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h. When a background task you started will wake you on its own — a backgrounded agent or a Monitor emits a single terminal `<task-notification>` (process exit, failure, cancellation, or a Monitor auto-stop on idle or max-events) — keep this wakeup as a long fallback heartbeat rather than a poll; see `delaySeconds`.',

— qwen3.7-max via Qwen Code /review

- `prompt`: `/loop ${original prompt}`
- `reason`: a short reason for the chosen delay.
5. Briefly tell the user what was done now. If a wakeup was scheduled, include when the next check is expected. If no wakeup was scheduled, say the loop is complete or not continuing.
5. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event first, then decide whether to re-arm the fallback wakeup or end the loop.

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] "handle that event first" contradicts the step ordering. Steps 2–4 tell the model to "Run the parsed prompt immediately now" and decide whether another check is useful (including scheduling a LoopWakeup). Step 5 is reached only after the prompt has been executed. On a notification-driven re-invocation, a model reading the numbered sequence top-to-bottom will re-run the prompt and potentially schedule a new fallback wakeup before handling the incoming <task-notification>. The word "first" in step 5 contradicts its sequential position.

This leads to duplicated work (re-reading a deploy log when the notification already says "deploy succeeded") and unnecessary tool calls on each notification-driven tick.

Either move this step above step 2, or add an explicit guard at the top of the self-paced path, e.g.:

Suggested change
5. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event first, then decide whether to re-arm the fallback wakeup or end the loop.
On a tick that opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), skip straight to step 5 before running the prompt.

— qwen3.7-max via Qwen Code /review

5. Briefly tell the user what was done now. If a wakeup was scheduled, include when the next check is expected. If no wakeup was scheduled, say the loop is complete or not continuing.
5. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event first, then decide whether to re-arm the fallback wakeup or end the loop.
- If the notification says the watched condition was met, finish the loop.
- If a monitor auto-stopped on idle or max-events, restart it if the watch is still useful and re-arm the fallback.

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] "restart it if the watch is still useful and re-arm the fallback" has no circuit-breaker. If a monitor keeps auto-stopping on the same trigger (a noisy log source that always hits maxEvents within seconds, or a stable file that idle-times-out immediately), the model will restart it, receive another auto-stop notification, restart again, and loop indefinitely — each cycle consuming a full model turn with tool calls. The loop can only terminate when the 24h wakeup-chain limit is reached, the token budget runs out, or the user intervenes. This is the exact class of misbehavior that step 3's terminal conditions are meant to prevent.

Suggested change
- If a monitor auto-stopped on idle or max-events, restart it if the watch is still useful and re-arm the fallback.
- If a monitor auto-stopped on idle or max-events, restart it once if the watch is still useful and re-arm the fallback. If it auto-stops again on the next tick, end the loop and report the repeated auto-stop to the user.

— qwen3.7-max via Qwen Code /review

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

Re-review after commit 49091d89 ("clarify loop task notifications").

The commit addressed one of the prior Suggestions — the test now asserts "Do not omit it just because something is watching" and "not a bare \/loop` wakeup prompt"`, closing the coverage gap flagged earlier. The other four prior findings (1 Critical + 3 Suggestions) remain unresolved on the current HEAD.

The Critical per-line-notification concern is still valid and is load-bearing for the guidance:

  • Registry layer (monitorRegistry.ts ~line 498): emitNotification does emit a <task-notification> with <status>running</status> for every stdout line — this part is correct.
  • Daemon path (Session.ts ~line 2638, #registerBackgroundNotificationCallbacks): the monitor callback has an explicit if (meta.status === 'running') { return; } guard — all per-line running-status events are unconditionally dropped. The model never sees them; only terminal (completed/failed/cancelled) notifications re-invoke the session.
  • TUI (useGeminiStream.ts ~line 3234), non-interactive CLI (nonInteractiveCli.ts ~line 648), headless SDK (nonInteractive/session.ts ~line 203): running events ARE delivered (with a stale-entry guard). Per-line notifications reach the model.

So the PR's wording — "emits <task-notification> events as it runs (per stdout line)" — is true for three of four paths but false for the daemon path, which is where /loop most commonly runs. A model following this guidance would expect to be woken by every stdout line, but in the daemon path it will only be woken on termination. The long-fallback-heartbeat advice is still directionally right, but the per-line justification overclaims. Suggested reword (both the tool description at loop-wakeup.ts:128 and SKILL.md step 3): drop the "per stdout line" qualifier and say "a Monitor or backgrounded agent emits a terminal <task-notification> when it completes, fails, or is cancelled" — accurate across all paths without leaking a per-line detail that only applies outside the daemon.

The three unresolved Suggestions are unchanged from the prior review:

  • SKILL.md step 5 "handle that event first" still contradicts the top-to-bottom ordering of steps 2–4, which commit the model to running the prompt and deciding on a LoopWakeup before step 5 is reached.
  • SKILL.md step 5's restart-the-monitor branch still has no circuit-breaker for a monitor that keeps auto-stopping on the same trigger.
  • SKILL.md step 5 still doesn't distinguish the four <task-notification> kinds a monitor can produce (status=running per-line, terminal completed, idle-timeout auto-stop, max-events auto-stop).

Deterministic checks clean: tsc --noEmit and eslint pass on the 4 changed files; vitest run src/tools/loop-wakeup.test.ts src/skills/bundled/loop/SKILL.test.ts passes 24/24.

— qwen3.7-max via Qwen Code /review

expect(body).not.toContain('delayMinutes');
});

it('teaches the self-paced loop to lean on monitor/background-task notifications', () => {

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] The new test asserts 15+ substrings from SKILL.md but misses the two core safety justifications added at SKILL.md line 58: the work may hang and one owned by another agent never notifies you. These are the primary reasons LoopWakeup must not be omitted — the central behavioral argument of this entire PR — yet they have no test guard. A future edit could soften or drop them without breaking any assertion.

Suggested change
it('teaches the self-paced loop to lean on monitor/background-task notifications', () => {
it('teaches the self-paced loop to lean on monitor/background-task notifications', () => {
const { body } = loadLoopSkill();
expect(body).toContain('<task-notification>');
expect(body).toContain('set LoopWakeup as a long fallback');
expect(body).toContain('auto-stops on idle or max-events');
expect(body).toContain('handle that event before re-running the prompt');
expect(body).toContain('terminal `<task-notification>`');
expect(body).not.toContain('per stdout line');
expect(body).toContain(
'If the notification says the watched condition was met',
);
expect(body).toContain('If a monitor auto-stopped');
expect(body).toContain('restart it once');
expect(body).toContain('report the repeated auto-stop to the user');
expect(body).toContain('If the signal is ambiguous');
expect(body).toContain('Do not omit it just because something is watching');
expect(body).toContain('the work may hang');
expect(body).toContain('another agent never notifies you');
expect(body).toContain('not a bare `/loop` wakeup prompt');
});

— qwen3.7-max via Qwen Code /review

2. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event before re-running the prompt.
- If the notification says the watched condition was met, finish the loop.
- If a monitor auto-stopped on idle or max-events, restart it once if the watch is still useful and re-arm the fallback. If it auto-stops again on the next tick, end the loop and report the repeated auto-stop to the user.
- If the signal is ambiguous, re-arm a shorter follow-up and investigate on the next tick.

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] "Restart it once" has no state-persistence mechanism. Loop ticks are 1200–1800s apart (20–30 min), and context truncation could erase the prior turn's reasoning. The model on a later tick sees a fresh <task-notification> about a monitor auto-stop with no history that it already used its one restart — it restarts again, creating a silent restart loop that never escalates to the user.

Consider instructing the model to include the restart count in its user-facing summary (step 6) so it survives context truncation — e.g., "When reporting, include the restart count (e.g., 'monitor restarted 1/1 time') so the next tick can read it from the conversation history."

— qwen3.7-max via Qwen Code /review

- If you started a background agent or a Monitor, it wakes you via a terminal `<task-notification>` on exit, failure, cancellation, or monitor auto-stop — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, a Monitor auto-stops on idle or max-events, and one owned by another agent never notifies you. Omit LoopWakeup only on the terminal conditions above (complete, or blocked).
5. When scheduling a continuation, call LoopWakeup with:
- `delaySeconds`: the next useful delay in seconds. The runtime clamps to 60–3600 (1–60 min); follow the tool's own guidance on picking a value — it accounts for the prompt-cache window and for the fallback-heartbeat case when a background task will wake you.
- `prompt`: `/loop ${original prompt}`

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] "one owned by another agent never notifies you" is factually wrong for background agents. The BackgroundTaskRegistry has no per-agent routing — it uses a single session-level notificationCallback (background-tasks.ts:966), so ALL background agent completions are delivered to the session regardless of which agent started them. Only the MonitorRegistry has per-agent routing via ownerAgentId. Since this sentence explicitly references both ("a background agent or a Monitor"), the claim misleads the model for background agents.

Also, the parenthetical "(complete, or blocked)" omits the repeated-auto-stop terminal condition introduced in step 2.

Suggested change
- `prompt`: `/loop ${original prompt}`
- If you started a background agent or a Monitor, it wakes you via a terminal `<task-notification>` on exit, failure, cancellation, or monitor auto-stop — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, a Monitor auto-stops on idle or max-events (and one owned by another agent routes its notification only to that agent). Omit LoopWakeup only on the terminal conditions above (complete, blocked, or repeated monitor auto-stop).

— qwen3.7-max via Qwen Code /review

- If a monitor auto-stopped on idle or max-events, restart it once if the watch is still useful, re-arm the fallback, and report the restart count to the user. If it auto-stops again on the next tick, end the loop and report the repeated auto-stop to the user.
- If the signal is ambiguous, re-arm a shorter follow-up and investigate on the next tick.
3. Run the parsed prompt immediately now.
- If it is a slash command, invoke it via the Skill tool.

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] The ambiguous-signal branch has no retry cap, unlike the other two branches: "condition met" terminates immediately, "auto-stopped" restarts once then terminates, but "ambiguous" re-arms with no limit. If monitored resources consistently produce ambiguous output, the loop re-arms indefinitely (up to the 24h chain cap).

Suggested change
- If it is a slash command, invoke it via the Skill tool.
- If the signal is ambiguous, re-arm a shorter follow-up and investigate on the next tick. If the signal remains ambiguous for three consecutive ticks, end the loop and report to the user that the watch could not reach a clear conclusion.

— qwen3.7-max via Qwen Code /review

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

No review findings. Downgraded from Approve to Comment: CI still running.

The previous round's Critical findings (per-line notification inaccuracy) have been correctly addressed — the guidance now accurately describes terminal-only <task-notification> delivery and the restart-once policy. The 6 stale comments from earlier commits are superseded by the current code.

— qwen3.7-max via Qwen Code /review

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

Re-review after commit db93f36d (two commits since last review at 49091d89).

The new commits address the two Critical findings from the prior review — "per stdout line" is fully replaced with "terminal <task-notification> on exit, failure, cancellation, or monitor auto-stop," and the step ordering is fixed by moving notification handling to step 2. Tests are updated accordingly and all 24 pass.

The prior Suggestions about restart state persistence and ambiguous-signal retry cap were partially addressed: "restart it once" + "end the loop on second auto-stop" adds a natural-language circuit breaker, and "report the restart count" surfaces the tracking intent. However, neither concern has a code-level backing — the restart count lives only in conversation memory (vulnerable to compaction over 1200-1800s ticks), and the ambiguous-signal branch still lacks explicit bounds.

One new finding below: the "restart it once" guidance assumes the model can reconstruct the monitor command, but the terminal <task-notification> XML from monitorRegistry.ts doesn't include it.

— qwen3.7-max via Qwen Code /review

2. Run the parsed prompt immediately now.
2. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event before re-running the prompt.
- If the notification says the watched condition was met, finish the loop.
- If a monitor auto-stopped on idle or max-events, restart it once if the watch is still useful, re-arm the fallback, and report the restart count to the user. If it auto-stops again on the next tick, end the loop and report the repeated auto-stop to the user.

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] The "restart it once" guidance assumes the model can (a) track that it already restarted across ticks 1200–1800s apart, and (b) reconstruct the original monitor command. Both are fragile:

  • State persistence: the restart count lives only in conversation memory. Over 1200–1800s ticks, context compaction can erase it, causing the model to either restart indefinitely or end the loop prematurely. Consider encoding the count in the LoopWakeup reason field or the continuation prompt so it survives compaction.
  • Command availability: the terminal <task-notification> XML emitted by monitorRegistry.ts (emitTerminalNotification) includes <task-id>, <status>, <event-count>, and <summary> — but not the original command. The <summary> contains only the truncated description (≤80 chars), not a runnable command. After context compaction, the model may not be able to reconstruct the exact command to restart.

Consider adding the original command to the terminal notification XML so the model has it available for restart without relying on conversation history.

— qwen3.7-max via Qwen Code /review

const [displayText, modelText] = callback.mock.calls[0] as [string, string];
expect(displayText).toContain('completed');
expect(modelText).toContain('<status>completed</status>');
expect(modelText).toContain('<command>tail -f /var/log/app.log</command>');

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] The <command> assertion uses the safe default tail -f /var/log/app.log. No test covers XML escaping of the command field — if escapeXml were accidentally removed from line 565 of monitorRegistry.ts, commands containing <, >, or & would produce malformed XML without any test failing.

Consider adding a test that registers a monitor with command: 'grep "a&b" < /dev/null' and asserts the escaped output:

expect(modelText).toContain('<command>grep &quot;a&amp;b&quot; &lt; /dev/null</command>');

— qwen3.7-max via Qwen Code /review

- `prompt`: `/loop ${original prompt}`
- `reason`: a short reason for the chosen delay.
5. Briefly tell the user what was done now. If a wakeup was scheduled, include when the next check is expected. If no wakeup was scheduled, say the loop is complete or not continuing.
- If you started a background agent or a Monitor, it wakes you via a terminal `<task-notification>` on exit, failure, cancellation, or monitor auto-stop — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, or a Monitor may auto-stop on idle or max-events (and one owned by another agent routes its notification only to that agent). Omit LoopWakeup only on the terminal conditions above (complete, blocked, or repeated monitor auto-stop).

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] When a task-notification arrives and the loop ends (condition met or repeated auto-stop), the previously scheduled 1200–1800s fallback wakeup is still pending in the CronScheduler. It will fire 20–30 minutes later as a stale prompt, and sessionSize counts pending wakeups so it keeps the headless process alive. With the previous 60–270s delays this was minor; at 1200–1800s it's a meaningful resource hold.

Consider adding a note here to cancel the pending fallback wakeup with CronDelete when ending the loop. If the wakeup ID was compacted away, the stale wakeup is self-limiting (fires once and is deleted), so a brief "ignore or answer briefly" note for unexpected wakeups would suffice.

— qwen3.7-max via Qwen Code /review

2. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event before re-running the prompt.
- If the notification says the watched condition was met, finish the loop.
- If a monitor auto-stopped on idle or max-events, restart it once if the watch is still useful, re-arm the fallback, report the restart count to the user, and include that count in the LoopWakeup prompt or reason (for example, `monitor restarted 1/1 time`) so it survives context compaction. If it auto-stops again on the next tick, end the loop and report the repeated auto-stop to the user.
- If the signal is ambiguous, re-arm a shorter follow-up and investigate on the next tick. If the signal remains ambiguous for three consecutive ticks, end the loop and report that the watch could not reach a clear conclusion.

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] The three-tick ambiguity cap has no persistence mechanism, unlike the adjacent restart counter which explicitly carries state via monitor restarted 1/1 time in the LoopWakeup prompt. After context compaction (likely across 1200–1800s ticks), the model may lose track of how many consecutive ticks were ambiguous and never trigger the cap — the loop keeps re-arming indefinitely.

Consider mirroring the restart-branch pattern: add an instruction to include the ambiguity count in the LoopWakeup prompt (e.g., ambiguous tick 2/3), and add it as an example in step 5's prompt field alongside monitor restarted 1/1 time.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report — PR #5844

I built this branch and ran a real-runtime verification locally (Linux, in tmux), not just a read-through. Summary first, evidence below.

Verdict: behaviour is correct and precisely guarded by the new tests. Recommend merge. One minor wording nit in the PR body (the change is no longer purely guidance — see Note 1), and two optional/neutral observations.


What I verified & how

  • Branch/commit: feat/loop-monitor-fallback-heartbeat @ 343374421, packages/core built green from source.
  • Tooling: the PR's own vitest suites, a real-compiled harness driving the actual MonitorRegistry from packages/core/dist, plus a RED/GREEN isolation pass and a static fact-check of the guidance against the live wake mechanism. All test runs were executed inside a tmux session and captured.
# Check Result
1 PR unit tests monitorRegistry + loop-wakeup + SKILL 80/80 pass (56 + 19 + 5)
2 RED/GREEN: revert each of the 3 source files to merge-base, keep the PR's tests exactly 3 fail, 77 pass — one failing test per file, no collateral; restore → green
3 Real MonitorRegistry harness: complete(0), complete(2), fail, cancel, max-events auto-stop, idle-timeout auto-stop, owner-scoped delivery 20/20 pass
4 Before/after on the one runtime line (pre-fix dist) terminal <command> absent on every path (8 presence checks flip); running-notification check stays green both ways
5 Guidance text fact-checked against the real code accurate (see table below)

The runtime change, observed (real compiled MonitorRegistry)

The harness fed a command laden with all five XML metacharacters plus a C0 control (BEL), an ANSI ESC sequence, and a bidi RLO override (U+202E), then drove each terminal path. Every terminal <task-notification> now carries a correctly escaped + control/bidi-stripped <command>:

  <task-notification>
  <task-id>sample</task-id>
  <kind>monitor</kind>
  <status>completed</status>
  <event-count>0</event-count>
  <summary>Monitor "watch app logs" completed. Total events: 0.</summary>
+ <command>tail -f /var/log/app.log</command>     ← added by this PR (terminal notifications only)
  <result>Exited with code 0</result>
  </task-notification>
  • Escaping/stripping is exact: grep "a&b" < … ' \x07\x1b[31m \u202e…grep &quot;a&amp;b&quot; &lt; … &apos; [31m … with the BEL/ESC/RLO bytes removed. This extends the PR's own escaping test (which covers " & <) to the control/bidi "Trojan Source" classes — also green.
  • Running notifications deliberately do not carry <command> — only terminal ones do, which matches the fact that a self-paced loop is woken solely by terminal notifications.
  • Owner-scoped monitors route the terminal notification (with <command>) only to the owning agent's callback, never the parent — consistent with the guidance's "another agent owns it" caveat.

Guidance fact-check (claims ↔ code)

Guidance claim Code Verdict
Monitor auto-stops on idle or max-events, emitting a terminal <task-notification> monitorRegistry.ts idle timer + eventCount >= maxEvents, both call emitTerminalNotification
A monitor owned by another agent routes its notification only to that agent dispatchNotificationagentNotificationCallbacks.get(ownerAgentId) else default
Only a terminal notification wakes the loop (no per-line poll) ACP path drops status==='running'; PR also removes any "per stdout line" wording
delaySeconds clamped to [60, 3600] WAKEUP_MIN_SECONDS=60, WAKEUP_MAX_SECONDS=3600
Notification re-invokes the session nonInteractiveCli.ts local-queue drain + Session.ts notification-queue drain

Notes (none blocking)

  1. Body wording: the description says "guidance only; it adds no runtime wiring." A later commit (9aec4ad6a) adds a real—though additive and safely escaped—runtime change: the <command> element in the terminal notification payload. It's not new wiring, but it is a behaviour change worth one line in the body so reviewers don't skip it.
  2. Optional (low): entry.command is not length-capped in the <command> element, whereas description is capped at 80 and event lines at 2000. Commands are normally short, so the risk is small; a cap would be symmetric/defensive.
  3. Neutral: running notifications omitting <command> is by design (terminal-only) and correct for the loop's wake semantics — flagged only so the asymmetry is intentional on record.

Recommendation: ✅ Looks good to merge. The new behaviour is correct, well-escaped, and exactly covered by the added tests; the model-facing guidance is accurate against the runtime.

中文说明

我在本地拉取该分支做了真实运行时验证(Linux,全程在 tmux 中执行并留存日志),不是只读代码。先给结论,证据在下。

结论:行为正确,且被新增测试精确覆盖,建议合并。 PR 正文有一处措辞需微调(本改动已不再是 guidance,见注 1),另有两条可选/中性观察。

验证内容与方法

  • 分支/提交: feat/loop-monitor-fallback-heartbeat @ 343374421packages/core 由源码构建通过。
  • 手段: PR 自带 vitest 套件;一个驱动 packages/core/dist真实编译 MonitorRegistry 的 harness;一轮 RED/GREEN 隔离验证;以及对照真实唤醒机制对 guidance 文本逐条 fact-check。所有测试均在 tmux 会话内运行并捕获。
# 检查项 结果
1 PR 单测 monitorRegistry + loop-wakeup + SKILL 80/80 通过(56 + 19 + 5)
2 RED/GREEN:把 3 个源文件回退到 merge-base、保留 PR 的测试 恰好 3 个失败、77 通过——每个文件各失败一个、无连带;恢复后转绿
3 真实 MonitorRegistry harness:complete(0/2)failcancel、max-events 自停、idle 自停、owner-scoped 投递 20/20 通过
4 对那一行运行时改动做 before/after(pre-fix dist) 各终止路径的 <command> 全部缺失(8 个存在性断言翻转);running 通知两侧都不含 command
5 guidance 文本对照真实代码 准确(见下表)

实际观察到的运行时改动(真实编译 MonitorRegistry

harness 喂入一个同时包含五个 XML 元字符、外加 C0 控制符(BEL)、ANSI ESC 序列、bidi RLO 覆盖符(U+202E)的命令,逐一驱动各终止路径。每个终止 <task-notification> 现都带上经正确转义 + 控制符/bidi 剥离的 <command>

  <summary>Monitor "watch app logs" completed. Total events: 0.</summary>
+ <command>tail -f /var/log/app.log</command>     ← 本 PR 新增(仅终止通知)
  <result>Exited with code 0</result>
  • 转义/剥离精确无误:grep "a&b" < … ' \x07\x1b[31m \u202e…grep &quot;a&amp;b&quot; &lt; … &apos; [31m …,BEL/ESC/RLO 字节被去除。这把 PR 自带的转义测试(覆盖 " & <)扩展到了控制符/bidi 的 "Trojan Source" 类——同样通过。
  • running 通知刻意<command>,仅终止通知带——这与"自定步循环只由终止通知唤醒"一致。
  • owner-scoped 监视器的终止通知(含 <command>投递给所属 agent 的回调,绝不投给父级——与 guidance 中"被别的 agent 持有"那条吻合。

Guidance fact-check(断言 ↔ 代码)

guidance 断言 代码 结论
Monitor 在 idle max-events 自停并发终止 <task-notification> monitorRegistry.ts idle timer + eventCount >= maxEvents,均调用 emitTerminalNotification
被别的 agent 持有的监视器,其通知只投给那个 agent dispatchNotificationagentNotificationCallbacks.get(ownerAgentId),否则默认
只有终止通知会唤醒循环(不逐行轮询) ACP 路径丢弃 status==='running';PR 也删除了任何 "per stdout line" 措辞
delaySeconds 钳制到 [60, 3600] WAKEUP_MIN_SECONDS=60WAKEUP_MAX_SECONDS=3600
通知会重新唤起会话 nonInteractiveCli.ts 本地队列 drain + Session.ts 通知队列 drain

备注(均不阻塞)

  1. 正文措辞: 描述写的是 "guidance only; it adds no runtime wiring"。后续提交(9aec4ad6a)加入了一处真实——但属增量且已安全转义——的运行时改动:终止通知里的 <command>。它不算新"接线",但确是行为变化,正文里补一句更稳妥,免得 reviewer 跳过。
  2. 可选(低): entry.command<command>做长度截断,而 description 截到 80、事件行截到 2000。命令通常很短、风险不大;加个上限会更对称/防御。
  3. 中性: running 通知不带 <command> 是有意为之(仅终止带),对循环唤醒语义是正确的——仅备案说明该不对称是刻意的。

建议: ✅ 可以合并。新行为正确、转义到位、被新增测试精确覆盖;面向模型的 guidance 与运行时一致。


🤖 Generated with Claude Code — Claude Opus 4.8 (1M context)

@wenshao

wenshao commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

- `prompt`: `/loop ${original prompt}`
- `reason`: a short reason for the chosen delay.
5. Briefly tell the user what was done now. If a wakeup was scheduled, include when the next check is expected. If no wakeup was scheduled, say the loop is complete or not continuing.
- If you started a background agent or a Monitor, it wakes you via a terminal `<task-notification>` on exit, failure, cancellation, or monitor auto-stop — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, or a Monitor may auto-stop on idle or max-events (and one owned by another agent routes its notification only to that agent). Omit LoopWakeup only on the terminal conditions above (complete, blocked, or repeated monitor auto-stop).

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] This line says "a background agent or a Monitor" but omits background shells (run_shell_command with is_background: true). BackgroundShellRegistry emits the same terminal <task-notification> pattern (verified at backgroundShellRegistry.ts:442-476 with <kind>shell</kind>), so a loop watching a background shell (e.g., npm run build in background) should also set a long fallback heartbeat. The tool description in loop-wakeup.ts uses the broader "a background task you started" which covers all three — consider matching that wording here for consistency.

Suggested change
- If you started a background agent or a Monitor, it wakes you via a terminal `<task-notification>` on exit, failure, cancellation, or monitor auto-stop — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, or a Monitor may auto-stop on idle or max-events (and one owned by another agent routes its notification only to that agent). Omit LoopWakeup only on the terminal conditions above (complete, blocked, or repeated monitor auto-stop).
- If you started a background task (a background agent, a Monitor, or a background shell), it wakes you via a terminal `<task-notification>` on exit, failure, cancellation, or monitor auto-stop — so set LoopWakeup as a long fallback rather than a short poll. Do not omit it just because something is watching: the work may hang, or a Monitor may auto-stop on idle or max-events (and one owned by another agent routes its notification only to that agent). Omit LoopWakeup only on the terminal conditions above (complete, blocked, or repeated monitor auto-stop).

— qwen3.7-max via Qwen Code /review


1. Do not call CronCreate for this path.
2. Run the parsed prompt immediately now.
2. If this tick opens with a `<task-notification>` block (a monitor or background event re-invoked you, not a bare `/loop` wakeup prompt), handle that event before re-running the prompt.

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] After handling a notification in step 2, the model continues to step 3 ("Run the parsed prompt immediately now") unconditionally. But the three sub-cases describe different outcomes:

  • Condition met: "finish the loop" — running the prompt again is unnecessary.
  • Auto-stopped: monitor restarted, fallback re-armed — running the prompt could launch a duplicate watcher.
  • Ambiguous: "investigate on the next tick" — running the prompt now contradicts the deferral.

Consider making the short-circuit explicit, e.g.: "handle that event and end the turn (do not continue to step 3)" or restructuring so steps 3–6 only run when there is no notification.

— qwen3.7-max via Qwen Code /review

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

LGTM, looks ready to ship. ✅ The guidance is accurate against the runtime, the <command> addition is small and well-escaped, and 80/80 unit tests pass. One non-blocking ask: update the PR body to mention the runtime <command> change (currently reads as guidance-only when it isn't). Optional: consider a length cap on entry.command for symmetry with MAX_DESCRIPTION_LENGTH and EVENT_LINE_TRUNCATE.

@wenshao
wenshao added this pull request to the merge queue Jun 25, 2026
Merged via the queue into QwenLM:main with commit 2653c9e Jun 25, 2026
54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[loop] Self-paced /loop should treat LoopWakeup as a fallback when a Monitor or background task will wake it

3 participants