qwen-code/docs/users/features
Shaojin Wen 74c67c9e94
feat(core): gate scheduled tasks on a shell command run outside the model (#15069)
* feat(core): gate scheduled tasks on a shell command run outside the model

A scheduled task can carry a `gate`: a shell command the scheduler runs
at each fire before the prompt reaches the session. Silence skips the
fire with no model turn; output delivers the fire with the output
attached as data; a failure is reported once per streak and counted.

The gate runs inside CronScheduler so every fire kind and every consumer
(TUI, ACP, headless, daemon-bound sessions) shares one path. It is
confirmed like a Monitor command when the task is created and checked
again against the session's approval mode and permission rules at fire
time. Durable tasks persist the gate together with a downgrade fuse in
the legacy `condition` field, so an older binary withholds the task
instead of firing it unconditionally every slot.

Also routes cron fires away from the composer's shell mode in the TUI.

Design: docs/design/2026-10-21-scheduled-task-gate.md
Refs #15056

* fix(core): normalize the gate rule hint and refuse a gate on a one-shot task

The rule a blocked fire suggests is now derived from the same normalized
command the cron_create dialog offers and the permission manager matches,
so a wrapper-style gate such as `bash -c "./probe.sh"` names
`Monitor(./probe.sh *)` rather than a `bash *` rule that never matches.

A one-shot task has no next slot, so a quiet gate would consume it with
nothing delivered and no trace; cron_create now refuses `gate` with
`recurring: false` and says why, and the design records the rule.

* fix(core): close the gate's permission, cancellation and lifetime gaps

Review of the gate found seven ways a gated task could run, keep running or
stall when it should not. All seven are closed here.

- An explicit `Monitor(...)`/`Bash(...)` deny rule now blocks the gate in
  every approval mode: the runner evaluates the command before the yolo/auto
  shortcut, the way an ordinary tool call is denied before the mode approves
  it, so a persisted gate stops the moment the rule is saved.
- In auto mode a bare `cron_create` allow rule takes the allow fast path
  around the classifier, so nobody looked at the command. `cron_create` now
  detects that case (a tool-only permission lookup answering `allow`) and
  holds the gate to the default-mode check: a `Monitor(...)`/`Bash(...)` rule
  or the read-only command, else the call is refused.
- A manual run of a gated task has no scheduler in the loop and used to
  dispatch the raw prompt. The daemon's per-run `/run` and the Web Shell's
  "Run now" now send a manual-run prompt that asks the model to run the gate
  itself first and stop when it prints nothing; the task view carries `gate`
  and `gateStatus` so the client can do that for a tool-created task.
- A gate's output goes to the consumer installed when it settles, not when
  it started: ACP re-installs its callback through `start()` after every
  turn, and that used to drop a gate that was mid-run. Only `stop()` ends
  the subscription, and it aborts the gate.
- A tasks-file reload that drops, disables or rewrites a job now aborts its
  running gate, as `delete()` does, instead of letting the command finish
  and discarding the result.
- A quiet final gate no longer pins a headless run: `sessionSize` counts a
  session-only one-shot or final fire until its gate settles, and a new
  `setGateSkippedListener` reports settlements that deliver nothing so the
  hold-open is re-checked.
- A gated catch-up persists its `lastFiredAt` stamp at once but books its run
  only when the gate delivers, as a `catch-up` run, instead of recording a
  run that reached no session and a second one on delivery.

Tests cover each case (runner, tool, scheduler, headless run, daemon route,
run prompt, Web Shell dialog); the design docs and the user guide describe
the new rules.

* fix(core): match the gate's auto-mode lookup to the permission flow and tighten the manual-run text

- In auto mode `cron_create` asked the permission manager about the tool
  name alone to tell whether an allow rule had skipped the classifier. A
  rule scoped to the call's parameters (`cron_create(prompt:…)`) takes the
  same fast path but matched only the real flow's context, so the gate was
  accepted unreviewed. The lookup now evaluates the same context the flow
  built for this call (`buildPermissionCheckContext` with the tool params),
  pinned by a test with a real permission manager.
- The manual-run prompt told the model to follow the task whenever the gate
  printed something, so partial output before a failure counted. It now
  follows the task only on exit 0 with output, reports the gate printed
  nothing on a silent exit 0, and reports the failure and stops on a
  non-zero exit, a timeout or a cancellation — the runner's three outcomes.
  The Web Shell copy, the tests and the docs say the same.

* fix(core): refuse a denied gate at creation and show the gate exactly as it runs

`cron_create` now evaluates the gate as a `monitor` command before any
other check, so an explicit `Monitor(...)`/`Bash(...)` deny refuses the
call in every mode, a confirmed dialog included. The runner already
checks that deny first at each fire, so such a task could only ever be
blocked.

The exec dialog showed Monitor's re-spelled spawn command while the
scheduler persists and runs the trimmed gate. They differed for a gate
whose wrapper script ends in `&` (`bash -c './probe.sh &'`), which the
background check let through: the dialog showed `bash -c './probe.sh'`
and every fire would have detached the probe. That gate is now refused
at validation, and the dialog shows the persisted string itself.

The design doc's approval section said the tool's default permission
followed Monitor's read-only check; it is `ask` unconditionally.

* fix(core): hold headless runs for running gates and close the wrapper-& gap

- Headless: the hold-open also waits for a gate still running, durable
  ones included. A durable catch-up flushed by start() is stamped before
  its gate decides, so stopping the scheduler there aborted the gate and
  lost the slot (`CronScheduler.hasGatesInFlight`).
- Validation: `backgroundsAnyPart` looks for a shell wrapper behind any
  prefix the Monitor parser does not unwrap (`nohup`, `sudo -u ci`,
  `command`, `env FOO=1`) and checks its script, nested scripts too, so
  `nohup bash -c './probe.sh &'` is refused like `bash -c './probe.sh &'`.
- Runner: the 16 KiB cap cuts on a character boundary instead of leaving
  a lone lead byte that decodes to U+FFFD; binary output stops the
  command and fails the fire instead of settling quiet with nothing kept.
- Web Shell: pin the manual-run prompt to the same literal as the daemon
  copy, so the two cannot drift apart; drop the test-only export.

* fix(core): re-check headless completion after a cancelled gate

A delete or disk reload that cancels the last running gate left the
headless run waiting: the cancelled settlement returned before reaching
the gate-skipped listener, so nothing re-checked the hold-open that
hasGatesInFlight() had kept open. The settlement now reports to the
listener (still delivering nothing) unless stop() cancelled it.
2026-10-22 20:21:10 +08:00
..
channels feat(channels): add WeCom application callback support (#13976) 2026-10-20 19:41:24 +08:00
_meta.ts feat(memory): bundle Mem0 with the main CLI (#12891) 2026-09-30 07:57:11 +00:00
agent-board.md docs(board): clarify suggested task owner semantics (#14967) 2026-10-21 10:37:10 +08:00
approval-mode.md fix(permissions): escalate repeated destructive-command denials to manual approval (#13636) 2026-10-10 14:42:00 +00:00
arena.md docs(arena): clarify unset run limits (#9860) 2026-09-06 07:26:53 +00:00
auto-mode.md fix(permissions): escalate repeated destructive-command denials to manual approval (#13636) 2026-10-10 14:42:00 +00:00
batch.md fix(cli): unwedge qwen batch after provider anomalies; bound maxCostUsd by the worst case (#12895) 2026-09-28 10:01:38 +00:00
browser-use.md docs(browser-use): point users at the Chrome Web Store listing (#12510) 2026-09-23 03:13:28 +00:00
code-review.md feat(review): capture-tui — rendering claims get pixels, not prose (#9273) 2026-10-04 13:23:59 +00:00
commands.md fix(cli): preserve Unicode queries in at completion (#13866) 2026-10-14 14:02:55 +00:00
computer-use.md feat(cua): streamline computer use and port native reliability fixes (#14846) 2026-10-21 14:03:31 +08:00
context-cost.md feat(core): defer agent and goal declarations by default (#13033) 2026-10-06 04:13:24 +00:00
cross-session-protocol.md feat(sdk): add a peer endpoint so a program outside Qwen Code can join cross-session messaging (#11560) 2026-09-10 22:13:39 +00:00
dual-output.md fix(cli): bound headless tool result content (#9012) 2026-08-12 16:22:36 +00:00
followup-suggestions.md test(lockfile): pin the Playwright parity check's first manifest and both nested directions (#11716) 2026-09-12 14:53:37 +00:00
goals.md refactor(goal): retire recovery of Goals recorded before Goal state was journaled (#12155) 2026-09-18 09:30:49 +00:00
headless.md docs: clarify safe mode tool schemas for small contexts (#14445) 2026-10-17 19:35:06 +08:00
hooks.md fix(hooks): gate headless file and skill prompts before expansion (#14356) 2026-10-16 09:37:25 +00:00
language.md refactor(cli): revert dynamic slash command LLM translation (#4145) 2026-05-15 16:01:16 +08:00
lsp.md fix(lsp): expose status and startup diagnostics (#3649) 2026-05-17 01:42:28 +08:00
markdown-rendering.md docs: document inline terminal image previews (#8656) 2026-08-21 07:24:20 +00:00
mcp.md fix(mcp): Support larger Apps, scoped tool calls and isolated origins (#12258) 2026-09-27 04:56:31 +00:00
mem0.md feat(memory): bundle Mem0 with the main CLI (#12891) 2026-09-30 07:57:11 +00:00
memory.md fix(memory): preserve complete index entries in prompts (#13315) 2026-10-05 14:30:06 +00:00
multi-agent-coordination.md feat(cli): add native multi-agent coordination (#8804) 2026-08-12 17:39:03 +00:00
omni-fixed-policies-preset.json feat(omni): integrate multimodal media pipeline into main (#12019) 2026-09-16 12:15:45 +00:00
omni-media-policies.md feat(omni): integrate multimodal media pipeline into main (#12019) 2026-09-16 12:15:45 +00:00
output-styles.md feat: load custom output styles from ~/.qwen/output-styles and .qwen/output-styles (#10761) 2026-09-04 15:47:08 +00:00
rules.md feat(core,docs): add conditional extension rules and cap context warnings (#12034) 2026-09-20 11:20:54 +00:00
sandbox.md fix(cli): Harden settings failures and sandbox command streams (#13140) 2026-10-03 12:02:48 +00:00
scheduled-tasks.md feat(core): gate scheduled tasks on a shell command run outside the model (#15069) 2026-10-22 20:21:10 +08:00
skills.md docs(skills): document .agents/skills and skills.directories (#15086) 2026-10-22 10:35:29 +08:00
status-line.md fix(cli): prevent dialog clipping in short terminals (#9040) 2026-09-13 07:58:04 +00:00
structured-output.md docs: user + design docs for --json-schema structured output (#4051) 2026-05-17 23:10:34 +08:00
sub-agents.md fix(core): isolate declarative subagent hooks by invocation (#12949) 2026-09-29 07:21:32 +00:00
terminal-images.md docs: document inline terminal image previews (#8656) 2026-08-21 07:24:20 +00:00
tips.md feat: add contextual tips system with post-response context awareness (#2904) 2026-04-13 17:40:27 +08:00
token-caching.md feat(core,docs): add conditional extension rules and cap context warnings (#12034) 2026-09-20 11:20:54 +00:00
tool-use-summaries.md refactor(core,cli): rename generic Gemini identifiers to Llm (#10124) 2026-08-28 03:05:49 +00:00
worktree.md fix(core): reap stale worktrees holding only symlinks or nested build output (#12785) 2026-09-28 10:07:37 +00:00