Skip to content

Restart to update loses idle Claude threads' awaited background tasks, with no restart notice #28208

Open

Description

@r3xsean

What happened

"When restarting via update, threads that are currently watching PRs AND running a background command (such as a test) do not get the 'T3 Code restarted' notification, so the background command is lost and the agent is not notified."

Two Claude threads (A, B) had ended their turn waiting on a long Linux E2E run (run_in_background, explicitly awaited with t3_background_wait). After Restart to update, both sat idle ~13 min until the user typed "the background task ended". A third thread (C), mid-turn in the same restart, was resumed correctly ("T3 Code restarted and resumed this turn").

Diagnosis

Inferred from source and projection; the old server's shutdown spans were not flushed, so their order wasn't observed.

  • Old server's last span 10:41:53Z, new startup 10:42:22Z. Desktop stops backends with instance.stop({ timeout: Duration.seconds(5) }) before quitAndInstall (DesktopUpdates.ts:776-781). No shutdown/reconcile spans in the old server's trace.
  • At the restart, both threads' Claude task list still contained the task.
  • After restart: no restartCancelledBackgroundWork on their runs, no provider-runtime.continue in the outbox, and startup reconcile wrote no events for them (it did for C). So by startup both provider threads had empty pendingBackgroundTasks and stopped sessions; recovery had nothing to record or continue.
  • Likely cause: when an idle Claude query closes, finalizeActiveTurnAfterQueryExit → resetBackgroundTaskStateForNativeThreadProcess (ClaudeAdapterV2.ts:7716, 5431-5494) emits provider_thread.updated with an empty task list. ProviderSessionManager deliberately keeps lists on server_shutdown (readReleasedBackgroundTasks, ProviderSessionManager.ts:754), but the adapter's own update bypasses that. Shutdown runs prepareForShutdown → providerSessions.shutdown → reconcile("shutdown") (serverRuntimeStartup.ts:475-478); once sessions stop the list is gone, so neither reconcile records anything, and continueRestartedRun would also bail (isRestartContinuationSource needs restartCancelledBackgroundWork). If prepareForShutdown doesn't finish within 5 s, no continuation is queued at all.
  • Mid-turn threads are found by run status, so they're unaffected. PR watching isn't the trigger; it makes the "turn ended, waiting on a background task" state common.

Steps to reproduce

Seen on two threads in one update; not confirmed with a minimal repro.

  1. Windows desktop app, local backend, default settings (continueThreadsAfterServerUpdate on).
  2. In a Claude thread, have the agent start a long run_in_background command (e.g. Start-Sleep 600), call t3_background_wait on it, and end its turn.
  3. With an update downloaded, click Restart to update.
  4. Expected: thread is woken with "T3 Code restarted and stopped background work" and the cancelled-work note. Actual: thread stays idle with no note; when the user next sends a message, Claude reports "Background shell command didn't finish before the previous session ended."

Version

Shutdown ran on 0.1.6-nightly.20261030.3940; startup ran on 0.1.6-nightly.20261031.3977 (desktop app, local server)

Environment

Windows 11 10.0.26200 x64, Node v24.22.1, desktop app with local backend, Claude Agent SDK provider (claude-opus-5-5)

Evidence

# Thread A, Claude provider log
10:33:38.545Z background_tasks_changed tasks=[b8zmhm2bk "Linux build and downloads suite", ...]
10:36:33.719Z background_tasks_changed tasks=[b8zmhm2bk]
10:40:25.527Z result success            # turn ends; agent waiting on b8zmhm2bk
# Desktop trace
10:41:24.496Z desktop.updates.applyUpdateDownloaded version=0.1.6-nightly.20261031.3977
10:41:52.433Z desktop.window.handleBackendNotReady
# Old server: last span 10:41:53.348Z, no shutdown/reconcile spans
10:42:22.033Z server.startup.settings.start
10:42:22.067Z ProviderRuntimeRecoveryService.reconcile   # no writes for A/B; C got restart-continuation
10:55:26.233Z query.open resume=<session>                # triggered by the user's message
10:55:26.235Z task_notification b8zmhm2bk status=stopped "Background shell command didn't finish before the previous session ended"
# Projection after restart: restartCancelledBackgroundWork=null on A/B runs; no provider-runtime.continue for A/B

Related issues

#13400 (closed by the Orchestrator v2 rewrite, #2829): same symptom on the pre-v2 engine; the maintainer asked for a new issue if it still happens on a nightly, and it does on v2. #15358 (closed as intended): startup recovery still recorded restartCancelledBackgroundWork there and only the wake-up was missing; here nothing is recorded and the setting is on. #15749 (closed): shutdown preparation cut short, but about Codex's shared session. #24059: too many continuations at once / loop guard, a different problem.

Fix applied or workaround

Nothing applied. Let waited-on background commands finish before Restart to update, or nudge any waiting thread afterwards.

Filed by

Claude Code (claude-opus-5-5) via t3 triage

Activity

  1. lingolooper-simon commented on Nov 1, 2026

    @lingolooper-simon

    Same thing on macOS: desktop 0.1.6 (stable), macOS 26.6.2, Claude Code 2.1.316, Claude Opus 5.5. I'm mentioning it because #28500 puts the cause in the Windows taskkill path and treats macOS as already covered by SIGTERM.

    My idle thread was a delegate_task child, which made it hard to notice. The child showed as done, and the parent had already been told it finished (#16603). Nothing in the app showed any running work. The parent's agent kept telling me on my phone that "one subagent is running". I only found out 45 minutes later by asking twice.

    Timeline (UTC, from thread activity, the desktop trace and the task output file):

    09:03:02.084  child starts the e2e rerun with run_in_background (task b5xlsf63m)
    09:03:04.946  child: t3_background_wait(["b5xlsf63m"]) → matched
    09:03:08.028  child ends its turn: "The rerun … is running; I'll report when it's done."
    09:03:08.344  parent gets "Delegated task … finished" (the early finish from #16603)
    09:03:20.414  Restart to update (desktop.updates.install, backend stop starts)
    09:03:24.716  parent's in-flight run cancelled; child thread updated in the same millisecond
    09:03:24      b5xlsf63m output file: "[killed]"
    09:03:25.024  quitAndInstall
    09:03:49.902  parent: "T3 Code restarted and resumed this turn"
                  child: no restart note, no continuation, no runs since
    

    Earlier in the same child, two background runs followed the same pattern without a restart. Both exited normally and woke the child as expected, so the wait itself works. Only the restart loses it.

    Drafted with Claude Opus 5.5 (Claude Code) on behalf of @lingolooper-simon.

  2. NotAnNvidiaFan commented on Nov 1, 2026

    @NotAnNvidiaFan
    Contributor

    Thanks, this helps. You're right that macOS doesn't go through the path #28500 fixes. There, the desktop's SIGTERM reaches only the backend, because claude runs in its own process group. So the backend shuts down gracefully and queues continuations before it closes providers.

    I tried to reproduce your case through that graceful path on current main. I used a Claude delegate_task child idle on an awaited run_in_background sleep, once with the parent idle and once with it mid-turn, on Claude Code 2.1.316. Both times the child woke after the restart with the restart note.

    One detail in your timeline stands out. Your parent was told the child finished at 09:03:08. In my runs the parent's task stayed open, because the child's background roster still held the awaited task. The task finishes early only when that roster is empty, and an empty roster also leaves restart recovery nothing to wake. So I suspect the child's roster lost, or never got, b5xlsf63m before the restart.

    Could you run this read-only query and paste the output? Put the child's thread id in place of <child thread id>.

    sqlite3 -readonly ~/.t3/userdata/statev2.sqlite "select occurred_at, event_type, json_extract(payload_json,'$.pendingBackgroundTasks') from orchestration_events where stream_id = '<child thread id>' and event_type in ('provider-thread.updated','run.background-work-cancelled') and occurred_at between '2026-11-01T09:02:55' and '2026-11-01T09:04:00' order by sequence;"

    I'm changing #28500 to reference this issue rather than close it, since your case looks separate.

    Drafted with Claude Opus 5.5 (Claude Code) on behalf of @NotAnNvidiaFan.

  3. lingolooper-simon commented on Nov 1, 2026

    @lingolooper-simon

    Here's the output. I shortened each description; it's the long npm run build && npx playwright … command.

    2026-11-01T09:03:02.079Z|provider-thread.updated|[{"taskId":"b5xlsf63m",…,"kind":"command"}]
    2026-11-01T09:03:02.082Z|provider-thread.updated|[{"taskId":"b5xlsf63m",…,"kind":"command"}]
    2026-11-01T09:03:04.941Z|provider-thread.updated|[{"taskId":"b5xlsf63m",…,"awaited":true,"kind":"command"}]
    2026-11-01T09:03:08.017Z|provider-thread.updated|[{"taskId":"b5xlsf63m",…,"awaited":true,"kind":"command"}]
    2026-11-01T09:03:08.019Z|provider-thread.updated|[{"taskId":"b5xlsf63m",…,"awaited":true,"kind":"command"}]
    

    So the roster still held the awaited task when the turn ended, and nothing emptied it afterwards. The only event on the child thread after 09:03:08.028 is provider-session.updated with status: "stopped" at 09:03:24.497. There's no run.background-work-cancelled and no empty roster.

    About the early finish: the child's last run wasn't the original delegate_task task. It was a wake turn after an earlier background run exited. On the parent it appears as an app-owned delegated-task:provider-continuation subagent node. That node completed at 09:03:08.217, when the child's turn ended, with "The rerun … is running; I'll report when it's done." as its result, even though the roster held the awaited task. The original task node had already completed at 08:50:57. So the early finish looks like the follow-up path from #16603 and #21837 rather than an empty roster. That could be the difference from your repro, if your child was still on its original task when you restarted.

    Drafted with Claude Opus 5.5 (Claude Code) on behalf of @lingolooper-simon.

  4. NotAnNvidiaFan commented on Nov 2, 2026

    @NotAnNvidiaFan
    Contributor

    Thanks, that output was exactly what was needed. Your point about the wake turn was the key: with the child on a follow-up turn instead of its original task, I can reproduce this on current main.

    When that follow-up task finishes early (#16603), its record on the parent starts pointing at the child's provider thread. At shutdown, the parent's recovery reaches the child's roster through that link and clears it, but it can't record the cancelled task on the child's run. The child's own recovery then finds nothing, so its continuation is skipped after the restart. This affects every platform, and it's separate from the Windows kill in #28500.

    The fix for this one is in #28994.

    Drafted with Claude Opus 5.5 (Claude Code) on behalf of @NotAnNvidiaFan.

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

    via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions