Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Configure an enabled recurring automation that sends a prompt to an existing thread, for example every five minutes.
- Leave the target thread unable to process new work, such as after its provider hits a usage limit.
- Allow several scheduled intervals to pass.
- Inspect the thread's queue and the automation's last-run status.
Expected behavior
A recurring check should retain one outstanding occurrence while its target is blocked, without building an unbounded backlog. Delivery should honor usage-limit and held-queue states, and the task should distinguish queued delivery from dispatch for execution.
Actual behavior
Each interval adds another copy of the prompt to the queue. The automation reports success after queue admission, even though the prompt has not started executing. The queue can continue growing while the thread still appears to be Working.
Impact
Major degradation or frequent failure.
Automatic checks accumulate stale work without making progress, and the success status obscures the delivery failure.
Version or commit
The bug continues to occur on the latest builds; it is not limited to a single version. The exact current commit was not recorded.
Environment
Codex provider; recurring automation bound to an existing thread.
Investigation
A controlled service-level reproduction confirmed that four scheduled intervals create four pending prompts when the target cannot process them. The scheduler releases its reservation after queue admission and advances to the next interval without checking whether work from the same task is still outstanding.
Queue promotion already checks recognized usage-limit blocks, but fresh queue-mode admission does not consistently apply those checks when no active run exists. A held queue can similarly be bypassed at admission.
The stale Working state has no confirmed root cause. Preventing duplicate admission alone does not establish a fix for that state.
Related: #16393 proposes an optional skip-if-busy setting for scheduled tasks.
Suggested fix
Check for outstanding work from the same task before admitting an automatic recurring occurrence. Retain that occurrence while advancing later intervals, apply usage-limit and held-queue checks consistently, and expose whether delivery was queued or dispatched. Preserve explicit Run now and webhook requests, existing queued messages, and the configured quota-recovery policy.
Workaround
Disable the recurring automation to prevent further queue growth. This does not clear the existing backlog.
Activity
juliusmarminge commentedon Oct 7, 2026
Note
Grok responding on behalf of Julius.
Thanks for the detailed report. I checked it against
main, and the accumulation and the misleading success status both come from the code.Root cause
runTaskinapps/server/src/scheduledTasks/ScheduledTaskService.ts:715-726only guards against an in-flight dispatch (activeRuns). For a bound thread, it callsthreadManagement.sendToThread({ mode: "queue" })(:816-831), and the reservation is released inEffect.ensuring(:867-873) as soon as admission returns.next_run_atthen advances (:845,:854-861). Nothing checks whether a run from an earlier occurrence (scheduledTaskId) is still queued, so each interval adds another copy.runSucceeded = result._tag === "Success"(:834-836) recordslast_run_status = 'succeeded'once the enqueue call returns, even if the queued run never starts.apps/server/src/orchestration-v2/Orchestrator.ts:1243-1269,startNextQueuedRun) refuses to start while a queued run isqueueHeldorusageLimitBlockedRun(...)is non-null. Fresh admission doesn't do the same:queue_after_activeinheritsqueueHeld(Orchestrator.ts:4937-4941), but the usage-limit state isn't checked.decideMessageDispatchturnsqueue_after_activeintostart_run(apps/server/src/orchestration-v2/CommandPolicy.ts:460-462, andstart_immediatelyat:469-471) without checking either a held queue or a usage-limit block. So a scheduled prompt can skip past held items, or start a run that fails right away against the limit.Stale "Working": I didn't find a mechanism in the scheduler for this. Duplicate admission alone doesn't explain it. If you can share the run statuses for the thread while it shows Working (from the thread's run list or logs), that would help.
Proposed fix
trigger === "scheduled"occurrence, skip or coalesce it if the bound thread already has aqueued(or not-yet-started) run with the samescheduledTaskId, and still advancenext_run_at. Keep explicit Run now and webhook deliveries as they are (webhooks already haveWEBHOOK_MAX_QUEUED_PER_TASK).usageLimitBlockedRunchecks indecideMessageDispatch/message.dispatchthatstartNextQueuedRunuses. A blocked thread should then queue the message (marked held) instead of starting a run, while still following the configured quota-recovery policy.queuedresult, or something similar, for the run, separate fromsucceeded/dispatched, so the automation UI shows when a prompt is still waiting.Workaround
Disable the recurring automation to stop the queue from growing, then clear the duplicate prompts from the thread's queue by hand.
Related
#16393 (the open skip-if-busy feature request) overlaps with the first fix item. An outstanding-occurrence guard covers part of what it asks for, so the two could be designed together. #16726 (closed, about reusing one thread per run for agent scheduled tasks) touches the same runner but is a separate issue.
thymikee commentedon Oct 15, 2026
I have a fix for the first item of the proposed fix (one outstanding occurrence per task) and would like to send it as a focused PR. A few behavior choices need your call first.
What it does
preparing,queued,starting,running, orwaiting). For a bound task that's the bound thread. For a task that launches a fresh thread per run, it's the thread its last timed run launched. While there is an active run, the occurrence is held.How it differs from your suggestion, and the questions
scheduledTaskId. That id only lives in the message payload JSON. I check run status per thread instead, which uses the existing(thread_id, status)index and also covers restart continuations. The side effect is that any active turn in a bound thread holds the task, including the user's own work. That is the stricter behavior from [Feature]: Skip scheduled task occurrences when the bound thread is already running #18038. Is that acceptable, or should only the task's own prompts count?last_thread_id). Include it, or keep this PR to bound threads?Next run when the current one finishes · due 17 min agoinstead of a next-run time in the past. Include it, or leave it out?Items 2 and 3 of your proposed fix (consistent admission checks, and recording queued separately from dispatched) are not included. They would be separate PRs.
Evidence. The scheduler change has service-level tests that use the test clock and real SQLite projections. They cover a slot that comes due during a long run (it runs once, when the run finishes), a restart continuation still holding the slot, a task rebound while its launched run is still working, and Run now not hiding a busy timed run. The code is in the closed #19381, which also bundled a cron feature (now proposed separately in #19798). The fix PR would contain only this change.
Posted by Claude (Opus 5.5, Claude Code via T3 Code) on behalf of @thymikee.
yosofbadr commentedon Oct 19, 2026
Another reproduction on v0.1.0 (macOS, Claude provider, desktop app against the local server), with a different trigger: the bound thread was not usage-limited, only busy with one long turn.
What happened
An agent created an interval task (every 30 min) bound to its own thread via
schedule_task, then kept working in a single turn for ~2.5 h. Run records for the thread:Every interval queued a new copy while run 1 was still active, which matches root cause 1 above. The task prompt said "if a turn is already working, do nothing". That guard can never fire, because a queued prompt only starts after the active turn ends.
Gap not covered by this issue: deleting the task does not clear its queued runs
The agent deleted the task at 07:24:10 (
delete_scheduled_task). Runs 4 to 7 were already queued, so they were still delivered over the next 48 s. That made it look as if the deleted task was still firing.deleteTask(ScheduledTaskService.ts:1075onmain) removes the row and the webhook rate and permit state, but does not cancel or drop queued thread runs that carry itsscheduledTaskId. The suggested workaround here ("disable the automation") has the same limit.Suggested addition to the fix: when a task is deleted or disabled, cancel its not-yet-started queued runs in the bound thread, or at least report how many remain. The one-outstanding-occurrence guard from @thymikee would have capped this case at one stale wake-up instead of four.
On question 1 in the comment above: in this case the run holding the task was the agent's own work in the same thread, not the task's own prompt. So checking "any active run in the bound thread" would have prevented it, and checking only "a queued run with the same
scheduledTaskId" would have let one copy through.Investigated with
t3 triage; posted by Claude (Opus 5.5, Claude Code via T3 Code) on behalf of @yosofbadr.Suryansh-23 commentedon Oct 21, 2026
Another reproduction on T3 Code 0.1.1, with Codex and recurring tasks bound to existing threads.
We have two tasks, running every 5 and 15 minutes. Both target threads were busy with long-running goal work. Scheduled prompts entered their message queues instead of steering the active turns. I had to manually promote the queued messages to steering.
Inspection of the installed runtime found that
ScheduledTaskService.runTaskcallsthreadManagement.sendToThreadwith hard-codedmode: "queue"for bound threads.Preventing duplicate occurrences would help, but it would not fully address this use case: periodic progress checks must reach an active turn without manual action.
Would you accept an optional delivery mode for thread-bound schedules, exposed through the UI and MCP create/update tools?
queue: preserve the existing behaviour.auto: start an idle thread or steer an active turn, subject to provider and run-state support.Is this covered by planned work on this issue, or should I open a separate feature request for scheduled steering?
juliusmarminge commentedon Oct 22, 2026
Reproduced on Linux x64 in the real web dev app and server at a12d6ed942. A Codex turn ran
sleep 600; an enabled one-minute recurring task targeted that same thread. Three automatic intervals created three queued copies while the original run remained active. The task recordedsucceededafter each admission.This confirms the busy-thread variant from the comments. The literal quota-limit case was not verified: this VM uses Codex API-key authentication, and the rate-limit probe requires ChatGPT authentication. The Working label was accurate during this reproduction; it does not establish the reported stale Working state.
Runtime transcript. Recording: task already configured at the start; first queue admission at 00:51, second at 01:51. No Run now clicks. The test task was then disabled and the original turn stopped.
juliusmarminge commentedon Oct 22, 2026
The held-queue variant also reproduces at a12d6ed942: Stop left runs 2–4 queued with
queueHeld: true. Re-enabling the one-minute task allowed the next timed occurrence (run 5) to start and complete while those three remained held. No Send now or Run now action was used.Runtime transcript. The browser later lost its connection under VM load, so the persisted run transcript is the evidence for this attempt.
westline-jordan commentedon Oct 27, 2026
Another reproduction, with a different cost angle: Claude provider, an interval task every 5 minutes bound to a long-lived orchestrating agent's own thread.
When a scheduled turn or a conversation turn runs longer than the interval, the next occurrence queues behind it and starts seconds after the previous turn finishes. The agent sees that it just ran and stops, but each of those occurrences still costs a full model turn on a long thread: about 1.3–1.5M tokens per skipped turn, almost all of it cache reads. On one day, 39 of 101 scheduled turns were these duplicates. Over 16 days there were at least 140.
The fix proposed above (one outstanding occurrence per task) would remove all of them: skip rather than queue when the bound thread already has an active or queued run. Latest-only coalescing would work just as well, provided a manual Run now still runs.
vitalyiegorov commentedon Oct 31, 2026
Additional reproduction on 2026-10-31: Claude subscription usage limit + automatic snooze + recurring automation, with 60 queued messages visible in the thread.
The reporter supplied two desktop screenshots captured at 19:07 local time (Europe/Vienna). They show:
This matches this issue; no duplicate issue or competing PR is needed. Related #26987 describes a mixed automatic-notification backlog, while this reproduction specifically shows a subscription-limited, snoozed thread.
Current source and fix status
Checked upstream
mainat15fe283272:ScheduledTaskService.runTaskreservesactiveRunsonly during dispatch. For a bound thread,sendBoundpassesscheduledTaskIdandmode: "queue"tosendToThread.lastRunStatustosucceeded, increments the run count, advancesnextRunAt, and releases the reservation. There is still no check here for an earlier queued occurrence from that same task. Snooze/quota recovery does not itself stop this scheduler path from admitting another occurrence.The focused existing fix is #23609, head
46ec89c730cc887bb49a35b697b3580ed4d21add. It currently has merge conflicts with main. Its per-task queued-occurrence guard targets this accumulation directly; its description explicitly records that a literal subscription-limit reproduction was not exercised. These screenshots provide additional real-world evidence for that trigger, but do not validate the fix against it.Suggested maintainer validation
After refreshing #23609, run the focused scheduler regression and verify a real or controlled recognized Claude usage-limit pause across multiple timer intervals: at most one queued occurrence per task,
nextRunAtcontinues advancing, and the thread is not woken before its reset time. Check two task ids independently, a held queue after Stop, restart, and normal delivery after release. Preserve explicit Run now and webhook requests. Existing backlog cleanup and the distinction between queue admission and execution remain separate concerns; the guard does not retroactively remove 60 already-queued items.Workaround: pause the recurring automation to stop new copies, then review and cancel duplicate queued occurrences while retaining user messages. This investigation did not modify the reporter’s live thread, queue, automation, or database. App version and host-server commit are not established by the captures.
Investigated and posted with GPT-6.1-Sol through the Codex harness, on behalf of @vitalyiegorov.
vycdev commentedon Nov 1, 2026
Note
🤖 GPT-6.1 Sol responding on behalf of @vycdev
Correction: this investigation has not independently confirmed a T3 bug. The original wording overstated what the observations established.
This installation has both an hourly T3 task bound to an existing thread and custom external automation that explicitly sends new messages through the CLI when work finishes. Those external sends explain much of the repeated resurfacing. A fresh message waking a snoozed or settled thread does not, by itself, establish a snooze defect.
For completeness, the read-only records from T3 Code 0.1.5 on macOS, Claude provider, also show a native scheduled-delivery occurrence:
thread.unsnoozed, clears the persisted snooze, and starts a run that fails onusage_limit/api_error_429in under one second.lastRunStatus: succeeded, although the admitted run fails.The captured history contains 37 usage-limit failures: eight from scheduled occurrences and 29 from external CLI messages. These counts describe this mixed setup, not 37 independently established scheduler failures.
No queued backlog was present at capture, so this does not independently reproduce the queue accumulation reported here. Whether the native scheduled-delivery occurrence violates the intended quota/queue policy remains a separate question requiring an isolated reproduction. No proposed fix was validated.
The investigation was historical and read-only; no settings, schedules, queues, or application code were changed. Thread titles, identifiers, prompts, paths, and work details have been omitted.
dyankov91 commentedon Nov 6, 2026
Note
🤖 Claude Opus 5.5, writing on behalf of the reporter.
Another way to reach the same backlog, plus one behaviour this issue doesn't cover yet: queued copies keep arriving after the scheduled task is deleted.
Setup
schedule_task(interval,everyMs: 1800000,bindToCurrentThread: true).What happened
AskUserQuestionquestion and waited for the answer. While the question was open, every interval queued another copy of the task's prompt; none ran.delete_scheduled_task. The call returneddeleted: true, andlist_scheduled_tasksreturned[]for this project, for the other projects on the same repo, and for the scratch project.Expected
Impact
Each stale copy starts a full turn that does nothing. The user had to read the same "no job to delete" answer over and over, and the only way to stop them was to wait for the queue to empty.