anthropics / anthropics/claude-code
Scheduled one-time tasks fire early / duplicate-execute, causing shared routine_id collisions
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Summary
A batch of 20 one-time scheduled tasks (`scisci-doc-01` through `scisci-doc-20`), each with a
distinct `fireAt` spaced 20 minutes apart, was created via the scheduled-tasks MCP tool
(`create_scheduled_task`). Each task's prompt hardcodes its own task name as an identifier
(`routine_id`) that the executed session writes into a JSON lock file on disk, used for mutual
exclusion between runs of a shared, long-running job (one task processes one queue item from a
shared work queue, protected by a file-based lock).
In practice, tasks were observed firing well before their scheduled `fireAt`, and in at least one
case two independent executions ran concurrently while both self-identifying with the *same*
`routine_id` — because the same one-time task appears to have been dispatched twice.
### Evidence
1. **Duplicate dispatch of the same one-time task.** Task `scisci-doc-04` (`fireAt:
2026-07-25T21:05:00.000Z`) was observed running from ~2026-07-25T21:01Z. Two independent
execution contexts both wrote the identical value to the shared lock file:
`{"routine_id":"scisci-doc-04","iniziata":"2026-07-25T21:03:38Z"}`
— same routine_id, same start timestamp, down to the second — and then proceeded to do real,
divergent work in parallel (rendering/reading the same source PDF, writing different notes)
for roughly 20 minutes before one of the two sessions noticed the overlap by re-reading shared
state and backed off. No data was lost only because one side happened to check before writing.
2. **Tasks firing far ahead of their scheduled `fireAt`.** Task `scisci-doc-07`
(`fireAt: 2026-07-25T22:05:00.000Z`) was already recorded as `"stato":"in_corso"` in the shared
lock file at `2026-07-25T21:26:40Z` — about 38 minutes before its scheduled time. Tasks
`scisci-doc-05` (fireAt 21:25Z) and `scisci-doc-06` (fireAt 21:45Z) both show log entries from
their own executions timestamped 21:10:33Z and 21:10:59Z respectively — 14 and 34 minutes
before their scheduled `fireAt`.
3. **Inconsistent completion bookkeeping.** A `list_scheduled_tasks` call at ~21:26Z showed
`scisci-doc-01`, `-02`, `-03` correctly marked `enabled: false` with `lastRunAt` populated
(i.e. the scheduler's normal one-time "ran once, then disabled" behavior). But `scisci-doc-04`
through `scisci-doc-20` all still showed `enabled: true` with **no `lastRunAt`** at all, despite
`-04`, `-05`, `-06`, `-07` having demonstrably already executed (per their own session logs and
the shared lock file). This suggests the scheduler is not reliably marking these one-time tasks
as completed after dispatch, which may be the root cause of the early/duplicate firing (e.g. a
retry or backlog-drain mechanism re-dispatching tasks it doesn't think have run yet).
### Impact
Any workflow that relies on a scheduled task's own declared name/id as a uniqueness token for
coordination (locks, idempotency keys, dedup) can silently break: two dispatches of "the same"
task are indistinguishable from each other by that id, even though they are different, concurrent
executions. In our case this defeated a file-based mutual-exclusion lock and could have caused
two sessions to overwrite each other's writes to shared files.
### Workaround applied
We changed our own lock protocol to mint a random per-invocation token (uuid4) at acquisition time
and immediately re-read the lock file to confirm we still own that token before proceeding, and
again before releasing. This mitigates the symptom for our use case but does not address the
underlying scheduler behavior.
### Suggested fix
- Ensure one-time (`fireAt`) tasks are dispatched at most once, and are marked
disabled/`lastRunAt`-populated atomically with dispatch (not after some later step that can be
skipped/duplicated).
- Investigate why several one-time tasks in this batch fired significantly earlier than their
`fireAt`.
- Consider exposing a unique per-dispatch execution id (distinct from the task's static id/name)
to running sessions, so downstream coordination logic (locks, logs) can tell separate dispatches
apart even when the scheduler itself misbehaves.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the scheduled-tasks MCP entry points create_scheduled_task and list_scheduled_tasks, then reproduce the reported batch using the recorded fireAt values. Compare dispatch timing, duplicate executions, enabled state, and lastRunAt after each one-time run. Done means one-time tasks dispatch once at or after fireAt and are consistently marked completed; the payload also requests a distinct per-dispatch execution id.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100