Skip to content

Provider update is killed when the client's connection drops mid-update #27361

Open

Description

@olschulz

What happened

Updating providers from the app ran claude update on a remote environment. About 202 seconds in, the client's WebSocket reconnected (that server's event loop was stalled at the time). The server.updateProvider call ended as Interrupted, and T3 Code killed claude update while it was still copying the new binary into place. That left an empty version file next to a complete download, and the environment stayed on the old Claude Code version.

Diagnosis

  • The RPC handler runs the update directly on the request: [WS_METHODS.serverUpdateProvider]: (input) => providerMaintenanceRunner.updateProvider(input) (apps/server/src/ws.ts:2421).
  • The update command is spawned inside that request's scope, with Effect.addFinalizer(() => child.kill()) (apps/server/src/provider/providerMaintenanceRunner.ts:102-118). Nothing forks it out of the request.
  • So when the client connection drops and the request is interrupted, the finalizer kills the update process, whatever stage it is in.
  • Checked against v0.1.6-nightly.20261030.3904; this code is the same there.

Possible fix: run the update in a server-owned fiber that outlives the RPC (the client can re-attach to its progress), or at least make the command uninterruptible once it has started.

Steps to reproduce

  1. Have a provider update available on a remote environment (seen with Claude Code).
  2. Start the update from the app.
  3. While claude update runs, drop the client's WebSocket connection (network change, or a stall long enough to trigger a reconnect).
  4. The update ends as interrupted, claude update is killed, and the provider stays on the old version.

Version

0.1.6-nightly.20261029.3892 (remote server). Claude Code 2.1.314.

Environment

T3 Code Desktop on Linux x64 connected to a remote T3 Code server.

Evidence

  • Server trace: server.updateProvider span ended with exit Interrupted about 202 s after start, at the same time as the client reconnect.
  • After the interrupt, Claude Code's install directory held the full new download and an empty version file, and the environment still ran the old version.

Related issues

#8280 (Claude Code provider update hangs, closed) is a different failure in the same flow.

Fix applied or workaround

Run the update again with a stable connection, or run claude update in a shell on the remote host.

Filed by

Claude Opus 5.5 in Claude Code, via T3 Code triage.

Activity

  1. runshotgun commented on Nov 4, 2026

    @runshotgun

    Same defect on Windows with the Codex npm install: the CLI launchers are removed

    What happened (T3 Code Desktop, Windows 11 ARM64, local server, Node 24.19.0, npm 11.17.0)

    1. The app started a Codex provider update: npm install -g @openai/codex (0.165.0 to 0.166.0).
    2. 6 s later, ws.rpc.server.updateProvider ended as Interrupted ("All fibers interrupted without error", from ProviderMaintenanceRunner.runCommand / collectCommandResult). The request finalizer killed npm.
    3. npm had already put the new package in %APPDATA%\npm\node_modules\@openai\codex and moved the old codex, codex.cmd and codex.ps1 aside (.codex-<random> names), but it had not linked the new bin shims.
    4. Result: %APPDATA%\npm\codex.cmd (the provider's binaryPath) did not exist. The Codex provider was broken until a manual npm install -g @openai/[email protected], which linked the shims again and removed the .codex-* debris.

    So on npm installs this failure is worse than an old version: the CLI disappears from PATH. #27056 (OpenCode gone from PATH after an update on Windows) may have the same cause.

    Trace (server.trace.ndjson, times in UTC)

    08:36:24.3  ws.rpc.server.updateProvider            start
    08:36:24.6  ProviderMaintenanceRunner.runCommand    start (npm spawned)
    08:36:30.3  runCommand / runProviderUpdate / updateProvider / ws.rpc.server.updateProvider  exit Interrupted
    

    Seen on the nightly of 2026-11-04 before 0.1.8-nightly.20261104.4165. The handler and the addFinalizer(() => child.kill()) are unchanged in 0.1.8-nightly.20261104.4165.

    Tested fix: run the update in a detached fiber and only join it from the request

    We patched the built server locally (apps/server/src/ws.ts equivalent):

    [WS_METHODS.serverUpdateProvider]: (input) =>
      Effect.forkDetach(providerMaintenanceRunner.updateProvider(input)).pipe(
        Effect.flatMap((fiber) => Fiber.join(fiber)),
      ),

    (In the bundle we wrote the join by hand with Effect.callback + fiber.addObserver, the same as the internal fiberJoin, because Fiber.join is not imported in that chunk.) An interrupt of the request now only removes the join observer. The update keeps its own scope, so the 30-minute UPDATE_TIMEOUT_MS from #28132 still applies, and the command lock is released when the update ends.

    Test with T3's own bundled Effect module: an "update" with addFinalizer (for child.kill) that sleeps 300 ms, forked from a caller that is interrupted after 50 ms:

    original ["finalizer","caller interrupted"]                    (update killed, never finishes)
    patched  ["caller interrupted","update finished","finalizer"]  (update finishes after the drop)
    

    Normal calls still return the update result, and update errors still reach the caller. The patched server starts and runs normally.

    Open point for an upstream fix: a client that reconnects does not re-attach to the running update's progress. The provider maintenance action state still shows it.

    Filed by Claude Opus 5.5 in Claude Code, via T3 Code triage.

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