Skip to content

[Bug]: An awaited background task is lost with no wake or note when a Claude provider process exits while the thread is idle #30156

Open

Description

@paul-cch

Area

apps/server

Steps to reproduce

Self-hosted T3 server (Linux, systemd user service), web client, Claude provider.

  1. Control: in a new Claude thread, ask the agent to run sleep 30 with run_in_background and end its turn. It is woken when the command exits. This works.
  2. In the same thread, ask the agent to run sleep 180 with run_in_background, call t3_background_wait on that task, and end its turn.
  3. After the turn ends, send SIGTERM to that thread's claude process only. Leave the T3 server running. The background sleep dies with the process.
  4. Wait past the 180 seconds.
  5. Send any message to the thread.

Expected behavior

When the provider process exits while the thread is waiting on background work, the agent is woken with a short note saying which background tasks were lost, as the server-restart path does ("T3 Code restarted and stopped background work"). It can then re-arm them.

Actual behavior

No wake and no note. At the moment of the kill T3 records the provider thread as idle with an empty pending-task list, and the session still shows ready. Nothing happens until the next user message (step 5). That message starts a new Claude process, which then reports: "Background shell command didn't finish before the previous session ended".

In normal use this looks like an agent that says "I'll report back when CI finishes" and never does. We've hit it several times with agents waiting on CI, a merge or a long test. The agent only noticed the lost watcher after the user poked the thread. In those real cases we can't tell whether the server restarted or only the provider process died. The repro above isolates the provider-death path.

From reading the source (commit 30654b3), the cause looks like this: when a Claude query process exits and no turn is active, finalizeActiveTurnAfterQueryExit in ClaudeAdapterV2.ts calls resetBackgroundTaskStateForNativeThreadProcess. That clears the pending background tasks and emits an idle provider thread. Nothing records the awaited tasks as cancelled work, and nothing queues a continuation. The startup recovery that handles server restarts never runs here, because the server didn't restart.

Impact

Major degradation or frequent failure

Version or commit

reproduced on 0.1.7-nightly.20261102.4066. Current install is 0.1.7-nightly.20261103.4119 (from t3 --version), not re-tested yet.

Environment

Ubuntu Linux (kernel 6.8), Node 24.19, Claude Code 2.1.317 via the Claude Agent SDK, model claude-sonnet-5-5. Self-hosted server, web client. Codex not tested.

Logs or stack traces

# Timeline from the repro (UTC)
13:35:15  control: background `sleep 30` wakes the agent with no user message
13:35:36  test: `sleep 180` started in background, t3_background_wait called, turn ends
13:35:50  SIGTERM to the thread's claude process; server PID and uptime unchanged
          provider thread -> status idle, pending background tasks []
13:40:40  no wake, no note (2m10s after the job would have finished)
13:40:59  user message -> new claude process -> task_notification:
          "Background shell command didn't finish before the previous session ended"

Workaround

Send any message to the thread after a provider process dies. For work that must finish, have the agent use a bounded foreground wait instead of a background one.

Suggested fix

  1. In the query-exit path with no active turn, check whether the roster being cleared held awaited tasks before clearing it. If it did, record them as cancelled background work on the thread's last run and queue the same continuation that server-restart recovery uses. The agent then wakes with "N background tasks were lost when the provider process exited: …" and can re-arm them.
  2. Longer term, persist registered background waits outside the provider process so a restarted process, or the server, can re-check or re-arm them.

Related

#28208, the same symptom after a server restart (fixes in #28994 and #28500). This report is the provider-process-exit path with the server still up, which that recovery doesn't cover. Also #29319 (subagents killed when the Claude process ends) and #29772 (t3_background_wait succeeding on missing task IDs).

Activity

  1. lojzezust commented on Nov 4, 2026

    @lojzezust

    I'm having similar problems of background processes/watchers randomly disappearing after some time. My workflow requires the use of long-term watchers that wake claude in case of certain events (e.g. training job failures), and this background task flakiness is making T3 a bit unusable in the current state.

  2. royalaid commented on Nov 7, 2026

    @royalaid

    #30214 matches what we saw for a process that exits on its own. Two adjacent paths we hit, in case they belong in the same fix or a follow-up:

    1. Releases T3 starts itself while awaited work is held. ProviderSessionManager releases a Claude session for idle_timeout (including the 4 hour pin expiry) and on runtime_error, and writeReleasedBackgroundWorkEvents then removes the dead tasks from the roster and writes nothing else. A thread whose last run had finished gets no run, no note and no error; it just stops being Waiting. fix(server): keep Claude background work alive past idle pin limit #29354 removes the pin-expiry case for process-bound work. As far as we can tell, fix(server): wake the agent when Claude exits while it awaits background work #30214 treats these as deliberate stops and reports nothing. Our test "idle release of a session past its pin delivers one notice" fails on main.
    2. Orderings around the wake. Once a lost-work wake exists, three orderings dropped or doubled it in our version: a user message queued before the wake whose run attaches a new session first; a second release of that new session before the first wake is handled; and a server restart between the release and the wake (restart recovery cleared the roster and the stopped session was not eligible for continuation, so neither path told the thread). fix(server): wake the agent when Claude exits while it awaits background work #30214's same-transaction design may already handle some of these. Our test names, in case they are useful: "a user message queued before the notice still gets the notice after its run", "a second release before the first notice runs does not tell the thread twice", "a restart between the release and the notice delivers one notice and no restart note for the same work", "a restart after the notice ran does not note the same work again".

    We found path 1 by reading the code and confirmed it with failing tests; we did not capture a live occurrence.

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