[BUG] macOS: recurring scheduled tasks advance lastRunAt but never launch a session; one-off tasks in the same minute do launch

Status Open
Reported on v2.1.205
Maintainer reply None cached
Activity 0 comments · opened Sep 10, 2026

What's Wrong?

On macOS, recurring scheduled tasks advance lastRunAt at each fire but no agent session is ever created — no transcript, no tool calls, no side effects. One-off tasks on the same machine, in the same minute, launch normally.

This is the same shape as #60658 (macOS, closed as stale/not-planned and now locked — the bot asked that a new issue be filed referencing it) and appears to be the macOS counterpart of #83253 (Windows: fires reliably only in the catch-up burst after app launch, then goes silent). It is the inverse of #89936, where lastRunAt never advances; here it advances every time.

Environment

  • Claude Code 2.1.205, Claude.app 1.49585.0
  • macOS 26.6.2, arm64
  • 8 recurring tasks (daily/6-hourly cron) + occasional one-offs, via mcp__scheduled-tasks__*

How this was measured

The decisive signal is the birth time of session transcripts in ~/.claude/projects/<project>/:

for f in *.jsonl; do stat -f '%SB  %N' -t '%F %T' "$f"; done | sort

mtime is not usable: the scheduler resumes/reattaches to old transcripts, and an attended session opened in an old transcript updates its mtime too. Several old files were modified on the affected days while containing zero new turns — read by mtime they look like successful runs.

Evidence — 10 Sep 2026

Sessions created: exactly 2, both born 05:59:54Z, both one-off tasks. Zero created by any of the 7 recurring tasks that fired:

| Task (recurring) | lastRunAt | Session created? |
|---|---|---|
| flow-sentinel | 03:09:01.008Z | no |
| ops-digest | 03:09:00.995Z | no |
| funnel-watch | 03:09:01.017Z | no |
| e2e-skipnames | 03:25:00.979Z | no |
| revenue-followup | 03:45:01.005Z | no |
| marketing-drafts | 04:16:00.971Z | no |
| bug-triage | 05:59:43.317Z | no |

Cleanest control: the recurring bug-triage stamped 05:59:43.317Z and created nothing; two one-off tasks stamped 05:59:48Z and had transcripts on disk 6 seconds later. Same minute, same scheduler, opposite outcome — so this is not machine sleep, not time-of-day, and not cron parsing. The three tasks stamping within 22 ms of each other also look like a catch-up burst rather than three independent fires.

No task produced any side effect (expected file, git commit, or DB row) on the affected days.

Two distinct failure modes in recordedSkips

~/Library/Application Support/Claude/claude-code-sessions/<id>/<id>/scheduled-tasks.json has a recordedSkips map. On 6–9 Sep it recorded refusals with reasons global_limit and per_task_limit (e.g. revenue-followup 132 entries, funnel-watch 109, e2e-skipnames 69). Those refusals appear to be counter-based and to persist in-memory: an app restart cleared them transiently.

On 10 Sep recordedSkips is empty for every task, yet lastRunAt still advanced on all 7 recurring tasks and still nothing launched. So there is a second path where the fire is consumed and marked as run without either launching a session or recording a skip.

Timeline

Sessions created per day in this project (birth time): 8–12/day from 4 Aug through 5 Sep, then 6 Sep: 0, 7 Sep: 0, 8 Sep: 6 (all within ~25 min of an app restart), 9 Sep: 0, 10 Sep: 0 recurring.

Throughout, every task shows healthy enabled: true, a valid cronExpression, and an advancing nextRunAt. Task SKILL.md files are all present and non-empty. The UI intermittently shows "Routine file not found or has an unexpected format", but that string does not appear anywhere in the app bundle and the files load fine.

Things tried that did not fix it: re-spreading the cron schedules; archiving ~230 stale sessions; restarting Claude.app (recovers only for minutes-to-hours, matching #83253); converting a task's symlinked SKILL.md to a regular file.

What Should Happen?

  1. lastRunAt should advance only when a session actually starts — otherwise it is a silent false-positive, exactly as argued in the #60658 comments. Enqueue / start / complete should be distinguishable.
  2. A fire that is refused or dropped should be visible in the UI, with its reason, rather than being indistinguishable from a successful run.
  3. Whatever budget causes global_limit / per_task_limit should be inspectable, and should not silently consume a fire.

Reproduction

Not reproducible on demand — it appears after some hours of app uptime and affects recurring tasks only. The observable is: recurring task's lastRunAt advances, and no transcript is created in ~/.claude/projects/<project>/ at that time.

View original on GitHub ↗