Sidebar stays on "Updating Claude" after a successful provider update; update state stuck at running until server restart#24998

opened7d agobyJJunttila

What happened

After upgrading T3 Code and re-pairing the browser, I updated Claude from the app. The update worked (the new Claude CLI was installed and chatting was fine), but the sidebar pill in the bottom left kept showing "Updating Claude" with a spinner. A new tab and a page refresh didn't clear it. Only restarting the server did.

Diagnosis

The provider update ran to completion, but its final update state was never recorded, so the server kept reporting Claude's update as running.

Timeline from server.trace.ndjson for the one server.updateProvider call (local time):

  • 21:21:34.019: setProviderMaintenanceActionState (queued), then running "Checking for the latest version"
  • 21:21:34.548 to 21:22:23.381: ProviderMaintenanceRunner.runCommand (claude update) succeeds; Claude CLI 2.1.310 is written to disk
  • 21:22:23.381: setProviderMaintenanceActionState, which by the code order in runCommandAndVerify is running "Verifying the installed version"
  • 21:22:23.385 to 21:22:34.475: refreshAfterUpdate (it contains checkClaudeProviderStatus, which takes 2.9 s)
  • 21:22:34.475/.476: runCommandAndVerify, runProviderUpdate, withCommandLock and ws.rpc.server.updateProvider all end successfully (no error status)
  • There is no setProviderMaintenanceActionState span after 21:22:23.381. That means finish(...) (succeeded / unchanged / failed) never seems to have written a terminal state, and recordFailedUpdate didn't either.

There are also no enrichProviderSnapshotWithVersionAdvisory spans under refreshAfterUpdate. That suggests verifyRefreshedProvider hit the refreshedProviders.length === 0 branch. On that path the code should still call finish with unchanged, but the trace shows no state write for it.

Since maintenanceActionStatesRef is in memory only, and applyProviderUpdateState (apps/server/src/provider/ProviderRegistry.ts) puts it on every later snapshot, every client that connected afterwards got updateState.status === "running". That includes new tabs and the providers refresh at 21:38. isProviderUpdateActive stayed true, so SidebarProviderUpdatePill showed "Updating Claude" until the process restarted.

A 7 s event loop stall was logged at 21:21:58, while the update command was running. It may or may not be related.

I couldn't confirm from the trace alone why finish didn't run. If the trace dropped that span, the cause is somewhere else, but the symptom (a running state lasting through refreshes and new tabs until restart) is certain.

Steps to reproduce

Not reproduced deterministically. Sequence on this machine:

  1. Run the background service (systemd user unit) on 0.0.40 with a paired browser session through a reverse proxy.
  2. Upgrade the service to 0.1.2.
  3. Re-pair the browser. The old session's server.updateProvider was rejected with The authenticated token is missing required scope: providers:manage.
  4. From the new session, update Claude when it is behind latest (2.1.309 → 2.1.310, native installer at ~/.local/bin/claude).
  5. The update command succeeds, but the sidebar pill stays on "Updating Claude" with a spinner. A new tab, a page refresh, and "Refresh providers" don't clear it. systemctl --user restart t3code.service does.

Version

0.1.2 (background service, upgraded from 0.0.40)

Environment

Arch Linux x64 (kernel 7.2.4), Node v26.8.2, Claude Code CLI 2.1.309 → 2.1.310 (native install); web client: Chrome 156 on Windows via reverse proxy

Evidence

shell
1# server.trace.ndjson span timeline (start → end, local time)
2# state labels in parentheses are inferred from call order in providerMaintenanceRunner.ts; spans carry no attributes
321:21:34.016 → 21:22:34.476 ws.rpc.server.updateProvider status ok
421:21:34.019 → 21:21:34.022 setProviderMaintenanceActionState (queued)
521:21:34.024 → 21:21:34.025 setProviderMaintenanceActionState (running: checking)
621:21:34.546 → 21:21:34.548 setProviderMaintenanceActionState (running: Running claude update)
721:21:34.548 → 21:22:23.381 ProviderMaintenanceRunner.runCommand
821:21:35.564 → 21:21:35.566 setProviderMaintenanceActionState (progress line)
921:22:23.374 → 21:22:23.375 setProviderMaintenanceActionState (progress line)
1021:22:23.381 → 21:22:23.382 setProviderMaintenanceActionState (running: Verifying the installed version)
1121:22:23.385 → 21:22:34.475 refreshAfterUpdate
1221:22:34.475 runCommandAndVerify / runProviderUpdate end status ok
13# no further setProviderMaintenanceActionState spans until the process restarted at 21:44
14
15# earlier attempt from the pre-repair session
1621:20:16 server.updateProvider code 2 "The authenticated token is missing required scope: providers:manage."
17
18# boot-service.log
19[21:21:58.425] WARN (#27): event loop stalled for 7003 ms

Related issues

#24330: the same in-memory maintenanceActionStatesRef that is never cleared without a restart, but there a terminal result message stays around after refresh. Here the state is stuck at running, so a spinner stays up forever. That points to a missing terminal write, not a missing cleanup. Fixing #24330 (e.g. clearing states on refresh) might hide this, but wouldn't explain the missing finish.

Fix applied or workaround

systemctl --user restart t3code.service. That clears the in-memory update state and the pill went away. Separately, a stale 0.0.40 tab that got 426 on /ws after the upgrade was closed.

Filed by

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

Tested on current main 5f1f604733 with the Linux web dev server and an isolated native Claude installation. Ran the app update from 2.1.309 to 2.1.310 twice: once unauthenticated, once with API-key authentication. Both completed normally; the sidebar showed “Claude updated: v2.1.310”, and page/provider refreshes did not leave an active spinner. Each trace included a successful setProviderMaintenanceActionState after refreshAfterUpdate. This did not exercise the 0.0.40 → 0.1.2 service upgrade or the reverse proxy.

Please attach the redacted raw server.trace.ndjson records for the affected update RPC and its child spans, including traceId, parentSpanId and exit, rather than only the summarized timeline. If it happens again, also include the Claude provider snapshot/updateState while the pill is stuck, before restarting. Those records are needed to distinguish a missing terminal write from a later state overwrite or incomplete trace.

Synced from GitHub

Reproduced on macOS. The server did write the terminal state; only the sidebar pill kept a stale running state until a renderer reload.

Environment: T3 Code desktop 0.1.6-nightly.20261031.3956 (local server on the same build), macOS 27.0 arm64, native Claude Code 2.1.314 at ~/.local/bin/claude.

Sequence (UTC)

  1. 05:07:01: "Update everything" relaunched the desktop app, which restarted both the renderer and the local server.
  2. 05:07:06: it ran the Claude provider update. claude update exited 0 and installed nothing, because of #27942 (npm latest is 2.1.316, the native feed's latest is 2.1.314).
  3. 05:07:10.018: the server wrote the terminal state after refreshAfterUpdate (last span below).
  4. The "Update everything" capsule showed Claude: Update command completed, but T3 Code still detects an outdated provider version. That text comes from updateState.message in the server.updateProvider response, so the client did receive the terminal unchanged state.
  5. The sidebar pill, which reads primaryServerProvidersAtom, still showed "Updating Claude" 14 minutes later.
  6. 05:20:56: a renderer reload (Cmd+R) cleared the pill. The server was not restarted.

So in this case it was not a missing terminal write. The terminal state reached the RPC response but apparently not the provider snapshot the pill subscribes to. A plausible trigger is the update starting 5 s after the app and server relaunch, while the client was still reconnecting. I have not confirmed that.

Raw server spans for the update RPC
1{"name":"ws.rpc.server.updateProvider","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"deab43533b622b0a","parentSpanId":"fec924145010e75f","startTimeUnixNano":"1793423226968223416","endTimeUnixNano":"1793423230018432208","durationMs":3050.208792,"exit":{"_tag":"Success"}}
2{"name":"ProviderMaintenanceRunner.updateProvider","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"46760938829b4e2c","parentSpanId":"deab43533b622b0a","startTimeUnixNano":"1793423226968242666","endTimeUnixNano":"1793423230018325208","durationMs":3050.082542,"exit":{"_tag":"Success"}}
3{"name":"setProviderMaintenanceActionState","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"59988927b1ad000f","parentSpanId":"f693885163a7454d","startTimeUnixNano":"1793423226968470083","endTimeUnixNano":"1793423226968698625","durationMs":0.228542,"exit":{"_tag":"Success"}}
4{"name":"ProviderMaintenanceRunner.runProviderUpdate","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"ef6da02430738cfe","parentSpanId":"f693885163a7454d","startTimeUnixNano":"1793423226968710208","endTimeUnixNano":"1793423230018291500","durationMs":3049.581292,"exit":{"_tag":"Success"}}
5{"name":"ProviderMaintenanceRunner.runCommandAndVerify","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"4f5aae53a24c08a8","parentSpanId":"ef6da02430738cfe","startTimeUnixNano":"1793423226968740625","endTimeUnixNano":"1793423230018285458","durationMs":3049.544833,"exit":{"_tag":"Success"}}
6{"name":"setProviderMaintenanceActionState","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"acd3fe3b7bf5d3cc","parentSpanId":"4f5aae53a24c08a8","startTimeUnixNano":"1793423226968906875","endTimeUnixNano":"1793423226969079750","durationMs":0.172875,"exit":{"_tag":"Success"}}
7{"name":"setProviderMaintenanceActionState","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"043327926ca7f27b","parentSpanId":"4f5aae53a24c08a8","startTimeUnixNano":"1793423227413776083","endTimeUnixNano":"1793423227413869500","durationMs":0.093417,"exit":{"_tag":"Success"}}
8{"name":"ProviderMaintenanceRunner.runCommand","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"5a2b0787bb519b37","parentSpanId":"4f5aae53a24c08a8","startTimeUnixNano":"1793423227413888791","endTimeUnixNano":"1793423228735934708","durationMs":1322.045917,"exit":{"_tag":"Success"}}
9{"name":"setProviderMaintenanceActionState","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"b4bc8d6ae62d8317","parentSpanId":"1482b0bdb6405a93","startTimeUnixNano":"1793423228416481250","endTimeUnixNano":"1793423228417123375","durationMs":0.642125,"exit":{"_tag":"Success"}}
10{"name":"setProviderMaintenanceActionState","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"55c9ff21a71fcfca","parentSpanId":"4f5aae53a24c08a8","startTimeUnixNano":"1793423228736007958","endTimeUnixNano":"1793423228736321541","durationMs":0.313583,"exit":{"_tag":"Success"}}
11{"name":"refreshAfterUpdate","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"eb1a2f9b41a6f489","parentSpanId":"4f5aae53a24c08a8","startTimeUnixNano":"1793423228738626958","endTimeUnixNano":"1793423229452145875","durationMs":713.518917,"exit":{"_tag":"Success"}}
12{"name":"setProviderMaintenanceActionState","traceId":"102b1d6255ff4b59948f350b1471b36f","spanId":"7032b132e6d37ff3","parentSpanId":"4f5aae53a24c08a8","startTimeUnixNano":"1793423230018140583","endTimeUnixNano":"1793423230018271666","durationMs":0.131083,"exit":{"_tag":"Success"}}

Workaround: reload the window (Cmd+R). A server restart is not needed.

Filed via t3 triage by Claude Code (claude-opus-5-5).