From fd6b9027b66337ce9ef70f3c1b5da27497b95b47 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Sun, 27 Sep 2026 10:56:45 +0200 Subject: [PATCH] fix(scheduler): address review findings - JobScheduler: a stored job whose zone lookup throws no longer fails StartAsync; timer re-armed under the lock; a delivery cut off by shutdown isn't recorded (one-offs fire again after restart); CancelJobAsync in one step - Schedules.ParseAt: ISO 8601 with a date only (no machine-clock date for a bare 09:00) - core: a FollowUp prompt that loses the race to a starting run joins it as a follow-up, not steering - Telegram: TimeZoneFor tolerates non-numeric targets; RunPromptAsync waits only when queued; one agent dir - ScriptRunner: bad workspace paths are Failed, not exceptions; zone id helpers share TimeZoneIds.ToIana's rule - tests: no wall-clock sleeps in scheduler tests; transcript-entry, disabled-scheduler and failing-prompt coverage - docs: spec notes for the above; README row for the scheduler --- README.md | 1 + .../specs/2026-09-26-scheduler-design.md | 25 ++++- .../ClaudeArgs.cs | 10 +- .../JobFormat.cs | 3 - .../JobScheduler.cs | 100 ++++++++++++------ .../ScheduleTool.cs | 5 +- .../Schedules.cs | 53 +++++++--- .../ScriptRunner.cs | 3 +- .../ZoneNames.cs | 22 ++-- .../Chats/ChatCommands.cs | 6 +- .../Chats/ChatRegistry.cs | 2 +- .../Chats/ChatTimeZoneService.cs | 5 +- .../Volcanic.AgenticCore.Telegram/Program.cs | 7 +- .../Scheduling/TelegramJobDelivery.cs | 16 ++- .../Features/Agent/AgentSession.Host.cs | 3 +- .../Features/Agent/AgentSession.Loop.cs | 20 +++- .../Features/Agent/AgentSession.cs | 8 +- .../Features/Agent/AgentSessionFactory.cs | 9 +- .../Scheduler/JobFormatTests.cs | 1 - .../Scheduler/JobSchedulerTests.cs | 59 +++++++++-- .../Scheduler/ScheduleTests.cs | 14 +++ .../Scheduler/Support/SchedulerFakes.cs | 3 + .../ScheduledPromptChatTests.cs | 17 +++ .../SchedulerChatTests.cs | 12 ++- .../Features/AgentLoopTests.cs | 23 ++++ 25 files changed, 326 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index ce4e186..03f8ab2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Claude Code as a model provider without an API key. | OpenAI-compatible provider | `extensions/Volcanic.AgenticCore.Extensions.OpenAI` | OpenAI, OpenRouter, Groq, Ollama, llama.cpp, vLLM | | Claude Code (subscription) provider | `extensions/Volcanic.AgenticCore.Extensions.ClaudeCode` (+ `.Bridge`) | Spawns `claude -p`, MCP "proposal" bridge | | Google Calendar (tool + connect commands) | `extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar` | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`; one account per chat | +| Scheduler (reminders, scheduled prompts, watcher scripts) | `extensions/Volcanic.AgenticCore.Extensions.Scheduler` | Cronos; per-chat jobs in `/scheduler/jobs.json`; see [docs/telegram-bot.md](docs/telegram-bot.md#scheduling) | | Telegram bot | `hosts/Volcanic.AgenticCore.Telegram` | Chat with the agent from Telegram; long polling, Claude Code, allow-list | | Reference headless host | `samples/Volcanic.AgenticCore.Sample.Headless` | Prompt in, JSON events out | | Tests | `tests/` | xunit; no test calls a paid API | diff --git a/docs/superpowers/specs/2026-09-26-scheduler-design.md b/docs/superpowers/specs/2026-09-26-scheduler-design.md index 240fb18..4f4630d 100644 --- a/docs/superpowers/specs/2026-09-26-scheduler-design.md +++ b/docs/superpowers/specs/2026-09-26-scheduler-design.md @@ -61,7 +61,10 @@ same chat. - **Scheduled agent prompts are queued as follow-ups, never as steering.** When the chat is idle a prompt job starts a run. When a run is active (a user request, or another job), the prompt goes in with `StreamingBehavior.FollowUp`: the current work finishes first, then the job's prompt is handled in the same run. A clock-driven prompt shouldn't - redirect what the user is doing mid-turn. User messages keep steering as they do today. + redirect what the user is doing mid-turn. User messages keep steering as they do today. This also holds when a run + starts between `PromptAsync`'s idle check and its run gate: the core's `StartRunAsync` gets the caller's + `StreamingBehavior`, and a `FollowUp` prompt joins the active run as a follow-up (with a `QueueUpdate`), not as + steering. - **Cron parsing uses Cronos** (`Cronos` on NuGet, Hangfire, MIT, no dependencies). It parses standard 5-field expressions and macros (`@daily`, `@hourly`, …). `GetNextOccurrence(DateTimeOffset, TimeZoneInfo)` works in a given zone and has defined DST semantics: a time skipped when clocks go forward fires at the transition, and an ambiguous @@ -72,7 +75,10 @@ same chat. sets a 1-minute floor for watchers. - **Each chat has its own time zone.** It applies to cron, to displayed times, to the session's system prompt, to the local-time header on each prompt, and to the `TZ` of the chat's `bash`, watcher and `claude -p` processes (§8). An `at` without an offset is read in the chat's zone, because - models often leave the offset out. An `at` with an offset is used as given. The zone is a chat setting, not a + models often leave the offset out. An `at` with an offset is used as given. The date is required and only ISO 8601 is + accepted (`yyyy-MM-dd`, or `yyyy-MM-dd[T| ]HH:mm[:ss[.fff]]` with an optional `Z`/offset): a bare `09:00` or a culture + format such as `09/10/2026` is rejected with a readable error, because it would be resolved against the machine's + clock rather than the chat's. The zone is a chat setting, not a scheduler setting, because the system prompt and `bash` need it with the scheduler disabled too: - **Stored by the host**, in a small per-chat settings store outside every workspace: `/telegram/chat-settings.json` (§7). Not in `jobs.json` (the scheduler can be off) and not in the session @@ -93,7 +99,9 @@ same chat. fires immediately. If it's more than 60 s late, direct text gets `(due