feat(cli): add /loop command for periodic task execution#2643
feat(cli): add /loop command for periodic task execution#2643wenshao wants to merge 24 commits intoQwenLM:mainfrom
Conversation
Implement a /loop slash command that periodically executes a prompt at a configurable interval, enabling hands-off monitoring and automation workflows. Core module (packages/core/src/loop/): - LoopManager: framework-agnostic state management with start/stop/resume - Interval parsing (30s, 5m, 1.5h) and formatting utilities - Configurable max iterations and auto-pause after 3 consecutive failures - Minimum interval 10s, maximum 24h to prevent API abuse and timer overflow - waitingForResponse flag to distinguish loop iterations from user prompts CLI command (packages/cli/src/ui/commands/loopCommand.ts): - /loop [interval] <prompt> — start a loop (default: 10m) - /loop [interval] --max N <prompt> — limit iterations - /loop status — show active loop info - /loop stop — cancel the active loop - Tab completion for subcommands and common intervals AppContainer integration: - Registers loop iteration callback via useRef to avoid stale closures - Notifies loop manager on streaming completion for scheduling - Only processes completions when loop is waiting (not user prompts) - Displays iteration headers and completion/pause messages with i18n - Stops loop on component unmount Tests: 24 tests covering interval parsing, formatting, loop lifecycle, failure handling, pause/resume, skipFirstIteration, waitingForResponse guard, timer cleanup, and singleton management. Closes QwenLM#2638 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📋 Review SummaryThis PR introduces a 🔍 General Feedback
🎯 Specific Feedback🟡 High
🟢 Medium
🔵 Low
✅ Highlights
|
… null state test - Explicitly clear timerId when loop completes via maxIterations, even though clearTimeout is idempotent, for defensive cleanup - Add test for onIterationComplete when no loop is active (null state) to verify it doesn't throw Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review! Here's how I addressed each point: High
Medium
Low8-9. Readonly / @module — Accepted as-is.
11-12. Import grouping / safe integer — Imports follow existing project conventions. |
…nsistency - Fix race condition: loop prompts now use a dedicated submission path (loopSubmitRef + submitQueryRef) instead of sharing the user message queue. Added loopInitiatedStreamRef to distinguish loop-initiated streams from user-initiated ones. - Fix error detection: scan pending items (API/streaming errors) and committed history backward for ERROR and tool_group errors, instead of only checking the last history item. - Fix slash command consistency: all loop iterations submit as Part[] to bypass isSlashCommand() parsing, matching the first-iteration submit_prompt path. - Fix /btw interaction: don't clear loop flag for btw commands that run alongside the current stream. - Fix /loop stop with queued prompt: discard queued prompt if loop was stopped while waiting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review & Fix Summary经过多轮代码审查,发现以下问题并在 commit 1. Race Condition — 用户消息与 loop 迭代竞态(中)问题:loop 迭代通过 修复:loop 迭代使用独立的提交路径( 2. 嵌套 Slash 命令不一致(中)问题:首次迭代通过 修复:所有迭代统一通过 3. 错误检测不可靠(中)问题:仅检查 修复:新增 4. 其他修复
Remaining Items (Low Priority)
|
There was a problem hiding this comment.
Pull request overview
Adds a new /loop CLI slash command backed by a framework-agnostic LoopManager in core, with CLI integration that schedules subsequent iterations when streaming completes.
Changes:
- Introduces
LoopManager(+ interval parse/format helpers, singleton accessors) and unit tests inpackages/core. - Adds
/loopcommand implementation in the CLI and registers it inBuiltinCommandLoader. - Integrates loop iteration triggering/completion handling into
AppContainer.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/loop/loopManager.ts | Implements core loop state machine, interval helpers, and singleton accessors. |
| packages/core/src/loop/loopManager.test.ts | Adds Vitest coverage for parsing/formatting and loop lifecycle behavior. |
| packages/core/src/loop/index.ts | Exposes loop module exports. |
| packages/core/src/index.ts | Re-exports loop module from core public API. |
| packages/cli/src/ui/commands/loopCommand.ts | Implements /loop start/status/stop + interval bounds and completions. |
| packages/cli/src/ui/AppContainer.tsx | Wires loop iteration callback to prompt submission and marks completion on stream idle. |
| packages/cli/src/services/BuiltinCommandLoader.ts | Registers the new loop built-in command. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… interval - Nested slash commands: loop iterations now submit as string (not Part[]) so `/loop 5m /review` executes /review on each iteration. First iteration also goes through the callback+state drain path instead of submit_prompt, ensuring consistent behavior. - Day interval unit: parseInterval/formatInterval now support `d` (e.g., `/loop 1d check status`). MAX_INTERVAL_MS already covers 24h. - Natural language interval: `/loop check deploy every 5m` extracts the interval from the prompt tail (regex: `every <interval>$`). - Changed pendingLoopPrompt from ref to React state to trigger re-renders when the timer callback queues a prompt during Idle. - Added tests for day parsing/formatting (27 total). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6012fdd to
8d1d89b
Compare
…validation - Fix client-side slash command hang: after submitQuery resolves, if no streaming was started (e.g. /help, /clear), yield to React's render cycle then manually call onIterationComplete so the loop can advance to the next iteration. - Fix parseLoopArgs: use hasExplicitInterval flag instead of comparing against DEFAULT_INTERVAL_MS for trailing "every" parsing, so `/loop 10m X every 5m` doesn't silently override the explicit 10m. - Add config validation in LoopManager.start(): reject intervalMs outside MIN/MAX bounds and non-integer maxIterations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix drain/messageQueue race: drain effect now checks messageQueue.length > 0 and skips if user messages are pending, preventing submitQuery from early-returning and dropping the loop prompt. - Fix --max parsing: `/loop --max` or `/loop --max foo` now returns empty prompt to trigger usage error, instead of silently including "--max" in the prompt text. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- isValidTask: validate iteration is non-negative integer - Slot-busy path preserves resumeIteration instead of resetting to 0 - Settings read from correct path (merged.model.loopEnabled etc.) - Sanitize loopMaxConcurrent and loopExpiryDays with bounds checking - Doc: clarify file lock scope (restore only, not persistence writes) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Persist state after pause/resume (all 4 paths) to prevent stale file on crash - Docs: settings use model.loopEnabled prefix (both user doc and design doc) - Design doc: settings JSON example shows correct nested model section - resumeIteration semantics and PID 999999 test addressed in replies Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Error message uses model.loopEnabled (fully qualified setting path) - Doc: loopMaxConcurrent reference uses model. prefix - Doc: drift protection wording corrected (fires on wake, not guaranteed <1s) - isValidTask optional fields: safe defaults handle corruption (reply only) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Export parseLoopArgs for testing. Cover: - 6 subcommands (status/stop/pause/resume/restore/list) - Target ID and --all flag parsing - Leading interval (s/m/h/d) - --id flag (with/without interval, missing value) - --max flag (valid, missing, invalid, 0, negative) - --id + --max combined - Trailing "every" syntax (short units, full words, non-match) - Leading interval precedence over trailing every - Edge cases (whitespace, multi-space, subcommand-like prompt) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR vs Claude Code 功能对比功能矩阵
PR 独有(Claude Code 没有的 12 项)
Claude Code 独有(PR 没有的 3 项)
架构差异
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
All 4 clear-file call sites now go through persistLoopStates([]) which awaits any in-flight write before deleting the file, preventing a race where a concurrent write recreates loop-state.json after unlink. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Closing in favor of #2731Hi @wenshao — thank you for the thorough work on this PR. The implementation is well-engineered, with solid persistence, backoff, and multi-session coordination. However, after reviewing both approaches, we're going with #2731 and closing this one. Here's why: 1. Tool-based interface vs. slash-command-only 2. Integration breadth 3. Scope and surface area 4. Scheduling primitive: cron expressions vs. setTimeout Again, the engineering quality here is genuinely strong — the drift protection, FNV-1a jitter, and backoff logic are well thought out. Some of those ideas (particularly the backoff and persistence format) may be worth revisiting as enhancements to the merged approach. Thank you for the contribution. |

Summary
/loopslash command that periodically executes a prompt at a configurable intervalLoopManagerin core for framework-agnostic loop state managementCloses #2638
Features
/loop 5m check if CI passed/loop 30s --max 10 is server healthy?/loop status/loop stop10sto24h, default10mwaitingForResponseflag ensures normal user prompts don't interfere with loop schedulingTest plan
/loop 10s check date— verify iterations appear with headers/loop stopduring active loop — verify clean stop/loop status— verify status display🤖 Generated with Claude Code