#3145·CowAgent

[Bug] Local-time schedules drift across DST; UTC servers misinterpret reminder times and daily memory dates

Author: kowkowhuangCreated Sep 12, 2026Updated Sep 17, 2026
Labelsenhancement

Self check

  • Searched existing issues (including closed). #1222 is related but concerns the older chat_start_time setting; this bug report covers the 2.1.8 Agent scheduler, user time context and daily memory.

Bug: actual behavior differs from the requested local time

Classification: correctness bug, not an optional scheduling feature. When a reminder is created for an explicitly requested local time, executing it at a different local time after a DST transition is incorrect behavior. Interpreting a browser-local one-time timestamp as server-local time is also a timezone conversion bug. The related memory issue is inconsistent local-date handling when user-local "today" and server-local "today" differ.

The missing timezone persistence/propagation is a cause of the bug, not a reason to classify the wrong execution time as a feature request. Location auto-detection, additional UI conveniences, and replacing the scheduling engine are optional implementation ideas; they are not prerequisites for fixing the core correctness defects. Cases where the original timezone intent is unknown are explicitly distinguished below and should not be automatically relabeled or migrated.

On a Linux deployment with system/process timezone UTC, CowAgent 2.1.8 does not consistently carry a user's or task's IANA timezone through schedule creation, execution, display and daily-memory dates. Remembering a timezone in USER.md/prompt text does not make the scheduler use it.

Source observations:

  • scheduler_service.py: comparisons use naive datetime.now(); _calculate_next_run feeds from_time directly to croniter and ignores schedule.timezone. _parse_naive_local converts aware input to server-local time then drops tzinfo.
  • scheduler_tool.py: cron creation stores type/expression without a timezone. Initial next-run calculation is separate from service rescheduling.
  • console.js: the one-time editor assigns datetime-local input directly to schedule.run_at without an offset, whereas the backend interprets it as server-local time. Source-path reproduction, not an actual reminder dispatched to a recipient.
  • summarizer.py: daily memory filename selection uses server datetime.now(). builder.py selects today's memory similarly, even though the identity prompt can display a user timezone.

Global impact — not an Australia-only issue

This affects CowAgent users across many overseas timezones that observe daylight saving time, including:

  • New York / US Eastern: America/New_York.
  • Europe: Europe/London, Europe/Paris, Europe/Berlin.
  • Australia's DST-observing regions: Australia/Sydney, Australia/Melbourne, Australia/Adelaide, Australia/Hobart.
  • New Zealand: Pacific/Auckland; also cover Pacific/Chatham to avoid assuming whole-hour UTC offsets.

Melbourne is just the reproducible example, not the proposed scope. A fixed UTC conversion can shift recurring reminders away from the intended local clock time anywhere seasonal offsets change. Server-local daily-memory dates can also disagree with the user's local date. This matters for reminders, work schedules, daily summaries and time-sensitive market monitoring across regions.

The implementation must use each named zone's IANA rules, rather than a single global summer/winter switch. Northern and Southern Hemisphere seasons differ; New York, Europe, Australia and New Zealand do not share one transition calendar. Do not assume every region within a country observes DST: include non-DST controls such as Australia/Perth, Australia/Brisbane and Asia/Hong_Kong.

Expand the acceptance matrix to both annual transitions in each region above, including nonexistent/repeated local times, cross-region schedules during weeks when only one region has changed offset, and local-midnight memory boundaries. Source of timezone rules: IANA time zone database. These are requested additional regression cases; the isolated executable tests reported below were Melbourne-focused.

Reproduction with synthetic times (no live task execution)

For a desired Friday 11:00 Australia/Melbourne reminder, a fixed UTC cron 0 1 * * 5 produces:

  • 2026-10-02 01:00Z -> 11:00 +10:00 (correct).
  • 2026-10-09 01:00Z -> 12:00 +11:00 (one hour late).
  • The desired second occurrence is 2026-10-09 00:00Z.

Adding "timezone": "Australia/Melbourne" to the existing schedule dict alone does not change _calculate_next_run. A naive browser input 2026-10-09T11:00:00 interpreted on a UTC server becomes 22:00 Melbourne. Explicit +11:00 input converts correctly, but recurring timezone intent still is not retained.

At instant 2026-10-03T14:30Z, the UTC date is October 3 while the Melbourne date is October 4. This matters when identifying "today's" memory, not just when sending reminders.

Live read-only inspection also found workarounds: paired UTC cron triggers with a natural-language local-time guard, and one-time dates preconverted separately for standard/daylight time. These can mitigate individual tasks, but are not timezone-aware dispatch and should not be blindly migrated as if every current task were wrong.

Expected behavior and repair requirements

A consistent time contract shared by memory and scheduling, without changing the server timezone or replacing the whole scheduler unnecessarily:

  1. Persist a canonical IANA timezone (e.g. Australia/Melbourne, America/New_York, Asia/Hong_Kong), its source and scope. Separate user/home timezone, temporary travel/display timezone and task/event timezone. Explicit task choice wins; use a confirmed user preference as a default. Do not infer the user's timezone from VPS/IP/proxy location or from the city whose weather is requested. Disambiguate ambiguous place names; coordinates-to-zone lookup can be optional.
  2. Give new cron tasks a timezone field; compute local wall-clock recurrences using that zone, then persist/compare aware UTC instants. Preserve both original local intent and zone. Keep elapsed intervals distinct from calendar recurrence. Task creation and subsequent rescheduling must use the same helper.
  3. Unify temporal context for prompts, user-scoped daily memory write/read/summary and "today/tomorrow". Record UTC event instants plus timezone/local-date context. Do not rename or lose historical memory during migration; handle shared/group-agent timezone policy explicitly.
  4. Web/desktop/API should expose the zone, show the next few local + UTC occurrences (including across DST), and submit aware one-time timestamps. Changing UI display zone must not silently change task semantics.
  5. Define spring-forward nonexistent-time and fall-back duplicate-time policies explicitly (skip/shift/reject; first/second/both), and document defaults. Distinguish market timezone from market holiday/open-close calendars.
  6. Migrate legacy tasks conservatively: preserve their existing server-time interpretation first, mark timezone unresolved, and preview conversions before opting into user-local recurrence. Keep task IDs, enabled status, recipients, agent/channel ownership and execution history. Preserve manual-run behavior, silent success, retries and the current 600-second catch-up behavior unless intentionally redesigned.

Smallest-change open-source approach (preferred)

Keep the existing pallets-eco/croniter integration and execution/store layers. Add a shared time-context helper using Python zoneinfo; python/tzdata can supply IANA data on platforms without it. The existing python-dateutil dependency offers datetime_exists / datetime_ambiguous for local-time validation.

An isolated calculation probe using installed croniter 6.2.4 + ZoneInfo kept Friday 11:00 and daily 06:30 fixed across Melbourne's 2026-10-04 and 2027-04-04 transitions while changing the UTC instant correctly. This is a tested version, not a claim that the current broad croniter>=2.0.0 range has identical behavior. Pin/validate a suitable dependency floor.

Boundary policies are still needed: in the probe a 02:30 spring-gap recurrence advanced to 03:00, while the repeated 02:30 in autumn produced both folds. Libraries do not decide the intended user policy. Validate before/after candidate generation and use UTC instants for ordering/deduplication.

APScheduler FIT/GAP (alternative, not a drop-in recommendation)

APScheduler 3.11.3 was evaluated only in an isolated temporary target, not installed into production dependencies.

Fits: timezone-aware CronTrigger, date/interval triggers, background scheduling, concurrency and misfire controls.

Gaps found in executable comparisons:

  • Numeric weekday semantics differ: croniter 0 1 * * 5 gives Friday; APScheduler 3.x from_crontab gives Saturday. Translate carefully or use weekday names.
  • croniter defaults to day-of-month OR day-of-week; APScheduler combines constraints. 0 9 13 * 5 cannot be copied unchanged, even after weekday-number translation.
  • Existing JSON TaskStore, agent/channel routing, silent replies and false-return delivery retries are application behavior. APScheduler does not preserve these automatically. A callback returning False is not a scheduler exception.
  • Existing manual/scheduled overlap protection must remain shared; interval anchoring and restart catch-up require parity tests.
  • DST gap/fold behavior still requires policy. An isolated 02:30 spring-gap CronTrigger candidate round-tripped through UTC as 03:30; autumn returned both folds.

Therefore a full APScheduler replacement would be materially larger than correcting time handling around the already-used croniter. A trigger-only adapter is possible but still introduces cron translation work. No package alone supplies user-location resolution, memory semantics and legacy-task migration.

Acceptance tests

  • Host UTC with different users/tasks in Melbourne, Perth, New York and Hong Kong; changing host timezone does not shift explicitly zoned tasks.
  • Both Melbourne DST transitions, New York transitions on their different dates, and a non-DST timezone; normal hours and gap/fold cases.
  • Local midnight memory write/read consistency; user travel must not rewrite old events or pinned task timezone.
  • Once input with explicit offset vs timezone-less UI input; ambiguous/invalid zones; multi-user/group context.
  • Legacy UTC/naive schedule migration, weekday/DOM-DOW parity, disabled tasks, bounded date ranges, restarts, manual runs, silence, failure retry and deduplication.

Contribution

  • I'd be interested in helping implement this.

This report provides read-only source findings and isolated calculation evidence. No live schedules or memory files were changed to produce the report.