qwen-code/docs/design
qqqys f4d405ca4a
feat(loop): wire prompt-only /loop to self-paced wakeups (#5197)
* feat(loop): add second-resolution session wakeup engine

Add a session-scoped wakeup primitive for self-paced /loop, aligned with
Claude Code's ScheduleWakeup. An independent, second-resolution channel in
CronScheduler — separate from cron jobs (never durable, not counted against
MAX_JOBS, fired at an exact time, not minute-rounded):

- scheduleWakeup(delaySeconds, prompt): clamps to [60, 3600]s (1200s default
  for non-finite input); returns {scheduledFor, clampedDelaySeconds, wasClamped}.
- Fires through the existing onFire channel and counts toward sessionSize, so
  there are no cli delivery-path changes and a pending wakeup holds a headless
  run open — re-arm keeps the loop alive, omitting the call ends it.
- cancelWakeup / cancelAllWakeups primitives (for loop-scoped cancellation).
- loop_wakeup tool: delaySeconds schema, structured clamp output, cache-window
  picking guidance, verbatim /loop prompt, reason shown to the user, and the
  "call to keep alive / omit to end" contract — all mirroring ScheduleWakeup.

getDefaultPermission stays 'ask' (out of SAFE_TOOL_ALLOWLIST) so AUTO still
routes scheduling future model input through the classifier, like CronCreate.

Closes #5156

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): tighten session wakeup lifecycle

* fix(loop): address wakeup review nits

* feat(loop): wire prompt-only /loop to self-paced wakeups

Make `/loop <prompt>` (no interval) a self-paced loop in the bundled loop
skill: run the prompt immediately, then schedule at most one future
continuation via loop_wakeup (delaySeconds) — no recurring cron.

- Three explicit paths: prompt-only self-paced (LoopWakeup), fixed-interval
  recurring (CronCreate), and list/clear management (CronList/CronDelete).
- The continuation uses delaySeconds (aligned with the second-resolution
  wakeup engine) and re-feeds `/loop ${original prompt}` verbatim to re-enter
  the skill; the model re-arms only when a further check is useful.
- Adds loop_wakeup to the skill's allowedTools.
- Static SKILL contract tests, including delaySeconds (not delayMinutes).

Closes #5184

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): clarify wakeup delay reporting

* fix(loop): make wakeups manageable

* fix(loop): address wakeup review feedback

* fix(loop): clarify wakeup management wording

* fix(loop): clarify wakeup continuation tooling

* fix(loop): bound self-paced wakeup chains

* fix(loop): distinguish wakeup fires in lists and UI

* test(loop): cover wakeup labels in cli paths

* fix(loop): enforce session wakeup chain limit

* fix(loop): align wakeup delay metadata

* fix(loop): handle stopped wakeup scheduling

* fix(loop): make the wakeup chain limit a true session-level budget

The 24h chain limit reset `wakeupChainStartedAt` whenever `wakeups`
emptied — on every fire and on cancel. Because a self-paced loop leaves
at most one pending wakeup, each fire emptied the map and restarted the
clock, so a continuous re-arming loop never reached the cap (and a
cancel-then-reschedule could reset it too).

Reset the chain clock only on stop()/destroy() (a new session): the 24h
budget now spans the whole session, bounds continuous re-arming, and
closes the cancel bypass. Tests cover the clock persisting across fires,
cancel not resetting it, and stop starting a fresh budget.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): correct CronList durability wording and bound wakeup prompt

Address two review suggestions on #5197:

- CronList tool description said cron jobs and loop wakeups are "both
  session-only and durable", implying wakeups can be durable. Loop
  wakeups are always session-only; only cron jobs can be durable.
  Reword so the model isn't misled into expecting durable wakeups.
- LoopWakeup `prompt` had no maxLength, unlike sibling tools
  (task-create, send-message). Add maxLength: 10000 to bound the
  model-generated continuation prompt.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): let the first wakeup arm before the scheduler starts

Critical (wenshao, #5197): LoopWakeup hard-rejected when
`scheduler.running` was false, but on the first self-paced /loop in a
session with no cron jobs the scheduler hasn't started yet
(#startCronSchedulerIfNeeded bails on !hasPendingWork). The post-prompt
hook starts the tick *after* the turn, once a wakeup exists — so the
guard rejected the very call that makes the loop possible, breaking the
primary use case.

The `running` check was a proxy for "cron is alive", added to reject
re-arms after the token-limit breaker. Replace it with an explicit,
permanent `disabled` state so the two cases are distinguishable:

- CronScheduler gains `disabled` + `disable()` (sets the flag, stops).
- LoopWakeup rejects only when `scheduler.disabled`, not when merely
  stopped — a stopped-but-restartable scheduler still accepts wakeups.
- The token-limit breaker calls `disable()` instead of `stop()`, so its
  rejection (the original intent) is preserved.

Also attribute cron-prompt errors by source: `[loop error]` vs
`[cron error]` (item.source was already in scope).

Tests: reject-when-disabled, schedule-when-stopped (the regression),
and a disable() unit test.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): clear the pending wakeup when a re-arm exceeds the 24h budget

Critical (round-4 review, #5197): scheduleWakeup() threw the 24h-limit
error *before* clearing the prior wakeup, so a rejected re-arm left the
previous wakeup in the map. Its fireAtMs is now in the past, so the next
tick fires it — one iteration past the budget it's meant to cap. A test
even codified this (sessionSize === 1 after a rejected re-arm).

Production can't actually reach it (the 1s tick fires the wakeup at
~+3600s, and a stopped scheduler clears wakeups), but the safety budget
should hold unconditionally. Clear the pending wakeup up front, before
the budget check, so a rejected re-arm leaves nothing behind. Update the
test to assert no wakeup remains.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): update Session token-limit test for the disable() breaker

CI regression from the disable() refactor (7f488e6c6): the token-limit
breaker now calls scheduler.disable() instead of stop(), but the
cron-fired token-limit test still asserted scheduler.stop and its mock
scheduler had no disable() — so the call hit an undefined method and the
stop spy saw 0 calls. Missed because the prior change ran core tests +
cli tsc but not the cli test suite (Session.test.ts is cli).

Add disable() to the mock and assert it's called once. The breaker
disables (permanent for the session, so a later LoopWakeup is rejected),
which internally stops too.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(loop): enforce disabled guard in scheduleWakeup, unexport SessionWakeup

Two review suggestions on #5197:

- scheduleWakeup() now rejects when the scheduler is disabled, enforcing
  the invariant at the layer that owns `_disabled`. A disabled scheduler
  never fires, so a wakeup scheduled past it would be a silent zombie.
  LoopWakeup still pre-checks `disabled` for a friendly message; this
  guards any other caller. (The CHANGES_REQUESTED "missing disable mock"
  critical was already resolved in 240e8892b — verified Session.test
  128/128.)
- SessionWakeup is only used inside cronScheduler.ts (private map +
  file-local wakeupToJob); drop the unused `export` to keep the public
  surface minimal.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): start the scheduler in finally so a post-arm error can't strand the loop

Review (doudouOUC, #5197): #startCronSchedulerIfNeeded() ran only in the
success path of prompt(), after #executePrompt() resolved. If a turn armed
a wakeup via LoopWakeup and then threw on a later step (e.g. an API error),
the call was skipped and the scheduler never started — the self-paced loop
died silently until the next successful user prompt.

Move it into the finally block. It's idempotent (start() no-ops when the
timer exists) and gated on hasPendingWork/disposed/disabled, so it only
starts when a wakeup or cron job is actually pending. Verified the existing
128 Session tests still pass (success path unchanged).

Also fix a test-type drift flagged in the same review: SessionInternals
typed cronQueue as string[], but the implementation switched it to
CronQueueItem objects ({ prompt, source }); type it accordingly and push a
proper object.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(loop): add zh/zh-TW display-name translations for LoopWakeup

After merging main, its i18n coverage test ("has a zh translation for
every core tool display name") failed on all 3 OS: the new LoopWakeup
tool had no toolDisplayName entry, so localizeToolDisplayName fell back
to the English name. Add 循环唤醒 / 循環喚醒, plus the en.js parity key
so check-i18n passes.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-18 18:36:24 +08:00
..
adaptive-output-token-escalation fix(core): remove unused debugResponses array and dead extractUsageFromGeminiClient (#4982) 2026-06-13 07:51:19 +08:00
auth refactor(cli): provider-first auth registry with unified install pipeline (#3864) 2026-05-08 12:19:28 +08:00
auto-memory feat(memory): managed auto-memory and auto-dream system (#3087) 2026-04-16 20:05:45 +08:00
channels docs(channels): consolidate design docs into single file 2026-04-02 11:17:37 +08:00
compact-mode feat: optimize compact mode UX — shortcuts, settings sync, and safety (#3100) 2026-04-16 09:29:24 +08:00
compaction-image-stripping feat(core): strip inline media before chat compaction summary (#4101) 2026-05-14 10:20:11 +08:00
customize-banner-area feat(cli): customize banner area (logo, title, hide) (#3710) 2026-05-07 10:17:53 +08:00
daemon-acp-http feat(core): durable cron jobs — /loop tasks that survive restarts (#5004) 2026-06-13 19:30:40 +08:00
daemon-transport-abstraction feat(serve): add daemon idle detection to GET /health?deep=true (#4934) 2026-06-18 06:55:03 +00:00
fork-subagent feat(core): implement fork subagent for context sharing (#2936) 2026-04-14 14:27:38 +08:00
prompt-suggestion fix(followup): prevent tool call UI leak and Enter accept buffer race (#2872) 2026-04-09 00:07:03 +08:00
rt-optimization feat(telemetry): foundation for skill-based RT optimization (P0+P1) (#4565) 2026-05-29 02:17:40 +08:00
session-idle-reaper feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
session-recap feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
session-title feat(session): auto-title sessions via fast model, add /rename --auto (#3540) 2026-04-23 20:37:05 +08:00
skill-nudge feat(memory): add autoSkill background project skill extraction (#3673) 2026-05-09 14:25:02 +08:00
slash-command feat(cli): improve slash command discovery (#3736) 2026-05-09 14:25:44 +08:00
structured-output docs: user + design docs for --json-schema structured output (#4051) 2026-05-17 23:10:34 +08:00
tool-use-summary fix(cli): add API Key option to qwen auth interactive menu (#3624) 2026-04-27 22:01:47 +08:00
virtual-viewport feat(cli): virtual viewport for long conversations on ink 7 (#4146) 2026-06-02 13:57:17 +08:00
2026-05-15-async-memory-recall-design.md fix(core): decouple auto-memory recall from main-agent request path (#4172) 2026-05-19 13:58:58 +08:00
auto-compaction-threshold-redesign.md feat(core)!: redesign auto-compaction thresholds with three-tier ladder (#4345) 2026-05-25 21:11:08 +08:00
custom-api-key-auth-wizard-prd.md docs(auth): add custom API key wizard PRD (#3583) 2026-05-13 14:04:41 +08:00
daemon-idle-detection-api.md feat(loop): wire prompt-only /loop to self-paced wakeups (#5197) 2026-06-18 18:36:24 +08:00
f2-mcp-transport-pool.md feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
markdown-syntax-extension.md feat(cli): expand TUI markdown rendering (#3680) 2026-05-07 16:24:13 +08:00
openrouter-auth-and-models.md refactor(cli): remove legacy qwen auth CLI subcommand, redirect to /auth TUI dialog (#3959) 2026-05-11 16:44:09 +08:00
telemetry-llm-request-timing-design.md feat(telemetry): Phase 4b — retry visibility for qwen-code.llm_request (#3731) (#4432) 2026-06-05 13:45:47 +08:00
telemetry-outbound-propagation-design.md feat(telemetry): client-side HTTP span + opt-in W3C traceparent propagation (#4384) (#4390) 2026-05-25 22:16:54 +08:00
telemetry-resource-attributes-design.md feat(telemetry): support custom resource attributes and add metric cardinality controls (#4367) 2026-05-21 13:54:37 +08:00
telemetry-subagent-spans-design.md feat(telemetry): Phase 3 — qwen-code.subagent span with concurrent isolation (#3731) (#4410) 2026-06-05 17:12:34 +08:00
workflow-tracing-gaps.md feat(telemetry): unify span creation paths for hierarchical trace tree (#4126) 2026-05-16 22:29:55 +08:00
worktree.md feat(worktree): Phase D — startup --worktree flag + symlinkDirectories + PR refs (#4381) 2026-05-27 17:04:51 +08:00