Skip to content

[Bug]: Desktop keeps rebuilding the local environment's shell state, causing hard rerenders (nightly 20261005.2667) #16018

Description

@itsdaymi

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Scope: being sent to the new-thread screen while shell state rebuilds is the cached-shell redirect already tracked in #14689, with a fix open in #14697. This issue is only about the rebuilds themselves: the client keeps recreating environment shell state, which causes the hard rerenders. It also triggers #14689 much more often than the "link to a thread created elsewhere" case it was filed for.

Area

apps/web

Steps to reproduce

  1. Run the macOS desktop app on 0.0.46-nightly.20261005.2667 with only the local environment, a few projects and several threads running at once.
  2. Keep the window in front for a few minutes.

I don't have a deterministic repro. In my session it happened every 5–40 s while the window was visible and never while it was hidden.

Expected behavior

Shell state for the local environment is built once per app session, then kept up to date over the existing subscription.

Actual behavior

EnvironmentShellState.make keeps running for the same local environment: 21 times in about 7 minutes. Each run reloads the IndexedDB cache, re-subscribes and fetches GET /api/orchestration/shell (~1.5 MB). The UI visibly hard-rerenders each time. The desktop sees the app's readiness (appActivation) flip false/true at the same timestamps (DesktopAppActivation.setRendererReady pairs at 12:25:33, 12:29:48, 12:33:48 local).

What the traces rule out:

  • Supervisor teardown. installEntryLocked takes ~0 ms on every 3 s reconcilePlatform, so it returns at the retainEquivalentRuntime check. EnvironmentRpc.getInitialServerConfig doesn't repeat.
  • A followStream restart from an entries write. None of the writers of entries show up during the rebuilds: no setCompatibility, setEnabled, learnRoutes, replaceRoutesLocked or removePlatformEnvironment spans.
  • A connection blip. EnvironmentSupervisor.signal / rpcSession.probe fired only 3 times and not at the rebuild timestamps.
  • A renderer restart or page reload. Same renderer PID throughout.
  • Other timers. No consistent correlation with EnvironmentServerConfigState.persist, preview automation focusHost calls, or the 3 s registrations poll.

That leaves the shell stateAtom(environmentId) atom in packages/client-runtime/src/state/shell.ts itself. It isn't keep-alive, so if every component subscribed to it unmounts at once, it is disposed and rebuilt on the next mount. I couldn't confirm what unmounts its subscribers without a debugger on the packaged renderer.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Nightly) 0.0.46-nightly.20261005.2667 (37de6cb), desktop.

This looks like a regression in this build: setRendererReady flaps went from a few per day up to the 11:37 auto-update from 20261004.2657 to every few minutes afterwards. I looked at the two client-runtime connection changes in that range, #15467 and #15468, and the traces don't implicate them for the local platform environment (see above). None of the 6 commits on main after 37de6cb touch shell.ts, registry.ts, runtime.ts or ThreadRouteView.tsx.

Environment

macOS (Apple silicon), Electron 44.4.2 / Chrome 152, desktop app with only the local environment, background activity profile performance. Several Claude Agent and Codex threads running at the same time, one using preview automation.

Logs or stack traces

# server.trace.ndjson (client otlp spans), local time
12:24:59 EnvironmentShellState.make
12:25:05 EnvironmentShellState.make
12:25:33 EnvironmentShellState.make   # + setRendererReady false/true
12:25:42 EnvironmentShellState.make
12:25:50 EnvironmentShellState.make
12:26:11 EnvironmentShellState.make
12:26:53 EnvironmentShellState.make
...
12:29:48 EnvironmentShellState.make   # + setRendererReady false/true
...
12:31:17 EnvironmentShellState.make

# one rebuild (ms relative to make); make is a root span
-908  EnvironmentRegistry.reconcilePlatform / installEntryLocked (0 ms, runtime retained)
 -13  EnvironmentServerConfigState.persist
  -1  EnvironmentRegistry.acquireSupervisor
   0  EnvironmentShellState.make
 +24  environment.initialSync -> fetchEnvironmentShellSnapshot -> GET /api/orchestration/shell (200, 1.48 MB)

Workaround

None reliable. #14697 would stop the redirect half. Rolling back to 0.0.46-nightly.20261004.2657 might help if this is a regression in today's build.


Investigated with Claude Code (Claude Opus 5.5) from local trace logs, the shipped bundle and the source at 37de6cb.

Activity

  1. changed the title [-][Bug]: Desktop UI keeps rebuilding environment shell state and kicks the open thread to the new-thread screen (nightly 20261005.2667)[/-] [+][Bug]: Desktop keeps rebuilding the local environment's shell state, causing hard rerenders (nightly 20261005.2667)[/+] on Oct 5, 2026
  2. itsdaymi commented on Oct 5, 2026

    @itsdaymi
    Author

    Narrowed the scope after checking merges: the redirect to the new-thread screen is a duplicate of #14689 (fix open in #14697). This issue now covers only the repeated shell-state rebuilds that keep triggering it.

  3. juliusmarminge commented on Oct 5, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for the really thorough trace, @itsdaymi! Your span timings lined up closely with what's in the code, and the redirect path is still present on current main (cf3e714b0f).

    What I found

    The redirect. ThreadRouteView treats any shell snapshot as a finished bootstrap:

    // apps/web/src/components/ThreadRouteView.tsx:75
    const bootstrapComplete = shell.data?.snapshot._tag === "Some";

    resolveThreadRouteRenderState then returns "missing" when that snapshot has no row for the open thread, and the effect at lines 144–158 navigates to / whenever the environment still has other threads. A rebuilt EnvironmentShellState seeds from cache.loadShell, and setSynchronizing keeps that snapshot while flipping status to "synchronizing", so the first value the route sees is the cached snapshot. The authoritative GET /api/orchestration/shell lands about 24ms later. Since shell cache writes are delayed (500ms, then every 10s in runCachePersistence), a recently created thread is often missing from the cache, which is why recent threads are the ones that get dropped. Elsewhere, createAllEnvironmentProjectSnapshotsReadyAtom already requires status === "live" before trusting a snapshot.

    The repeated rebuilds. These look like restarts of the shell stream rather than a new supervisor. shellStateChanges and the server-config stream both go through followStream, which switchMaps to a new effect when the environment's catalog entry isn't Equal. That matches your persist, acquireSupervisor, make, then shell fetch sequence. installEntryLocked with retainEquivalentRuntime returns before writing the catalog, so the 3s platform poll alone doesn't seem to be the trigger, and learnRoutes returns early for a platform primary route. The shell atom also isn't Atom.keepAlive, so a gap with no subscribers would rebuild it too. The writer that changes the entry between makes hasn't been pinned down yet; tracing whether entries changes, and whether learnRoutes, setEnabled, or setCompatibility runs at those moments, would be the next step.

    Related: #14689 covers the same cached-snapshot redirect for thread links, and open PR #14697 ("fix(web): keep thread links open until the shell is live") addresses that redirect half but not the rebuild loop.

    Likely fix area

    • The redirect in ThreadRouteView / resolveThreadRouteRenderState: one option is treating "cached" and "synchronizing" as "loading" rather than "missing", matching the "live" bar used elsewhere (overlapping with fix(web): keep thread links open until the shell is live #14697).
    • The rebuild loop in followStream and whichever catalog writer is changing the environment entry, or the shell atom's lifetime.

    A maintainer will decide on the fix direction.

  4. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 5, 2026
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

    bugSomething is broken or behaving incorrectly.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