mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 15:31:27 +00:00
feat(cron): add distinct Cron message type and exit summary
- Introduce SendMessageType.Cron to differentiate cron-triggered prompts from user queries - Skip UserPromptSubmit hook for cron messages - Add getExitSummary() to display active loops when session ends - Add tests for exit summary functionality This improves cron loop handling by treating scheduled prompts differently from user-initiated queries and provides better UX when sessions end with active loops running. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
314a9ded78
commit
aa454a5a72
5 changed files with 94 additions and 7 deletions
|
|
@ -92,6 +92,8 @@ export enum SendMessageType {
|
|||
ToolResult = 'toolResult',
|
||||
Retry = 'retry',
|
||||
Hook = 'hook',
|
||||
/** Cron-fired prompt. Behaves like UserQuery but skips UserPromptSubmit hook. */
|
||||
Cron = 'cron',
|
||||
}
|
||||
|
||||
export interface SendMessageOptions {
|
||||
|
|
@ -465,7 +467,12 @@ export class GeminiClient {
|
|||
// Fire UserPromptSubmit hook through MessageBus (only if hooks are enabled)
|
||||
const hooksEnabled = this.config.getEnableHooks();
|
||||
const messageBus = this.config.getMessageBus();
|
||||
if (messageType !== SendMessageType.Retry && hooksEnabled && messageBus) {
|
||||
if (
|
||||
messageType !== SendMessageType.Retry &&
|
||||
messageType !== SendMessageType.Cron &&
|
||||
hooksEnabled &&
|
||||
messageBus
|
||||
) {
|
||||
const promptText = partToString(request);
|
||||
const response = await messageBus.request<
|
||||
HookExecutionRequest,
|
||||
|
|
@ -507,7 +514,10 @@ export class GeminiClient {
|
|||
}
|
||||
}
|
||||
|
||||
if (messageType === SendMessageType.UserQuery) {
|
||||
if (
|
||||
messageType === SendMessageType.UserQuery ||
|
||||
messageType === SendMessageType.Cron
|
||||
) {
|
||||
this.loopDetector.reset(prompt_id);
|
||||
this.lastPromptId = prompt_id;
|
||||
|
||||
|
|
@ -605,7 +615,10 @@ export class GeminiClient {
|
|||
|
||||
// append system reminders to the request
|
||||
let requestToSent = await flatMapTextParts(request, async (text) => [text]);
|
||||
if (messageType === SendMessageType.UserQuery) {
|
||||
if (
|
||||
messageType === SendMessageType.UserQuery ||
|
||||
messageType === SendMessageType.Cron
|
||||
) {
|
||||
const systemReminders = [];
|
||||
|
||||
// add subagent system reminder if there are subagents
|
||||
|
|
|
|||
|
|
@ -203,6 +203,44 @@ describe('CronScheduler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getExitSummary', () => {
|
||||
it('returns null when no jobs', () => {
|
||||
expect(scheduler.getExitSummary()).toBeNull();
|
||||
});
|
||||
|
||||
it('returns summary with single job', () => {
|
||||
scheduler.create('*/5 * * * *', 'check the build', true);
|
||||
const summary = scheduler.getExitSummary()!;
|
||||
expect(summary).toContain('1 active loop cancelled:');
|
||||
expect(summary).toContain('Every 5 minutes');
|
||||
expect(summary).toContain('check the build');
|
||||
});
|
||||
|
||||
it('returns summary with multiple jobs', () => {
|
||||
scheduler.create('*/5 * * * *', 'check the build', true);
|
||||
scheduler.create('*/30 * * * *', 'check PR reviews', true);
|
||||
const summary = scheduler.getExitSummary()!;
|
||||
expect(summary).toContain('2 active loops cancelled:');
|
||||
expect(summary).toContain('check the build');
|
||||
expect(summary).toContain('check PR reviews');
|
||||
});
|
||||
|
||||
it('truncates long prompts', () => {
|
||||
const longPrompt = 'a'.repeat(100);
|
||||
scheduler.create('*/1 * * * *', longPrompt, true);
|
||||
const summary = scheduler.getExitSummary()!;
|
||||
expect(summary).toContain('...');
|
||||
// Should not contain the full 100-char prompt
|
||||
expect(summary).not.toContain(longPrompt);
|
||||
});
|
||||
|
||||
it('returns null after all jobs are deleted', () => {
|
||||
const job = scheduler.create('*/1 * * * *', 'temp', true);
|
||||
scheduler.delete(job.id);
|
||||
expect(scheduler.getExitSummary()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('stops and clears all jobs', () => {
|
||||
scheduler.create('*/1 * * * *', 'a', true);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { matches, nextFireTime } from '../utils/cronParser.js';
|
||||
import { humanReadableCron } from '../utils/cronDisplay.js';
|
||||
|
||||
const MAX_JOBS = 50;
|
||||
const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000;
|
||||
|
|
@ -224,6 +225,27 @@ export class CronScheduler {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable summary of active jobs for display on session
|
||||
* exit. Returns null if there are no active jobs.
|
||||
*/
|
||||
getExitSummary(): string | null {
|
||||
if (this.jobs.size === 0) return null;
|
||||
|
||||
const count = this.jobs.size;
|
||||
const lines = [
|
||||
`Session ending. ${count} active loop${count === 1 ? '' : 's'} cancelled:`,
|
||||
];
|
||||
for (const job of this.jobs.values()) {
|
||||
const schedule = humanReadableCron(job.cronExpr);
|
||||
// Truncate long prompts
|
||||
const prompt =
|
||||
job.prompt.length > 60 ? job.prompt.slice(0, 57) + '...' : job.prompt;
|
||||
lines.push(` - [${job.id}] ${schedule}: ${prompt}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all jobs and stops the scheduler.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue