Skip to content

Auto-settle on PR merge races the Claude wake turn after an awaited background task completes #26083

Open

Description

@hanneslipusch

Version: 0.1.3 (also on main as of 80f411f13d)

What happens
An agent runs a background Bash task (a merge wait), registers it with t3_background_wait, and ends its turn. The linked PR merges while the task runs. When the task exits, the thread is auto-settled before the Claude wake turn is registered, and settling closes the session, so the wake turn is lost. The agent never runs its post-merge steps.

Timeline from one real thread

  • .015 background task exits (exit 0); task list empties and triggers the settle check (ThreadSettlementService.ts:618)
  • .022 thread settled (auto-settle on merge; no active run yet)
  • .040 session detached, reason "Thread settled." (Orchestrator.ts:3851)
  • .045 query.close sent to Claude
  • Claude had started the wake turn (API request at .227); it never completed.

Why
#24610 (fixing #17625) protects the thread while the awaited task runs. It does not cover the gap between task completion and the wake run attaching: ClaudeAdapterV2.ts:7632-7669 only registers the wake run once the reply streams, and its pending-work check (:10560) reads the already-empty task list.

Repro
Background sleep 120 + t3_background_wait on a thread whose linked PR has already merged, then end the turn. In a control run without a PR, the settle step fired at +8 ms after the task list emptied and the wake turn registered at +375 ms; with a merged PR the thread settles inside that gap.

Suggested fix
Keep the adapter's pending-work signal true from task completion until the wake run attaches, and have the settle check ask the live session (as stillReportsBackgroundWork already does). The ACP adapter already counts buffered continuations; doing it in the settle check covers every provider.

Workaround: disable sidebarAutoSettleOnMerge.

Activity

  1. danddanddand commented on Oct 31, 2026

    @danddanddand
    Contributor

    Another occurrence on 0.1.6 (Claude Code via the Agent SDK). This time the merged PR was a self-hosted GitLab MR.

    • The agent merged the MR, called t3_background_wait on a background Bash watcher, and ended its turn.
    • About 40 s later the watcher exited. The task_notification and the empty background_tasks_changed reached T3, and T3 sent query.close 13 ms later. That was before the CLI's wake API request (~50 ms).
    • No run or notification item appeared. The user got no update for 2 hours, until they sent a message themselves.

    Also, the "No response requested." that shows up in the Claude transcript after this is not the model ignoring the notification. On resume, the CLI writes it ("model": "<synthetic>") to close the dangling wake turn, so the model never saw the task finish.

    #26108 matches the trace. It now conflicts with main, so it needs a rebase before it can land.

  2. altintasutku commented on Nov 1, 2026

    @altintasutku

    Another occurrence on 0.1.6 (Windows, desktop app, Claude Agent SDK, claude-opus-5-5).

    • The agent merged its PR (~20:48Z), started a ~5 min background test run, called t3_background_wait on it (matched), and ended its turn at 20:53:54Z.
    • 20:59:13.496Z task_notification (completed) and an empty background_tasks_changed arrived.
    • 20:59:13.514Z ThreadSettlementServiceV2.sweep dispatched thread.auto-settle; query.close was sent at .602Z, 106 ms after the notification. No wake run or item appeared.
    • The thread stayed silent until the user wrote ~10 min later. On resume the model saw the synthetic "No response requested." and wrongly told the user it had ended the turn itself.

    sidebarAutoSettleOnMerge was at its default. +1 for landing #26108.

    Prepared by Claude Opus 5.5 (claude-opus-5-5) via T3 Code triage.

  3. aport73 commented on Nov 2, 2026

    @aport73

    Another occurrence on 0.1.7-nightly.20261101 (macOS, Claude Code 2.1.316 via the Agent SDK). The PR host was a self-hosted Forgejo, sidebarAutoSettleOnMerge was at its default, and after-days was off. I found 12 occurrences across 3 threads, all with merged PRs. About 270 idle wakes on other threads resumed fine.

    • task_notification (completed) and an empty background_tasks_changed arrived. About 100 ms later the CLI emitted system/status: requesting, its own wake turn.
    • 7 ms after that: thread.auto-settle → ProviderSessionManager.detach → query.close.
    • One more detail: the detach queues a recovery continuation as a buffered provider wake (creationSource: "provider"). The follow-up run resumes the query but never sends prompt.offer, because Claude drains an empty wake buffer (the notification died with the closed query). The run finalizes about 10 ms later. So the fallback can't recover a wake that was cut off. Delivering that continuation as message_text when the session that buffered it is gone would let it recover, independent of fix(server): auto-settle on merge waits for a pending Claude wake turn #26108.

    The thread stayed idle for 28 minutes (856 on another thread) until the user wrote. +1 for rebasing and landing #26108.

    This comment was written by an AI model, Claude Opus 5.5 (claude-opus-5-5), running in Claude Code inside T3 Code, and posted with the user's approval.

  4. ethan-huo commented on Nov 4, 2026

    @ethan-huo

    Still reproduces on 0.1.7-nightly.20261102.4093 (macOS, Claude Agent SDK, claude-opus-5-5), and main at e68e19d60 has no change to this path.

    Our trigger makes it deterministic rather than a race: agents merge their PR and then wait in the background for the deploy pipeline to promote the merge commit (15–40 min). The linked PR is therefore always merged when the awaited task exits, so every such wait is lost. Three threads, same shape each time:

    • system/task_notification (completed or failed) + empty background_tasks_changed, then outgoing query.close 12–45 ms later; no init, no wake run.
    • Earlier wakes in the same thread, before its PR merged, resumed normally (init → wake turn → result with origin.kind: "task-notification").

    Side note: sidebarAutoSettleKeepPullRequests: true does not help here, since resolveAutoSettlementAt returns for a merged PR before it reads that setting. Disabling sidebarAutoSettleOnMerge is the only workaround we found.

  5. enzotrp974 commented on Nov 5, 2026

    @enzotrp974

    Still reproduces on 0.1.8-nightly.20261104.4165 (macOS, Claude Code 2.1.316 and 2.1.318). We crossed the two conditions over 299 idle wake-ups from about two days of local logs:

    Linked PR merged before the wake-up No merged PR
    t3_background_wait used 71 wake-ups, 23 missed 106, 0 missed
    t3_background_wait not used 37, 0 missed 85, 0 missed

    "Missed" means no model reply within 5 minutes of the task notification. In the misses we read, query.close follows the empty background_tasks_changed by 43 to 258 ms, as described above.

    Two things this adds:

    • Every miss needed both conditions. A merged PR alone and t3_background_wait alone each lost nothing here.
    • Leaving out t3_background_wait is a second workaround, besides turning off sidebarAutoSettleOnMerge. The sample is small, and the thread no longer shows as working while it waits. The T3 system prompt tells Claude threads to call this tool, so the default instructions lead agents into this bug.

    Measured and written by Claude (Opus 5.5) from local session and provider logs.

  6. lukasbxy commented on Nov 6, 2026

    @lukasbxy

    Measured data from two self-hosted machines, in case it helps prioritise #26108, #29820 or #31449. Versions were nightlies from 0.1.7-nightly.20261104.4151 to 0.1.8-nightly.20261105.4234, with Claude Code 2.1.318 through the Agent SDK. sidebarAutoSettleOnMerge was at its default.

    I joined the provider protocol logs with thread.settled events. A "wake" below is a task_notification that arrived while the session was idle.

    Wakes from 2026-10-31 to 2026-11-06 Count query.close within 3 s
    thread.settled within 1.31 s after the notification 13 13
    No thread.settled nearby, session stayed open 310 0
    No thread.settled nearby, session closed anyway 12 12
    • In the most recent 48 hours, 15 of 88 idle wakes were lost, or 17%. Auto-settle accounts for 13 of those 15.
    • The settle landed 10 ms to 1,310 ms after the notification, with a median of 28 ms.
    • Ten of the lost wakes were only noticed when the user asked what had happened. Those threads sat idle for 1 to 422 minutes, about 17 hours in total. Three others were never resumed.
    • Before each loss the agent had done everything right: it started the background command, t3_background_wait returned it as matched, and the turn ended normally.

    Two details that may not be covered yet:

    1. A close without a settle. The 12 closes in the last table row show the same shape, task_notification, then an empty background_tasks_changed, then query.close, but with no thread.settled event within 5 s. Ten predate the 48-hour window. I could not identify the caller, because the server traces rotate after about 90 minutes here. This suggests fixing only the settle check may leave a second path open. hasPendingBackgroundWork in ClaudeAdapterV2 still reads as false while the buffer holds only the task_notification, since it counts user, assistant and result frames.
    2. A wake run that starts and dies. In 2 of the 15 cases T3 did create the run. The notification item appeared, query.open followed about 3 s later, query.close came within a second, and the run completed in 2 to 5 s with no assistant message or tool call. From the UI that looks like an agent that read the notification and decided to do nothing.

    As a workaround I set sidebarAutoSettleOnMerge to false in settings.json on both machines today. The server picked it up without a restart. I'll report back here if wakes are still lost.

    Prepared by Claude Opus 5.5 from local logs.

  7. lucasbastianik commented on Nov 6, 2026

    @lucasbastianik

    Another occurrence on 0.1.7-nightly.20261103.4119 (macOS, Claude Code 2.1.318 via the Agent SDK, claude-opus-5-5). sidebarAutoSettleOnMerge was at its default. Two wakes were lost in one thread, both on background commands awaited with t3_background_wait.

    • 2026-11-05 21:53:58Z: task_notification (failed), linked PR merged 21:51:07Z, last user message 21:45:31Z. provider-session.detach ("Thread settled.") came 7 ms later. Idle for 2h34 until the user wrote.
    • 2026-11-06 02:59:10Z: task_notification (completed), linked PR merged 02:33:39Z, last user message 02:01:01Z. Detach came 39 ms later. Idle for 6h02 until the user wrote.
    • The other 5 notifications in the session did not meet that condition, because no PR had merged since the last user message, and they all woke normally.
    • On resume the model again read the synthetic "No response requested." as its own reply and told the user it had failed, which hid the cause until we checked the raw transcript fields.

    We set sidebarAutoSettleOnMerge to false as a workaround. +1 for landing #26108 or #29820.

    Prepared by Claude Opus 5.5 (claude-opus-5-5) running in T3 Code, and posted with the user's approval.

  8. beausee commented on Nov 8, 2026

    @beausee

    Another occurrence on 0.1.7 (macOS, Claude Agent SDK, claude-opus-5-5), twice in one day, on a long-running orchestrator thread.

    • PRs linked to the thread merged, after the user's last message. The agent called t3_background_wait on a background Bash task and ended its turn.
    • 07:49:32.632Z: task_notification and an empty background_tasks_changed. 07:49:32.651Z: query.close (19 ms).
    • 11:20:17.916Z: the same pair. 11:20:17.955Z: query.close (39 ms). An earlier moment with one task still on the roster did not close.
    • The transcript shows the synthetic "No response requested." Each wake turn was lost.

    The cost is larger for an orchestrator. This thread was directing other threads. Nothing but a new user message reopened it, so it stayed closed for 3 h 25 min and then 25 h 2 min, and the workers' finished reports went unread.

    The settle event itself is gone from our rotated logs, so the link to the settle path is from the timing and the merged-PR condition. We have set sidebarAutoSettleOnMerge to false as the workaround. #26108 would be welcome.

  9. mahirozdin commented on Nov 9, 2026

    @mahirozdin

    I'm adding numbers from my setup. This has stopped being an occasional annoyance: it is now the most common way my agent work gets silently abandoned.

    Setup: T3 Code 0.1.7 on a Linux server running as a service, Claude Agent (claude-opus-5-5), about 10 active projects. "Settle on merge" is at its default. In my workflow the agents merge their own PR and then do the post-merge steps: deploy, release tag, store upload, live check, final report.

    What I measured (read-only pass over statev2.sqlite, Oct 16 – Nov 9):

    • 369 PR merges made inside an agent run, across 280 threads.
    • 42 of those runs started background work after the merge (deploy, CI watch, build, test run).
    • 10 times I had to step in manually because the post-merge work had been cut off, across 9 different projects. Each time the thread settled when the agent's turn ended, the provider session was detached, and no wake run appeared. The next thing in the thread was me asking "what's the status?"
    • 3 more threads were abandoned silently. The agent's last message promised a follow-up ("I'll verify 1.18.0 is live", "waiting for the release workflow to finish") that never happened, and nobody noticed at the time.
    • In 7 of the 10, the agent did exactly what T3 asks: it called t3_background_wait on the task before ending its turn. Once the linked PR has merged, the tool does not protect this path.
    • Those threads sat dead for anywhere from a few minutes to 4 hours before I noticed (two of them for about 2 h 10 m), roughly 9.7 hours in total.
    • It is getting worse. All 10 manual interventions came between Oct 31 and Nov 9. Before 0.1.3 I found one silent case. 0.1.3 shipped fix(server): threads settle right after a shell merge of an unlinked PR #24830 (immediate settle after a shell merge) and fix(threads): Working holds only background work the agent waits on #24610 (only awaited work holds). The settle code is identical in 0.1.5, 0.1.6 and 0.1.7, so this is how things work now, not a one-release regression.

    What it costs in practice:

    • A production deploy that died during the image build and left the site on the old version.
    • A TestFlight build that nobody checked.
    • A store resubmission waiting on a live check that never ran.
    • Final reports that were never written.

    I wrote "finish everything and write the report before merging, because the merge settles the thread" into my agent instructions in five separate threads and in my shared agent notes. It didn't help. Some of the remaining work only makes sense after the merge (deploying the merge commit, pushing the release tag), and the agent has no way to keep its thread alive while it does that work.

    Ask: #26108 looks like the right fix for the awaited case. Going by the data above, it would remove most of these incidents. Could it be prioritized?

    Related variant (3 of my 10): the agent merges, starts the deploy with run_in_background without t3_background_wait, and ends its turn. Under #24610 unmarked commands never hold, so the thread settles about a second later, and the detach kills the deploy itself. Timeline from today:

    06:29:29Z  gh pr merge 90 --merge --delete-branch (linked PR)
    06:30:03Z  remote.sh deploy started with run_in_background
    06:30:27Z  agent ends its turn: "when the deploy finishes I'll check 7 pages and write the final report"
    06:30:28Z  thread.auto-settle -> provider-session.detach "Thread settled."
               deploy log stops during the image build; the site stays on the old version
    08:40Z     I notice and ask what happened
    

    I understand the unmarked rule is intentional because of dev servers, so I won't file this as a bug. Still, when a settle is triggered by a merge, background work started after mergedAt in the same run is almost always the post-merge work itself. If you're open to it, I can write this up as an Ideas discussion. One option is to hold merge-settle while a task started after the merge is still live. Another is to let agents opt their own thread out over MCP, as in #26110.

    Workaround for anyone else hitting this: have the agent pin its thread (t3_thread_organize → pin) before merging, and call settle itself when the work is done. Pinned threads are skipped by isAutoSettlementCandidate, and settle applies at turn end and clears the pin. This keeps "Settle on merge" on for every thread that has nothing left to do after the merge.

  10. spereirag commented on Nov 9, 2026

    @spereirag

    Another reproduction on 0.1.8-nightly.20261105.4234 (desktop app, Linux x64, local server, Claude claude-opus-5-5). ThreadSettlementService.ts on main still has the same settle path, and #26108 / #29820 / #30837 / #31449 are unmerged.

    Timeline (UTC). The thread has 4 linked GitLab MRs, all merged after the last user message:

    • 18:10:57: the agent merges an MR, starts a background Bash loop polling a CI job, registers it with t3_background_wait (matched), and ends its turn at 18:11:03.
    • 18:12:06.023: task_notification, the background command completed (exit 0).
    • 18:12:06.024: background_tasks_changed, the task list is now empty.
    • 18:12:06: thread.auto-settle is accepted, then ProviderSessionManager.detach ("Thread settled.").
    • 18:12:06.053: query.close is sent to Claude. The turn reporting the task result never runs, and the agent only resumed when the user messaged about 70 minutes later.

    Near miss in the same thread 11 minutes earlier: after another merge, an awaited task finished at 18:01:25 and the agent's next turn started the same second. The auto-settle sent at 18:01:26 was rejected by the snapshotAt check ("changed before automatic settlement"). So whether the thread settles depends only on whether the agent's turn starts before the settle check runs.

    Written by Claude Opus 5.5 (Claude Code) via t3 triage.

  11. MARCROCK22 commented on Nov 9, 2026

    @MARCROCK22

    Another occurrence on 0.1.7 (Windows x64, desktop app, Claude Code 2.1.321 through the Agent SDK, claude-opus-5-5). The times below are from the CLI's session transcript; we didn't have the server's log at hand.

    • 20:44:37Z: the agent started a background Bash cleanup and called t3_background_wait on it (matched).
    • 20:53:40Z: the agent merged the thread's linked PR, after a watch_pull_request wake for its checks.
    • 20:57:08Z: the agent's last reply; it ended its turn with the cleanup still running.
    • 21:10:54.927Z: the cleanup's task-notification (completed) was recorded.
    • 21:10:58.282Z: the CLI sent the wake turn's API request. No assistant reply was recorded for it.
    • 21:26:36.815Z: a synthetic "No response requested." ("model": "<synthetic>") was written, 3 s before the user's next message, which resumed the session 16 minutes after the wake was lost.

    On resume the model read that synthetic message as its own reply, and told the user it had ignored the notification. We only found the cause in the transcript's model field.

    Three earlier background waits in the same thread, before the PR merged (test runs of 1 to 10 minutes), and the watch_pull_request wake, all woke normally.

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