feat(cron): add in-session loop scheduling with cron tools

Add session-scoped recurring jobs that fire while you work. Jobs live
inside the current Qwen Code process and are gone when you exit.

New tools:
- cron_create: schedule a prompt to run on a cron expression
- cron_list: list active cron jobs
- cron_delete: cancel a scheduled job

Components:
- CronScheduler service for in-process job management
- cronParser utility for 5-field cron expressions
- /loop skill for natural language scheduling
- Non-interactive mode integration to keep process alive

Constraints:
- Max 50 jobs per session
- 3-day expiry for recurring jobs
- Jitter to prevent thundering herd
- No catch-up for missed fire times

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-28 14:37:29 +00:00
parent 070ec5b43e
commit aa4939111c
17 changed files with 1395 additions and 3 deletions

View file

@ -41,6 +41,7 @@ import {
type FileEncodingType,
} from '../services/fileSystemService.js';
import { GitService } from '../services/gitService.js';
import { CronScheduler } from '../services/cronScheduler.js';
// Tools
import { AskUserQuestionTool } from '../tools/askUserQuestion.js';
@ -63,6 +64,9 @@ import { WebFetchTool } from '../tools/web-fetch.js';
import { WebSearchTool } from '../tools/web-search/index.js';
import { WriteFileTool } from '../tools/write-file.js';
import { LspTool } from '../tools/lsp.js';
import { CronCreateTool } from '../tools/cron-create.js';
import { CronListTool } from '../tools/cron-list.js';
import { CronDeleteTool } from '../tools/cron-delete.js';
import type { LspClient } from '../lsp/types.js';
// Other modules
@ -525,6 +529,7 @@ export class Config {
private readonly usageStatisticsEnabled: boolean;
private geminiClient!: GeminiClient;
private baseLlmClient!: BaseLlmClient;
private cronScheduler: CronScheduler | null = null;
private readonly fileFiltering: {
respectGitIgnore: boolean;
respectQwenIgnore: boolean;
@ -1675,6 +1680,17 @@ export class Config {
return this.geminiClient;
}
getCronScheduler(): CronScheduler {
if (!this.cronScheduler) {
this.cronScheduler = new CronScheduler();
}
return this.cronScheduler;
}
isCronDisabled(): boolean {
return process.env['QWEN_CODE_DISABLE_CRON'] === '1';
}
getEnableRecursiveFileSearch(): boolean {
return this.fileFiltering.enableRecursiveFileSearch;
}
@ -2194,6 +2210,13 @@ export class Config {
await registerCoreTool(LspTool, this);
}
// Register cron tools unless disabled
if (!this.isCronDisabled()) {
await registerCoreTool(CronCreateTool, this);
await registerCoreTool(CronListTool, this);
await registerCoreTool(CronDeleteTool, this);
}
if (!options?.skipDiscovery) {
await registry.discoverAllTools();
}