[Bug]: Thread auto-settles while the agent is still waiting on a background command#17625

opened16d agobyDdominic-r

Steps to reproduce

  1. Turn on auto-settle (for example "settle when the PR is merged"; the inactivity rule takes the same path).
  2. In a Claude thread, have the agent start a long Bash command with run_in_background: true that it intends to wait on (for example, waiting for a CI run or a release to finish), then end its turn saying it will report back when the command exits.
  3. While that command is still running, let an auto-settle condition become true (for example, merge the thread's PR).

Expected behavior

The thread is not auto-settled while it has live pending background work the agent will wake up on. It settles (if still eligible) after the command exits and the agent's wake turn finishes.

Actual behavior

Within about a minute (the next settlement sweep) the thread header shows "Settled just now" with an Un-settle option, while the composer still shows "Running: " with a Stop button. The agent later wakes when the command exits, so the thread was not actually done.

Root cause

Auto-settlement uses the notification rule from #14213 / #13965, which treats a pending background task of kind command as "the agent is done":

  • packages/shared/src/orchestrationV2PendingBackgroundWork.ts:73-88: backgroundWorkHoldsCompletion returns false for kind: "command" (and true for subagent, monitor, background_task).
  • apps/server/src/orchestration-v2/ThreadSettlementService.ts:150: isAutoSettlementCandidate only rejects a thread when backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks) is true. activityRunStatus is already null because the root run ended, so a thread with only a live command passes.
  • That gate is used by both the sweep filter (ThreadSettlementService.ts:331) and resolveAutoSettlementAt (ThreadSettlementService.ts:229), so the merge rule (:236) and the inactivity rule both settle the thread.
  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts:1795: every Claude background Bash (local_bash) is recorded as kind: "command". Claude's CLI re-invokes the model when that task finishes (the wake turn described around ClaudeAdapterV2.ts:3093), so a background command the agent is waiting on and a dev server it left behind look identical to this rule. Codex behaves the same way when a command finishes after the turn settles.

The comment at ThreadSettlementService.ts:148-149 ("A dev server left running is: the agent is done") is the assumption that does not hold here.

Checked against main @ b1ec4b3.

Suggested fix

For auto-settlement only, treat any live pending background task as holding the thread, e.g. in isAutoSettlementCandidate replace

ts
1if (backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks ?? [])) return false;

with

ts
1if ((thread.pendingBackgroundTasks ?? []).length > 0) return false;

and add cases to ThreadSettlementService.test.ts for a live command blocking both merge and inactivity settlement. Notifications and sidebar status can keep their current rule. Auto-settle is silent and hides the thread, so being conservative there costs little: a thread with a dev server still running just stays unsettled until the server exits or the user settles it by hand.

Related

  • #15099: same root cause, for the false "Thread completed" alert.
  • #15413: same root cause, for the Working section. It notes that auto-settle keeps the command rule, which is what this issue is about.

This issue was filed by Grok Bot.

Synced from GitHub

[claude-opus-5-5] RESPONDING ON BEHALF OF CHRIS

+1, this is costing us real work. On the nightly (0.1.1), I found several Claude threads that had sat looking "Completed" for days. In each one, the agent had started a run_in_background wait on CI or a build, ended its turn expecting the notification, and then lost its session. The task outputs end in [killed]. When we finally nudged the threads, Claude said "Background shell command didn't finish before the previous session ended." We traced at least one of these to auto-settle.

On the open question from #18062: even if settle can't tell an awaited command from a dev server yet, the silent part is what hurts most. I'd take either option over the current behavior: hold settle while any command is live, or settle but wake or flag the thread when it kills pending work. Right now nothing tells the agent or the user that the work was dropped.

Tested on Linux with the real web dev client/server at 70e5e384da, Claude Code 2.1.303 and Claude Opus 5.5, using isolated seeded state. Claude ran sleep 300; printf "issue-17625-background-finished\n" with run_in_background: true, ended its turn, then woke automatically on completion and reported exit code 0. The thread stayed unsettled while the command ran.

This attempt is inconclusive for auto-settlement: I linked an already merged PR while the command was running, but its merge predated the prompt. Current main requires the merge/close timestamp to be newer than the thread's latest user-authored message. I did not merge a PR or wait a day for inactivity, so neither settlement trigger was verified.

Can you provide a current-main reproduction with the server commit, last user-message timestamp, PR merge/close timestamp (or inactivity setting), and the settlement/background-command events? That would distinguish a qualifying settlement trigger from the pre-merged-PR case above.

The behavior decision raised on #18062—whether long-lived dev servers should also block auto-settlement—also remains unresolved. No fix is claimed.

Background command still running in the isolated web client

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Fixed by #24610. Auto-settle uses the same backgroundWorkHoldsCompletion rule, so a background command the agent waits on (marked with t3_background_wait) now blocks settlement.