anthropics / anthropics/claude-code
[BUG] Local scheduled task silently collects nothing on workspace switch, and the dead run reports `succeeded`
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
### Environment
- Claude desktop app `1.52386.3`, CLI `2.1.266`, macOS (Darwin 25.6.0)
- **Local** scheduled tasks (`~/.claude/scheduled-tasks/`), not cloud Routines
Related but distinct: #92910 is cloud Routines stuck in `PENDING`. This one is the local desktop scheduler, and the run reports **`succeeded`**.
### What's wrong
A local cron scheduled task silently collected nothing on 2 of 3 firings over three days. Both losses report `succeeded` and one of them leaves no session record at all, so run history cannot be used to tell whether a scheduled task did its work.
There are three separate defects here that compound. I have the main-process logs for all of it.
---
### 1. The scheduler only evaluates the *currently active* workspace
Each org/workspace has its own `scheduled-tasks.json` and its own scheduler instance. A cron task whose workspace is not the active one does not fire at its scheduled time — it fires whenever you next switch into that workspace.
My task is `0 9 * * *` (09:00 local). Across three days it never once fired at 09:00:
| due | actually dispatched | delay |
|---|---|---|
| 09-15 09:00 | 09:25:48 | +26m |
| 09-16 09:00 | 10:47:50 | +1h48m |
| 09-17 09:00 | 09:41:41 | +42m |
Every one of those is the moment I switched workspaces, confirmed by a `Reset` → `Initialized` pair for that org immediately before. On 09-16 and 09-17 the app had been running all night in the *other* workspace, and the 09:00 slot passed unfired.
`mcp__scheduled-tasks__list_scheduled_tasks` is also scoped to the active workspace, so it does not show all of a user's scheduled tasks — which made this hard to see.
### 2. That switch is exactly when dispatches get lost
The missed-run check runs immediately on `Initialized`, which happens on every workspace switch — while the app is tearing down every shell PTY and reloading sessions. Both lost runs show `Reset` → `Initialized` → `Spawning new session` **within the same second**:
```
09-15 09:25:48 [info] [CCDScheduledTasks] Initialized { accountId: '', orgId: '' }
09-15 09:25:48 [info] [CCDScheduledTasks] Spawning new session for scheduled task
{ cronExpression: '0 9 * * *', lastRunAt: undefined, missed: '2026-09-15T14:00:00.000Z' }
09-15 09:25:48 [info] [CCDScheduledTasks] Dispatch acknowledged by renderer:
```
…and then nothing. No `Confirmed task run`, no session record, no transcript. The run is simply gone.
The next day it happened again, this time with the 5s ack timeout visible:
```
09-16 10:47:50 [info] [CCDScheduledTasks] Initialized { accountId: '', orgId: '' }
09-16 10:47:50 [info] [CCDScheduledTasks] Spawning new session for scheduled task
09-16 10:47:55 [warn] [CCDScheduledTasks] Dispatch not acknowledged by renderer within 5s;
slot left unconsumed, will retry:
09-16 10:47:55 [info] [CCDScheduledTasks] Spawning new session for scheduled task
09-16 10:47:55 [info] [CCDScheduledTasks] Dispatch acknowledged by renderer:
09-16 10:47:56 [info] [CCDScheduledTasks] Confirmed task run for:
09-16 10:47:58 [info] [ScheduledTasks] Reset
09-16 10:47:59 [info] [ScheduledTasks] Initialized { accountId: '', orgId: '' }
```
The retry was acked and confirmed — and then the app switched *away* to the other workspace two seconds later. A session record was written but the CLI was never started.
**There is already a guard for this and it is armed for the wrong event.** The one firing that worked is the one that was deferred:
```
09-17 09:40:41 [info] [CCDScheduledTasks] Initialized { accountId: '', orgId: '' }
09-17 09:40:41 [info] [CCDScheduledTasks] Deferring check: 11s post-wake delay remaining
09-17 09:41:41 [info] [CCDScheduledTasks] Spawning new session for scheduled task
09-17 09:41:41 [info] [CCDScheduledTasks] Dispatch acknowledged by renderer:
09-17 09:41:41 [info] [CCDScheduledTasks] Confirmed task run for:
```
60s after `Initialized`, onto a settled renderer, and it worked. There are 188 `Deferring check: 60s post-wake` lines in my log — but **zero** around either lost dispatch, because those were workspace switches, not system wakes.
### 3. A run that never started still reports `succeeded`, and `lastRunAt` advances anyway
Diffing the dead 09-16 session record against the working 09-17 one:
| field | dead run | working run |
|---|---|---|
| `cliSessionId` | **absent** | present |
| `model` | **absent** | `claude-opus-5[1m]` |
| `completedTurns` | **absent** | `10` |
| `toolSurfaceSnapshot` | **absent** | present |
| `lastActivityAt` | `== createdAt`, to the millisecond | advances |
| transcript file | **none on disk** | present |
The record has **no `status` and no `error` field at all**, so status appears to be derived as "not running + no error ⇒ `succeeded`" — which a never-started run satisfies vacuously. `list_task_runs` duly reports `succeeded`.
Worse, `lastRunAt` is stamped on **ack**, not on completion. The 09-15 dispatch produced no session record whatsoever, yet the next day's spawn log carries `lastRunAt: '2026-09-15T14:25:48.324Z'` — the ack timestamp to the millisecond. So that firing is invisible to `list_task_runs` (`totalRuns` was 2, not 3) while still counting as the last run and consuming the cron slot.
### Impact
Any scheduled task that writes data is silently exposed. A burst designed to collect N samples quietly under-collects, and neither the run list, the `succeeded` status, nor `lastRunAt` will tell you. The only reliable signal is the task's own output file, which means every data-collecting task has to be written to verify its own writes landed.
### Expected behavior
1. The post-dispatch settling delay should apply to workspace-switch/`Initialized` dispatches, not only to system wake.
2. A dispatch should not be acked — and the cron slot should not be consumed, nor `lastRunAt` stamped — until the session has actually started (has a `cliSessionId`).
3. A run whose CLI never started should report `failed`, not `succeeded`. "Not running and no error recorded" is not sufficient evidence of success.
4. Ideally, cron evaluation should not depend on which workspace happens to be active.
### Steps to reproduce
1. Have two workspaces/orgs in the desktop app.
2. Create a local cron scheduled task in workspace A that writes to a file.
3. Leave workspace B active across the task's scheduled time.
4. Switch to workspace A. The task dispatches immediately on `Initialized`.
5. Observe in `~/Library/Logs/Claude/main.log`: either no `Confirmed task run` follows the ack, or one does but the session never starts. Either way `list_task_runs` reports `succeeded` and the output file is untouched.
Switching away again within a second or two of the dispatch makes it reproduce more readily.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the local scheduler records under ~/.claude/scheduled-tasks/ and reproduce the workspace-switch flow while watching ~/Library/Logs/Claude/main.log. Inspect the dispatch and run-history entry points exposed by list_task_runs, using the reported ack, cliSessionId, status, and lastRunAt behavior as checkpoints. Done means switched-workspace dispatches settle safely, runs are recorded only after the CLI starts, failures report failed, and cron slots are not consumed prematurely.
Written by the indexing model from the issue text.
Assessment
- Domain
- desktop, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100