Affected area
Desktop app
Installation method
Built from source
Lody version or commit
3edbd47 (current main)
Operating system
macOS 15.7.4 arm64
Agent or runtime
Builtin Kimi Code (managed runtime); the derivation is provider-neutral, so any agent emitting CronCreate/CronDelete tool calls is affected.
What happened?
The "session will continue" chip above the composer (ScheduleChip / ScheduledTasksPanel) keeps listing a one-shot cron long after it fired, with a countdown of roughly a year ("fires in 364 days"). The job itself fired on time and the runtime auto-deleted it; only the panel's derived state is wrong.
Root cause, verified against a real session document:
- The panel derives pending tasks from the
CronCreate tool_call items in history (collectPendingScheduledTasksFromHistory in @lody/shared) and anchors a one-shot cron's single fire time at the owning turn's endedAt (resolveFireMs).
- Cron-fire follow-up turns are runtime-internal steers, so one history entry aggregates several runtime turns and its
endedAt keeps advancing — in the observed case the CronCreate executed at 03:18 for a 03:33 fire, while the merged entry's endedAt is 03:39.
- Anchoring at 03:39 means the fire minute 03:33 has already passed, so
nextCronFireMs('33 3 3 9 *', …) resolves to the next year's Sep 3 03:33 — a phantom fire time ~364 days in the future.
- Because that phantom time is future, the panel's "already fired → hide" filter (
FIRED_HIDE_GRACE_MS) can never hide the row. The scheduler's auto-delete of a fired one-shot leaves no CronDelete tool call, so the deriver also keeps the task "pending" forever.
What did you expect?
After a one-shot cron fires, the panel stops showing it (within the short fired-grace window), and a pending one-shot shows the fire time the runtime actually committed to — never a next-year rollover.
How can we reproduce it?
- In a session, have the agent create a one-shot cron a few minutes out (e.g. "check X in 15 minutes" →
CronCreate with recurring: false and pinned minute/hour/day/month).
- Keep the turn (or a cron-fire follow-up chain) running until past the pinned fire minute, so the history entry's
endedAt lands after it.
- Reopen the session once the fire time is in the past.
- Observe the chip: the fired one-shot is still listed, counting down ~364 days.
Unit-level reproduction: feed collectPendingScheduledTasksFromHistory an entry whose endedAt is later than the cron's fire minute and call resolveFireMs with now after the fire — it returns next year's date instead of the past fire time.
How often does it happen?
Every time
Relevant log output
# Derived from a real session doc (prompt/paths redacted):
# CronCreate rawInput: { cron: "33 3 3 9 *", recurring: false } # executed 03:18:43 +08:00
# owning history entry: timestamp 03:13:39, endedAt 03:39:15 # merged cron-fire turns
# runtime: fired 03:33:00 (coalescedCount=1), one-shot auto-deleted
# panel deriver: nextCronFireMs anchored at 03:39 -> 2027-09-03T03:33 (+364 days, never hides)
Additional context
Not a timezone bug: schedulingTimeZone is recorded and applied correctly; the corruption comes from the anchor timestamp. The persisted CronCreate output already carries the runtime-committed nextFireAt line (one-key-per-line format), which is the exact truth the panel needs. A PR with a fix (prefer committed nextFireAt, stamp scheduling calls with a first-persisted recordedAtMs, and stop anchoring at endedAt) follows.
Before submitting
Affected area
Desktop app
Installation method
Built from source
Lody version or commit
3edbd47 (current
main)Operating system
macOS 15.7.4 arm64
Agent or runtime
Builtin Kimi Code (managed runtime); the derivation is provider-neutral, so any agent emitting
CronCreate/CronDeletetool calls is affected.What happened?
The "session will continue" chip above the composer (
ScheduleChip/ScheduledTasksPanel) keeps listing a one-shot cron long after it fired, with a countdown of roughly a year ("fires in 364 days"). The job itself fired on time and the runtime auto-deleted it; only the panel's derived state is wrong.Root cause, verified against a real session document:
CronCreatetool_callitems in history (collectPendingScheduledTasksFromHistoryin@lody/shared) and anchors a one-shot cron's single fire time at the owning turn'sendedAt(resolveFireMs).endedAtkeeps advancing — in the observed case theCronCreateexecuted at 03:18 for a 03:33 fire, while the merged entry'sendedAtis 03:39.nextCronFireMs('33 3 3 9 *', …)resolves to the next year's Sep 3 03:33 — a phantom fire time ~364 days in the future.FIRED_HIDE_GRACE_MS) can never hide the row. The scheduler's auto-delete of a fired one-shot leaves noCronDeletetool call, so the deriver also keeps the task "pending" forever.What did you expect?
After a one-shot cron fires, the panel stops showing it (within the short fired-grace window), and a pending one-shot shows the fire time the runtime actually committed to — never a next-year rollover.
How can we reproduce it?
CronCreatewithrecurring: falseand pinned minute/hour/day/month).endedAtlands after it.Unit-level reproduction: feed
collectPendingScheduledTasksFromHistoryan entry whoseendedAtis later than the cron's fire minute and callresolveFireMswithnowafter the fire — it returns next year's date instead of the past fire time.How often does it happen?
Every time
Relevant log output
Additional context
Not a timezone bug:
schedulingTimeZoneis recorded and applied correctly; the corruption comes from the anchor timestamp. The persisted CronCreate output already carries the runtime-committednextFireAtline (one-key-per-line format), which is the exact truth the panel needs. A PR with a fix (prefer committednextFireAt, stamp scheduling calls with a first-persistedrecordedAtMs, and stop anchoring atendedAt) follows.Before submitting