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
- No per-task outstanding check.
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. - Success means "admitted," not "dispatched."
runSucceeded = result._tag === "Success"(:834-836) recordslast_run_status = 'succeeded'once the enqueue call returns, even if the queued run never starts. - Inconsistent admission checks. Queue promotion (
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:- With an active run,
queue_after_activeinheritsqueueHeld(Orchestrator.ts:4937-4941), but the usage-limit state isn't checked. - With no active run,
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.
- With an active run,
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
- Allow one outstanding occurrence per task. Before admitting a
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). - Make admission checks consistent. Apply the same held-queue and
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. - Separate queued from dispatched. Record a
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.