From 439a1a46e20ebcfa8dca5a3736dcfac53e240524 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Sun, 29 Mar 2026 02:25:28 +0000 Subject: [PATCH] feat(cron): make cron tools opt-in via experimental settings Change cron/loop tools from opt-out to opt-in. Cron tools are now disabled by default and can be enabled via: - settings.json: { "experimental": { "cron": true } } - Environment variable: QWEN_CODE_ENABLE_CRON=1 This ensures experimental features are explicitly enabled by users who want to try them. Co-authored-by: Qwen-Coder --- integration-tests/cron-tools.test.ts | 62 ++++++++++++------- packages/cli/src/config/config.ts | 1 + packages/cli/src/config/settingsSchema.ts | 15 ++++- packages/cli/src/nonInteractiveCli.test.ts | 2 +- packages/cli/src/nonInteractiveCli.ts | 2 +- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 2 +- packages/cli/src/ui/hooks/useGeminiStream.ts | 2 +- packages/core/src/config/config.ts | 11 +++- .../schemas/settings.schema.json | 10 ++- 9 files changed, 75 insertions(+), 32 deletions(-) diff --git a/integration-tests/cron-tools.test.ts b/integration-tests/cron-tools.test.ts index f62efba51..6a69ffa30 100644 --- a/integration-tests/cron-tools.test.ts +++ b/integration-tests/cron-tools.test.ts @@ -14,13 +14,15 @@ describe('cron-tools', () => { if (rig) { await rig.cleanup(); } - // Clean up env var if set by disable test - delete process.env['QWEN_CODE_DISABLE_CRON']; + // Clean up env vars + delete process.env['QWEN_CODE_ENABLE_CRON']; }); - it('should have cron tools registered', async () => { + it('should have cron tools registered when enabled via settings', async () => { rig = new TestRig(); - await rig.setup('cron-tools-registered'); + await rig.setup('cron-tools-registered', { + settings: { experimental: { cron: true } }, + }); 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".', @@ -30,9 +32,37 @@ describe('cron-tools', () => { expect(result.toLowerCase()).toContain('yes'); }); + it('should have cron tools registered when enabled via env var', async () => { + rig = new TestRig(); + await rig.setup('cron-tools-env-var'); + + process.env['QWEN_CODE_ENABLE_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".', + ); + + validateModelOutput(result, null, 'cron tools via env var'); + expect(result.toLowerCase()).toContain('yes'); + }); + + it('should NOT have cron tools by default', async () => { + rig = new TestRig(); + await rig.setup('cron-tools-disabled-by-default'); + + const result = await rig.run( + 'Do you have access to a tool called cron_create? Reply with just "yes" or "no".', + ); + + validateModelOutput(result, null, 'cron disabled by default'); + expect(result.toLowerCase()).toContain('no'); + }); + it('should create, list, and delete a cron job in a single turn', async () => { rig = new TestRig(); - await rig.setup('cron-create-list-delete'); + await rig.setup('cron-create-list-delete', { + settings: { experimental: { cron: true } }, + }); const result = await rig.run( 'Call cron_create with cron_expression "*/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.', @@ -59,7 +89,9 @@ describe('cron-tools', () => { it('should create a one-shot (non-recurring) job', async () => { rig = new TestRig(); - await rig.setup('cron-one-shot'); + await rig.setup('cron-one-shot', { + settings: { experimental: { cron: true } }, + }); const result = await rig.run( 'Do these steps: (1) Call cron_create with cron_expression "*/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".', @@ -81,23 +113,11 @@ describe('cron-tools', () => { validateModelOutput(result, 'one-shot', 'cron one-shot'); }); - it('should not have cron tools when QWEN_CODE_DISABLE_CRON=1', async () => { - rig = new TestRig(); - await rig.setup('cron-disable-flag'); - - process.env['QWEN_CODE_DISABLE_CRON'] = '1'; - - const result = await rig.run( - 'Do you have access to a tool called cron_create? Reply with just "yes" or "no".', - ); - - validateModelOutput(result, null, 'cron disable flag'); - expect(result.toLowerCase()).toContain('no'); - }); - it('should exit normally in -p mode when no cron jobs are created', async () => { rig = new TestRig(); - await rig.setup('cron-no-jobs-exit'); + await rig.setup('cron-no-jobs-exit', { + settings: { experimental: { cron: true } }, + }); // A normal -p call without cron should still exit quickly const result = await rig.run('What is 2+2? Reply with just the number.'); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index fcc33f76a..097efa24d 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -1089,6 +1089,7 @@ export async function loadCliConfig( maxSessionTurns: argv.maxSessionTurns ?? settings.model?.maxSessionTurns ?? -1, experimentalZedIntegration: argv.acp || argv.experimentalAcp || false, + cronEnabled: settings.experimental?.cron ?? false, listExtensions: argv.listExtensions || false, overrideExtensions: overrideExtensions || argv.extensions, noBrowser: !!process.env['NO_BROWSER'], diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index d2cf5081c..6006b3add 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1594,9 +1594,20 @@ const SETTINGS_SCHEMA = { category: 'Experimental', requiresRestart: true, default: {}, - description: 'Setting to enable experimental features', + description: 'Settings to enable experimental features.', showInDialog: false, - properties: {}, + properties: { + cron: { + type: 'boolean', + label: 'Enable Cron/Loop Tools', + category: 'Experimental', + requiresRestart: true, + default: false, + 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.', + showInDialog: true, + }, + }, }, } as const satisfies SettingsSchema; diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts index df9afa9ea..8bd34ca22 100644 --- a/packages/cli/src/nonInteractiveCli.test.ts +++ b/packages/cli/src/nonInteractiveCli.test.ts @@ -144,7 +144,7 @@ describe('runNonInteractive', () => { }), getExperimentalZedIntegration: vi.fn().mockReturnValue(false), isInteractive: vi.fn().mockReturnValue(false), - isCronDisabled: vi.fn().mockReturnValue(true), + isCronEnabled: vi.fn().mockReturnValue(false), getCronScheduler: vi.fn().mockReturnValue(null), } as unknown as Config; diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 69f0f1fbf..8c0b7c28d 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -372,7 +372,7 @@ export async function runNonInteractive( currentMessages = [{ role: 'user', parts: toolResponseParts }]; } else { // No more tool calls — check if cron jobs are keeping us alive - const scheduler = config.isCronDisabled() + const scheduler = !config.isCronEnabled() ? null : config.getCronScheduler(); if (scheduler && scheduler.size > 0) { diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 94c868c38..29d0c4690 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -204,7 +204,7 @@ describe('useGeminiStream', () => { .mockReturnValue(contentGeneratorConfig), getMaxSessionTurns: vi.fn(() => 50), getArenaAgentClient: vi.fn(() => null), - isCronDisabled: vi.fn(() => true), + isCronEnabled: vi.fn(() => false), getCronScheduler: vi.fn(() => null), } as unknown as Config; mockOnDebugMessage = vi.fn(); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 8e3388b3a..9b4b7553c 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -1643,7 +1643,7 @@ export const useGeminiStream = ( // Start the scheduler on mount, stop on unmount useEffect(() => { - if (config.isCronDisabled()) return; + if (!config.isCronEnabled()) return; const scheduler = config.getCronScheduler(); scheduler.start((job: { prompt: string }) => { cronQueueRef.current.push(job.prompt); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 007b22ec2..d9867310c 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -370,6 +370,7 @@ export interface ConfigParameters { maxSessionTurns?: number; sessionTokenLimit?: number; experimentalZedIntegration?: boolean; + cronEnabled?: boolean; listExtensions?: boolean; overrideExtensions?: string[]; allowedMcpServers?: string[]; @@ -557,6 +558,7 @@ export class Config { private readonly cliVersion?: string; private readonly experimentalZedIntegration: boolean = false; + private readonly cronEnabled: boolean = false; private readonly chatRecordingEnabled: boolean; private readonly loadMemoryFromIncludeDirectories: boolean = false; private readonly importFormat: 'tree' | 'flat'; @@ -680,6 +682,7 @@ export class Config { this.sessionTokenLimit = params.sessionTokenLimit ?? -1; this.experimentalZedIntegration = params.experimentalZedIntegration ?? false; + this.cronEnabled = params.cronEnabled ?? false; this.listExtensions = params.listExtensions ?? false; this.overrideExtensions = params.overrideExtensions; this.noBrowser = params.noBrowser ?? false; @@ -1687,8 +1690,10 @@ export class Config { return this.cronScheduler; } - isCronDisabled(): boolean { - return process.env['QWEN_CODE_DISABLE_CRON'] === '1'; + isCronEnabled(): boolean { + // Cron is experimental and opt-in: enabled via settings or env var + if (process.env['QWEN_CODE_ENABLE_CRON'] === '1') return true; + return this.cronEnabled; } getEnableRecursiveFileSearch(): boolean { @@ -2211,7 +2216,7 @@ export class Config { } // Register cron tools unless disabled - if (!this.isCronDisabled()) { + if (this.isCronEnabled()) { await registerCoreTool(CronCreateTool, this); await registerCoreTool(CronListTool, this); await registerCoreTool(CronDeleteTool, this); diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index c7f53048e..b540147b4 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -1445,9 +1445,15 @@ } }, "experimental": { - "description": "Setting to enable experimental features", + "description": "Settings to enable experimental features.", "type": "object", - "properties": {} + "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.", + "type": "boolean", + "default": false + } + } }, "$version": { "type": "number",