From d4f51bdd8b4d8529833cbfd5d967e8fefe37ddc0 Mon Sep 17 00:00:00 2001 From: ilya-bov <111734093+ilya-bov@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:21:06 +0300 Subject: [PATCH] Fix recurring 30s cron normalization and guidance --- src/lib/cron/tool-normalize.ts | 45 ++++++++++++++++++++++++++++++++-- src/prompts/tool-cron.md | 6 +++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/lib/cron/tool-normalize.ts b/src/lib/cron/tool-normalize.ts index 8791691..781043a 100644 --- a/src/lib/cron/tool-normalize.ts +++ b/src/lib/cron/tool-normalize.ts @@ -190,12 +190,40 @@ function normalizeScheduleFromRecord(input: UnknownRecord): CronSchedule | null : input; const rawKind = readString(scheduleRaw.kind)?.toLowerCase(); + const scheduleKindHint = readString(scheduleRaw.scheduleKind)?.toLowerCase(); + const explicitEveryKind = rawKind === "every" || scheduleKindHint === "every"; const at = readString(scheduleRaw.at) ?? readString(scheduleRaw.scheduleAt) ?? readString(scheduleRaw.runAt) ?? readString(scheduleRaw.when); - const everyMs = readNumber(scheduleRaw.everyMs); + const everyMsDirect = + readNumber(scheduleRaw.everyMs) ?? + readNumber(scheduleRaw.intervalMs) ?? + readNumber(scheduleRaw.repeatMs); + const everySeconds = + readNumber(scheduleRaw.everySeconds) ?? + readNumber(scheduleRaw.intervalSeconds) ?? + readNumber(scheduleRaw.repeatSeconds) ?? + (explicitEveryKind ? readNumber(scheduleRaw.seconds) : undefined); + const everyMinutes = + readNumber(scheduleRaw.everyMinutes) ?? + readNumber(scheduleRaw.intervalMinutes) ?? + readNumber(scheduleRaw.repeatMinutes); + const everyHours = + readNumber(scheduleRaw.everyHours) ?? + readNumber(scheduleRaw.intervalHours) ?? + readNumber(scheduleRaw.repeatHours); + const everyMs = + typeof everyMsDirect === "number" && everyMsDirect > 0 + ? everyMsDirect + : typeof everySeconds === "number" && everySeconds > 0 + ? everySeconds * 1_000 + : typeof everyMinutes === "number" && everyMinutes > 0 + ? everyMinutes * 60_000 + : typeof everyHours === "number" && everyHours > 0 + ? everyHours * 3_600_000 + : undefined; const anchorMs = readNumber(scheduleRaw.anchorMs); const expr = readString(scheduleRaw.expr) ?? readString(scheduleRaw.cronExpr); const tz = readString(scheduleRaw.tz) ?? readString(scheduleRaw.cronTz); @@ -209,7 +237,7 @@ function normalizeScheduleFromRecord(input: UnknownRecord): CronSchedule | null ? "every" : expr ? "cron" - : readString(scheduleRaw.scheduleKind)?.toLowerCase(); + : scheduleKindHint; if (kind === "at" && at) { return { kind: "at", at }; @@ -244,6 +272,16 @@ function normalizeScheduleFromRecord(input: UnknownRecord): CronSchedule | null : typeof delaySeconds === "number" && delaySeconds > 0 ? delaySeconds * 1_000 : 0; + if (kind === "every" && totalMs > 0) { + return { + kind: "every", + everyMs: Math.max(1, Math.floor(totalMs)), + anchorMs: + typeof anchorMs === "number" && Number.isFinite(anchorMs) + ? Math.max(0, Math.floor(anchorMs)) + : undefined, + }; + } if (totalMs > 0) { return { kind: "at", at: new Date(Date.now() + totalMs).toISOString() }; } @@ -323,6 +361,9 @@ function explainAddInputFailure(source: UnknownRecord): string { problems.push( "Example: {\"action\":\"add\",\"delaySeconds\":30,\"message\":\"Отправь пользователю: привет\"}" ); + problems.push( + "Recurring example: {\"action\":\"add\",\"schedule\":{\"kind\":\"every\",\"everyMs\":30000},\"payload\":{\"kind\":\"agentTurn\",\"message\":\"Отправь пользователю: привет\"}}" + ); return problems.join(" "); } diff --git a/src/prompts/tool-cron.md b/src/prompts/tool-cron.md index df74e61..42f7e1c 100644 --- a/src/prompts/tool-cron.md +++ b/src/prompts/tool-cron.md @@ -5,8 +5,10 @@ When the user asks to "remind later", "через N минут/секунд", " Rules: - For one-time reminders: use `action="add"` with `schedule.kind="at"` and ISO timestamp. - For recurring reminders: use `schedule.kind="every"` or `schedule.kind="cron"`. +- For sub-minute recurring reminders, use `schedule.kind="every"` with `everyMs` (example: 30s -> `everyMs=30000`). - Put the actual reminder text/instruction in `payload.message`. - Do not send raw natural-language text as the job definition; always send structured fields (`schedule` + `payload` or `delaySeconds` + `message`). +- `delaySeconds` / `delayMs` are one-shot delays and should not be used for recurring jobs. - If cron returns a preflight validation error, immediately retry once with normalized args (`action="add"`, explicit `schedule`, explicit `payload.message`) and do not repeat identical invalid arguments. - After creating a job, report `id`, schedule, and expected next run time. - For management requests, use: @@ -32,3 +34,7 @@ Examples: - `action="add"` - `schedule={ "kind":"cron", "expr":"47 19 * * *" }` - `payload={ "kind":"agentTurn", "message":"Отправь пользователю: прогноз погоды в Москве" }` +- Every 30 seconds: + - `action="add"` + - `schedule={ "kind":"every", "everyMs":30000 }` + - `payload={ "kind":"agentTurn", "message":"Отправь пользователю: Привет!" }`