feat(core): enable loop/cron tools by default (#4950)

Graduate cron/loop from experimental opt-in to enabled-by-default.
Flip env var polarity from QWEN_CODE_ENABLE_CRON to
QWEN_CODE_DISABLE_CRON for users who want to opt out.
Update integration tests, docs, and VS Code schema accordingly.
This commit is contained in:
tanzhenxin 2026-06-10 23:21:53 +08:00 committed by GitHub
parent f479f7a660
commit 9fc7b07602
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 39 additions and 51 deletions

View file

@ -6,7 +6,7 @@ Scheduled tasks let Qwen Code re-run a prompt automatically on an interval. Use
Tasks are session-scoped: they live in the current Qwen Code process and are gone when you exit. Nothing is written to disk.
> **Note:** Scheduled tasks are an experimental feature. Enable them with `experimental.cron: true` in your [settings](../configuration/settings.md), or set `QWEN_CODE_ENABLE_CRON=1` in your environment.
> **Tip:** Scheduled tasks are enabled by default. To disable them, set `experimental.cron: false` in your [settings](../configuration/settings.md), or set `QWEN_CODE_DISABLE_CRON=1` in your environment.
## Schedule a recurring prompt with /loop

View file

@ -87,7 +87,6 @@ function setupAcpCronTest(rig: TestRig) {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
QWEN_CODE_ENABLE_CRON: '1',
},
},
);
@ -302,9 +301,7 @@ async function initSession(
'cron job fires and streams results via sessionUpdate after prompt returns',
async () => {
const rig = new TestRig();
rig.setup('acp-cron-e2e', {
settings: { experimental: { cron: true } },
});
rig.setup('acp-cron-e2e');
const {
sendRequest,

View file

@ -22,15 +22,12 @@ describe('cron-tools', () => {
if (rig) {
await rig.cleanup();
}
// Clean up env vars
delete process.env['QWEN_CODE_ENABLE_CRON'];
delete process.env['QWEN_CODE_DISABLE_CRON'];
});
it('should have cron tools registered when enabled via settings', async () => {
it('should have cron tools registered by default', async () => {
rig = new TestRig();
await rig.setup('cron-tools-registered', {
settings: { experimental: { cron: true } },
});
await rig.setup('cron-tools-registered');
const result = await rig.run(
'Do you have access to tools called cron_create, cron_list, and cron_delete? Reply with just "yes" or "no".',
@ -43,31 +40,37 @@ describe('cron-tools', () => {
// Env vars set in the test process are not forwarded into Docker containers,
// so this test cannot pass in sandbox mode.
(IS_SANDBOX ? it.skip : it)(
'should have cron tools registered when enabled via env var',
'should NOT have cron tools when disabled via env var',
async () => {
rig = new TestRig();
await rig.setup('cron-tools-env-var');
await rig.setup('cron-tools-disabled-env-var');
process.env['QWEN_CODE_ENABLE_CRON'] = '1';
process.env['QWEN_CODE_DISABLE_CRON'] = '1';
const result = await rig.run(
'Do you have access to tools called cron_create, cron_list, and cron_delete? Reply with just "yes" or "no".',
'Try to create a cron job with cron_create using cron "*/5 * * * *", prompt "disabled test", and recurring true. If you cannot call that tool, say so briefly.',
);
validateModelOutput(result, null, 'cron tools via env var');
expect(result.toLowerCase()).toContain('yes');
validateModelOutput(result, null, 'cron disabled via env var');
const toolLogs = rig.readToolLogs();
expect(
toolLogs.some((log) => log.toolRequest.name === 'cron_create'),
'cron_create should not be callable when cron is disabled',
).toBe(false);
},
);
it('should NOT have cron tools by default', async () => {
it('should NOT have cron tools when disabled via settings', async () => {
rig = new TestRig();
await rig.setup('cron-tools-disabled-by-default');
await rig.setup('cron-tools-disabled-via-settings', {
settings: { experimental: { cron: false } },
});
const result = await rig.run(
'Try to create a cron job with cron_create using cron "*/5 * * * *", prompt "disabled test", and recurring true. If you cannot call that tool, say so briefly.',
);
validateModelOutput(result, null, 'cron disabled by default');
validateModelOutput(result, null, 'cron disabled via settings');
const toolLogs = rig.readToolLogs();
expect(
toolLogs.some((log) => log.toolRequest.name === 'cron_create'),
@ -77,9 +80,7 @@ describe('cron-tools', () => {
it('should create, list, and delete a cron job in a single turn', async () => {
rig = new TestRig();
await rig.setup('cron-create-list-delete', {
settings: { experimental: { cron: true } },
});
await rig.setup('cron-create-list-delete');
const result = await rig.run(
'Call cron_create with cron "*/5 * * * *", prompt "test ping", recurring true. Then call cron_list. Then delete that job using cron_delete. Then call cron_list again. How many jobs remain? Reply with just the number.',
@ -106,9 +107,7 @@ describe('cron-tools', () => {
it('should create a one-shot (non-recurring) job', async () => {
rig = new TestRig();
await rig.setup('cron-one-shot', {
settings: { experimental: { cron: true } },
});
await rig.setup('cron-one-shot');
const result = await rig.run(
'Do these steps: (1) Call cron_create with cron "*/5 * * * *", prompt "one-shot test", recurring false. (2) Call cron_list. Is the job marked as recurring or one-shot? Remember the answer. (3) Delete all cron jobs. Reply with just "recurring" or "one-shot".',
@ -132,9 +131,7 @@ describe('cron-tools', () => {
it('should exit normally in -p mode when no cron jobs are created', async () => {
rig = new TestRig();
await rig.setup('cron-no-jobs-exit', {
settings: { experimental: { cron: true } },
});
await rig.setup('cron-no-jobs-exit');
// A normal -p call without cron should still exit quickly
const result = await rig.run('What is 2+2? Reply with just the number.');

View file

@ -13,8 +13,8 @@
* invoke them in the same session.
*
* Cron tools (cron_create, cron_list, cron_delete) are convenient deferred
* targets: deterministic, side-effect-free in -p mode, and gated behind the
* `experimental.cron` setting so we control when they're registered.
* targets: deterministic, side-effect-free in -p mode, and enabled by default
* (can be disabled via `experimental.cron: false`).
*/
import { describe, it, expect, afterEach } from 'vitest';
@ -33,9 +33,7 @@ describe('tool-search / deferred tools', () => {
it('reveals a deferred tool via select: and lets the model invoke it', async () => {
rig = new TestRig();
await rig.setup('tool-search-select-then-invoke', {
settings: { experimental: { cron: true } },
});
await rig.setup('tool-search-select-then-invoke');
// Force the model down the select: path so the assertion isn't dependent
// on whether the model spontaneously chose keyword search vs. select.
@ -74,9 +72,7 @@ describe('tool-search / deferred tools', () => {
it('finds deferred tools via keyword search', async () => {
rig = new TestRig();
await rig.setup('tool-search-keyword', {
settings: { experimental: { cron: true } },
});
await rig.setup('tool-search-keyword');
// The tool_search response is a synthetic <functions>...</functions>
// block; we check the ARGS the model sent (a keyword query, not select:)
@ -116,9 +112,9 @@ describe('tool-search / deferred tools', () => {
it('does not register deferred tools when their feature flag is off', async () => {
rig = new TestRig();
// No experimental.cron setting → cron_* tools must not be registered at
// all (deferred or otherwise). tool_search has nothing to surface.
await rig.setup('tool-search-no-cron');
await rig.setup('tool-search-no-cron', {
settings: { experimental: { cron: false } },
});
const result = await rig.run(
'Call tool_search with query "select:cron_list". ' +

View file

@ -26,7 +26,6 @@ function makeEnv(): NodeJS.ProcessEnv {
delete env['NO_COLOR'];
return {
...env,
QWEN_CODE_ENABLE_CRON: '1',
FORCE_COLOR: '1',
TERM: 'xterm-256color',
NODE_NO_WARNINGS: '1',

View file

@ -76,7 +76,7 @@ export class InteractiveSession {
* @example
* ```ts
* const session = await InteractiveSession.start({
* env: { QWEN_CODE_ENABLE_CRON: '1' },
* env: { QWEN_CODE_DISABLE_CRON: '1' },
* args: ['--approval-mode', 'yolo'],
* });
* ```

View file

@ -1876,7 +1876,7 @@ export async function loadCliConfig(
maxWallTimeSeconds: resolveMaxWallTimeSeconds(argv, settings),
maxToolCalls: resolveMaxToolCalls(argv, settings),
experimentalZedIntegration: argv.acp || argv.experimentalAcp || false,
cronEnabled: settings.experimental?.cron ?? false,
cronEnabled: settings.experimental?.cron ?? true,
computerUseEnabled: settings.tools?.computerUse?.enabled ?? true,
emitToolUseSummaries: settings.experimental?.emitToolUseSummaries ?? true,
listExtensions: argv.listExtensions || false,

View file

@ -2501,9 +2501,9 @@ const SETTINGS_SCHEMA = {
label: 'Enable Cron/Loop Tools',
category: 'Experimental',
requiresRestart: true,
default: false,
default: true,
description:
'Enable in-session cron/loop tools (experimental). When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can also be enabled via QWEN_CODE_ENABLE_CRON=1 environment variable.',
'Enable in-session cron/loop tools. When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can be disabled via QWEN_CODE_DISABLE_CRON=1 environment variable.',
showInDialog: true,
},
emitToolUseSummaries: {

View file

@ -1143,7 +1143,7 @@ export class Config {
private readonly cliVersion?: string;
private runtimeStatusEnabled = false;
private readonly experimentalZedIntegration: boolean = false;
private readonly cronEnabled: boolean = false;
private readonly cronEnabled: boolean = true;
private readonly forkSubagentEnabled: boolean = false;
private workflowsEnabled = false;
private readonly computerUseEnabled: boolean = true;
@ -1328,7 +1328,7 @@ export class Config {
this.sessionTokenLimit = params.sessionTokenLimit ?? -1;
this.experimentalZedIntegration =
params.experimentalZedIntegration ?? false;
this.cronEnabled = params.cronEnabled ?? false;
this.cronEnabled = params.cronEnabled ?? true;
this.forkSubagentEnabled = params.forkSubagentEnabled ?? false;
this.workflowsEnabled = params.workflowsEnabled ?? false;
this.computerUseEnabled = params.computerUseEnabled ?? true;
@ -3312,8 +3312,7 @@ export class Config {
}
isCronEnabled(): boolean {
// Cron is experimental and opt-in: enabled via settings or env var
if (process.env['QWEN_CODE_ENABLE_CRON'] === '1') return true;
if (process.env['QWEN_CODE_DISABLE_CRON'] === '1') return false;
return this.cronEnabled;
}

View file

@ -2534,9 +2534,9 @@
"type": "object",
"properties": {
"cron": {
"description": "Enable in-session cron/loop tools (experimental). When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can also be enabled via QWEN_CODE_ENABLE_CRON=1 environment variable.",
"description": "Enable in-session cron/loop tools. When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can be disabled via QWEN_CODE_DISABLE_CRON=1 environment variable.",
"type": "boolean",
"default": false
"default": true
},
"emitToolUseSummaries": {
"description": "Generate a short LLM-based label after each tool batch completes. In compact mode the label replaces the generic `Tool × N` header; in full mode it appears as a dim `● <label>` line below the tool group. Requires a fast model to be configured; runs in parallel with the next API call so latency is hidden. Currently affects interactive CLI rendering only — SDK / non-interactive emission of the `tool_use_summary` message is not yet wired (the message factory is exported for a follow-up PR). Can be overridden with QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0 or =1.",