Skip to content

[Bug]: A busy orchestrator thread's queue grows without bound: PR-watch wakes and recurring tasks never coalesce (75 queued, 59 stale PR wakes) #26987

Open

Description

@rahulkarajgikar

Note

🤖 Claude Opus 5.5, writing on behalf of the reporter.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

This is related to three open issues but is not a duplicate of any of them. It is what happens when all three hit one long-lived thread at the same moment:

Area

apps/server

Summary

We run one long-lived orchestrator thread. It watches the PRs its child workers open, and two recurring scheduled tasks are bound to it: a sweep every 30 min and a check every hour. Over about a day the thread's queue grew to 75 undelivered messages, and new user messages waited behind all of them. The thread spent its turns on notifications that were already stale. Meanwhile, workers that had finished sat idle because the orchestrator never reached them. Only 2 of 10 worker slots were in use.

What was in the queue (snapshot)

Kind Queued
Update on pull request #N … PR-watch wakes 59
Copies of the same 30-min recurring sweep prompt 6
Copies of the same hourly recurring prompt 3
Real messages from users or other threads 7

The 59 PR-watch wakes covered only 13 PRs. Per PR: 9, 9, 8, 6, 6, 5, 4, 3, 2, 2, 2, 2, 1. Most were superseded before they could be delivered. A typical sequence for one PR was "Checks failed on A", then "3 new comments", then "Checks failed on B", then "All checks passed on C". All four stayed queued, and each would run as its own turn.

Thread size when we retired it: about 35,000 timeline items over 3,866 runs in 8 days. Each turn on a thread that size is slow. That slows draining, the queue grows faster than it drains, and the backlog snowballs.

Steps to reproduce

  1. Have one thread call watch_pull_request on about 10 active PRs. Child workers push to them several times an hour, and review bots comment.
  2. Bind a recurring scheduled task with an interval of 30 min or less to the same thread.
  3. Keep the thread busy, so that each turn takes longer than the gap between wakes. A large thread does this naturally.
  4. Inspect the thread's queue (t3_queue list) after a few hours.

Expected behavior

Actual behavior

Every update for a PR, and every recurring tick, is queued as its own message and runs as its own turn. Nothing invalidates or merges the queued copies. The queue is unbounded.

Cancelling the backlog by hand took 66 separate t3_queue cancel calls, about 7 minutes in total. Some sends and cancels to that thread returned HTTP 503 while it was in this state.

Impact

Major degradation or frequent failure. The orchestrator stopped responding in practice, and user messages appeared to be "swallowed". The workaround was to cancel the queue by hand, retire the thread and start a successor.

Version or commit

T3 Code (Beta) 0.1.5, macOS 15 (Darwin 24.1), Codex provider on the orchestrator thread. statev2.sqlite was 16 GB at the time, which may be a factor in the slow turns and 503s (#24895, #21840).

Workaround we're using

The orchestrator no longer watches PRs; the worker that owns a PR watches it. Another thread trims stale wakes from the orchestrator's queue every hour, and the orchestrator is handed off at about 12k items.

Activity

  1. onlydole commented on Nov 3, 2026

    @onlydole
    Contributor

    Two single-PR cases where the wakes didn't coalesce, both on GitHub repos with review bots.

    • macOS desktop nightly, Oct 14: one watched PR produced 20 "new comments" and "checks passed, new comments" notifications in 104 seconds, from 23:14:33 to 23:16:17 UTC.
    • Raspberry Pi 5, 0.1.7-nightly.20261102.4093, Nov 2: one watched PR woke a Codex thread 7 times in 16 minutes (20:01 to 20:17 UTC), each time as its own run of 9 to 75 seconds. 3 of the 7 were for a review bot's progress comments.

    In the second case, Codex also called link_pull_request on the same PR 3 times (19:59, 20:02, 20:08), even though each update says "This PR stays linked and watched, so do not link, watch, or list it again."

  2. genesiscz commented on Nov 5, 2026

    @genesiscz

    A data point for the coalescing side, on T3 Code 0.1.7 (macOS arm64 desktop, Claude provider). The unwatch side of the same case is in #21009 (comment).

    One busy orchestrator thread watched about 25 PRs. In one day it had 466 PR-watch runs: 350 completed (350 full-context turns woken by PR notices) and 115 still queued after every watch had ended.

    No coalescing. When the 115 were removed, 15 PRs had between 1 and 13 queued notices each: 13, 12, 12, 10, 10, 10, 10, 9, 8, 8, 4, 3, 3, 2, 1. On main (337c5858c) each sweep that finds a change dispatches a sync with a fresh message:pr-watch:<uuid> (PullRequestWatchReactor.ts:279-290), and the orchestrator turns it into a new queue_after_active message (Orchestrator.ts:3025-3040). Nothing looks for a queued notice for the same PR first.

    No bulk cleanup. Removing them took 115 t3_queue cancel calls over about 4.5 minutes (66 in the original report). cancel takes one queuedRunId (thread/tools.ts:83-109) and dispatches one queued-run.cancel with one runId (orchestrationV2.ts:3259-3264). The only path that cancels all automatic queued runs together is thread.settle (Orchestrator.ts:3373-3384), which refuses while a run is active (Orchestrator.ts:3399) and settles the thread, so a busy orchestrator cannot use it.

    Suggestions, in addition to the ones above:

    Written by Claude Opus 5.5 in T3 Code for the reporter, from read-only queries on the local database.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions