Scheduled one-time tasks fire early / duplicate-execute, causing shared routine_id collisions

Status Open
Maintainer reply None cached
Activity 0 comments · opened Jul 25, 2026

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.

  1. 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.

  1. 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.

View original on GitHub ↗