First launch after a desktop update can start the backend in web mode without the bootstrap secret, so the desktop's own login is rejected (invalid_credential)#17367

openedOct 8, 2026byFfreddy-d

What happened

I'm on T3 Code nightly on Windows. Always when I update the app and restart it, this error appears:

1Something went wrong.
2Primary environment request failed during fetch-session-state (HTTP 500).

Fully quitting and reopening the app makes it work again. "Try again" and "Reload app" don't help (see #3513).

Diagnosis

On the first launch after an update, the desktop's own backend can start without its desktop bootstrap envelope. It then runs as a plain web mode server with no desktopBootstrapSecret and no desktopBootstrapToken, so it rejects the desktop's credential.

  1. The desktop spawns the backend with --bootstrap-fd 3 and writes the JSON envelope (mode, desktopBootstrapSecret, etc.) to that fd at spawn (apps/desktop/src/backend/DesktopBackendManager.ts:449-507).
  2. The server reads it in readBootstrapEnvelope (apps/server/src/bootstrap.ts:74) with a default 1000 ms Effect.timeoutOption (bootstrap.ts:86, :145). A timeout resolves to Option.none(), and startup continues silently.
  3. With no envelope, mode falls back to "web" (apps/server/src/cli/config.ts:288-294). desktopBootstrapSecret and desktopBootstrapToken are both undefined, so PairingGrantStore neither accepts rotating desktop tokens nor seeds a desktop grant (apps/server/src/auth/PairingGrantStore.ts:330-350).
  4. The desktop's POST /oauth/token with currentDesktopBootstrapToken(secret, now) returns invalid_credential. isTransientBearerBootstrapError (apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts:27) treats that as final, so it surfaces as DesktopLocalEnvironmentAuthSessionBootstrapError and the renderer's root beforeLoad fails.

Why it happens after updates: the first launch of a freshly installed server.asar is a cold start (no compile cache, and likely Defender scanning new files). On this machine that backend took about 9 s from spawn to initialized services (spawn 23:39:54.8Z, services up about 23:40:03–05Z). My hypothesis is that a long synchronous stretch blocks the event loop past 1 s around the envelope read. When the loop wakes up, the expired timer is handled before the pending pipe read (Node runs timers before poll I/O), so the envelope is dropped even though it is already in the pipe. I couldn't confirm the timeout directly because no readBootstrapEnvelope span appears in the trace. But the result, a desktop-spawned backend running with server.mode: "web", is confirmed, and the other Option.none() paths (fd not stat-able, stream closed before a line) seem unlikely for a pipe the desktop has just written to.

The core problem is that a backend launched with an explicit --bootstrap-fd silently degrades to web mode instead of waiting longer or failing loudly. Possible directions, for maintainers to decide:

  • When --bootstrap-fd is given explicitly, wait much longer for the envelope, or until EOF.
  • Exit with a clear error instead of starting in web mode, so the desktop's supervisor restarts it.
  • Have the desktop verify that the backend reports desktop mode before treating it as ready.

Steps to reproduce

Not deterministic. It depends on how cold the first post-update start is.

  1. Install T3 Code (Nightly) desktop on Windows, using the Windows backend (not WSL-only).
  2. Let it download a nightly update, then click install/restart.
  3. On the first launch of the new version, the app often shows "Primary environment request failed during fetch-session-state (HTTP 500)". In this user's logs it happened on 2 of the last 3 updates.
  4. server.trace.ndjson for that launch shows server.startup.welcome.autobootstrap with server.mode: "web", followed by failing environment.auth.token spans.
  5. Fully quit and relaunch: the backend starts in desktop mode and login succeeds.

It might reproduce more reliably by artificially delaying the server's startup before readBootstrapEnvelope by more than 1 s. I didn't verify this.

Version

Desktop 0.0.46-nightly.20261008.2833 (tag v0.0.46-nightly.20261008.2833, a6ec88f7a7); bootstrap.ts unchanged on main as of 2026-10-08

Environment

Windows 11 Home 10.0.26200 x64, desktop app with local Windows backend on port 3773 (no WSL backend, no background service)

Evidence

shell
1# Update installs and app startups (desktop.trace.ndjson)
22026-10-07T20:20:01Z desktop.updates.install
32026-10-07T20:20:40Z desktop.startup -> FAILED (backend in web mode)
42026-10-07T20:20:57Z desktop.startup -> OK (manual relaunch)
52026-10-08T13:22:44Z desktop.updates.install
62026-10-08T13:23:23Z desktop.startup -> OK
72026-10-08T23:39:15Z desktop.updates.install
82026-10-08T23:39:53Z desktop.startup -> FAILED (backend in web mode)
9
10# Failing launch: desktop side
1123:39:54.760Z desktop.backendInstance.start {"id":"primary"}
1223:39:54.821Z http.client GET /.well-known/t3/environment Failure (backend not up yet)
1323:40:06.857Z http.client GET /.well-known/t3/environment Success
1423:40:07.519Z http.client POST http://127.0.0.1:3773/oauth/token
1523:40:07.514Z desktop.localEnvironmentAuth.getBearerToken Failure
16 DesktopLocalEnvironmentAuthSessionBootstrapError: Failed to create the local desktop bearer session.
17 [cause]: EnvironmentAuthInvalidError: The environment rejected this client's credentials (invalid_credential).
18
19# Failing launch: server side (same traceId 74529fb7…, so the same backend handled the request)
2023:40:05.694Z server.startup.welcome.autobootstrap {"server.mode":"web","server.port":3773,"server.host":"default"}
2123:40:07.523Z environment.auth.token Failure EnvironmentAuthInvalidError (invalid_credential) url.path=/oauth/token
2223:40:07.633Z environment.auth.token Failure EnvironmentAuthInvalidError (invalid_credential)
23
24# Oct 7 failure shows the same pattern
252026-10-07T20:20:50.958Z server.startup.welcome.autobootstrap {"server.mode":"web",...}
262026-10-07T20:20:52.867Z environment.auth.token Failure
27
28# Only one listener on the port; it is the desktop-spawned backend
29LocalAddress LocalPort OwningProcess
30127.0.0.1 3773 22848 # T3 Code (Nightly).exe ... server.asar\apps\server\dist\bin.mjs --bootstrap-fd 3 (child of the desktop main process)

#16826: same invalid_credential symptom, but there a WSL service takes the port via wslrelay.exe; here only the desktop's own backend listens and it reports server.mode web. #3513: why the error screen needs a full quit. #12918/#12919: retries only cover transient 502-504, invalid_credential is final.

Fix applied or workaround

Nothing was changed on the machine. Workaround: when the error appears, fully quit T3 Code (including the tray icon) and reopen it so the backend is respawned.

Filed by

Claude Code (Claude Opus 5.5, claude-opus-5-5) via t3 triage

Note

Grok responding on behalf of Julius.

Thanks for the detailed traces. Triage notes from current main (393ff4c):

What the code does on a missed envelope

Why it might miss the window after an update (not verified)

  • The 1 s timer only starts once readBootstrapEnvelope runs, so a slow module load before that point should not count against it on its own. Something has to stall the read after the timer is armed.
  • On Windows there is no /proc or /dev/fd path (bootstrap.ts#L233-L241), so the envelope is read via fs.createReadStream on the pipe fd (L207-L214). Reads on that path go through the libuv threadpool (see the analysis in #14776 for the telemetry fd). On a cold first launch with Defender scanning freshly installed files, threadpool contention or a long synchronous stall, as you describe, could plausibly push the read past 1 s even with the envelope sitting in the pipe. Neither has been reproduced here.
  • No test covers the timeout-then-web-mode fallback; bootstrap.test.ts only exercises the success, unavailable-fd and error paths.

Desktop side

Possible fix directions (for maintainers to choose)

  1. Fail closed on the server: when --bootstrap-fd/T3CODE_BOOTSTRAP_FD is set and no envelope arrives, exit nonzero with a clear error instead of starting in web mode, so the desktop supervisor restarts it. Smallest change; it does turn a slow start into a restart loop if the stall repeats.
  2. Wait longer: raise the timeout for an explicit fd (or wait until a line or EOF), possibly with a log line when the envelope is late. On Windows, reading the pipe through net.Socket instead of fs.createReadStream, as #14776 does for telemetry, may remove the threadpool dependency.
  3. Detect the mismatch on the desktop: check the auth descriptor (expecting desktop-managed-local) or treat invalid_credential from the primary local backend as "respawn once" before surfacing the error.

Options 1 or 2 address the root cause; option 3 is a safety net and could pair with either.

Related

  • #16826: same invalid_credential symptom, different cause (WSL service on the same port). Not a duplicate.
  • #3513: sticky error UI with no recovery.
  • #12918 / #12919: transient bearer bootstrap retries, which don't cover a rejected credential.
  • #14776 (open): same fs.createReadStream-on-pipe pattern for the telemetry fd.
  • I didn't find an open or merged PR that changes the envelope timeout or the silent web-mode fallback.

Existing fix: #17568, still open at d52b1cc7fb. No replacement PR created. Its recorded verification covers focused bootstrap/config tests and real Linux CLI launches; the original Windows post-update trigger remains unverified. This Linux triage run did not reproduce the Windows/Electron update sequence. Handing the existing PR to Julius because the fix changes bootstrap credential delivery and runtime mode selection, which this triage flow reserves for maintainer approval.

Fresh triage on main at 49e97d48f9 reproduced the missing-envelope failure mode with the real Linux CLI: empty or delayed bootstrap input allowed startup, then the desktop credential exchange returned invalid_credential. Existing #17568 at d52b1cc7fb rejects empty/invalid/malformed input before state creation and accepts valid input delivered at 12 seconds (OAuth 200); all 33 focused tests pass. Preserved the existing fix, added via-triage, and updated its proof. Native Windows/Electron post-update startup remains unverified; the Linux injection does not establish the original cold-start cause.

Synced from GitHub

Another occurrence on Windows nightly (0.1.3-nightly.20261024.3604). This one was not the first launch after an update, so updates are not the only trigger. Any slow cold start seems to be enough.

Timeline (UTC)

  • 10-24 03:28: nightly installed (asar mtime)
  • 10-24 16:05: normal launch. Backend ready in ~6 s, desktop /oauth/token succeeded.
  • 10-25 04:14: launch with the same build. Backend started in web mode without the bootstrap envelope.

Evidence from the bad launch (server.trace.ndjson / desktop.trace.ndjson)

  • desktop.backendInstance.start at 04:14:24.46. /.well-known/t3/environment gave transport errors until 04:14:27.3, then 503s until it succeeded at 04:14:28.5.
  • Every desktop POST /oauth/token was rejected: 35 of 35 attempts (2 at 04:14:29–30, 33 at 04:48:03–08 after "Reload app"), all EnvironmentAuthInvalidError ... (invalid_credential) in about 1–3 ms.
  • Server spans present only in the bad run: EnvironmentAuth.issueStartupPairingUrl, EnvironmentAuth.issueStartupPairingCredential, server.startup.welcome.autobootstrap, environment.auth.browserSession, PairingGrantStore.consume. A browser session was then created from http://localhost:3773, so the server opened the default browser as in web mode.
  • Server spans present only in the good run: resourceTelemetry.desktopTelemetryReceiver.writeAllToFileDescriptor and recordSampleHealth. The desktop telemetry fd from the envelope was never used in the bad run.
  • The backend process was the child of the current desktop process, with no stale backend on :3773, so a secret mismatch from a leftover server is ruled out.

This matches the diagnosis above: readBootstrapEnvelope hits its 1000 ms timeoutOption or gets nothing, startup continues silently in web mode, and the only recovery is to fully quit and relaunch. Looking forward to #17568. Making a missing envelope fatal (or retried) in desktop mode, and logging it, would also make this much easier to spot.

Synced from GitHub

Another occurrence on Windows nightly 0.1.7-nightly.20261101.4035, on the first launch right after a desktop auto-update.

Environment

  • Windows 11 Pro 10.0.26200, x64
  • Desktop app (Nightly channel) against its own local backend on 127.0.0.1:3773. No WSL backend and no other server on the port.
  • Previous build: 0.1.6-nightly.20261030.3904. The update was installed at 17:54Z.

Timeline (UTC, 2026-11-01)

  • 17:55:05: desktop starts and the backend is spawned.
  • 17:55:07–17:55:15: /.well-known/t3/environment returns transport errors, then 503s until about 17:55:19.
  • 17:55:20–17:56:11: readiness probes are Interrupted.
  • 17:56:12: the first 200, about 67 s after spawn. Normal starts on this machine take a few seconds.
  • 17:56:28: the desktop's POST /oauth/token fails with EnvironmentAuthInvalidError ... (invalid_credential). The UI shows PrimaryEnvironmentRequestError / DesktopLocalEnvironmentAuthSessionBootstrapError: Failed to create the local desktop bearer session.

Evidence that the backend fell back to web mode (server.trace.ndjson)

  • server.startup has the attributes "server.mode":"web", "server.port":3773.
  • Spans that only appear on the web-mode path: server.startup.welcome.autobootstrap, EnvironmentAuth.issueStartupPairingCredential, EnvironmentAuth.issueStartupPairingUrl, PairingGrantStore.issueOneTimeToken, server.startup.browser.open.
  • 12 of 12 desktop exchanges failed with PairingGrantStore.consume → UnknownBootstrapCredentialError and then ServerAuthInvalidCredentialError.
  • server-runtime.json records the backend as serverVersion: 0.1.7-nightly.20261101.4035, so the same build as the desktop. A stale or old-version server is ruled out.
  • No readBootstrapEnvelope span appears in the trace, so the miss itself left no log line. This matches the silent Option.none() fallback described above.

This is another data point that a slow cold start after an update on Windows pushes the envelope read past the 1 s window. It also supports #17568, or at least logging and failing when --bootstrap-fd is set and no envelope arrives.

Produced by T3 Code triage with Claude Code (Claude Opus 5.5).

Synced from GitHub

Another occurrence on Windows, plus a direct reproduction of the dropped envelope with the real readBootstrapEnvelope on Windows, which this thread lists as unverified.

Occurrence

  • Build: v0.1.10-nightly.20261112.4580, built locally with an unrelated desktop tray patch. apps/server/src/bootstrap.ts and the backend launch path are unmodified.
  • Trigger: the first launch after an unclean OS reboot (the machine bugchecked overnight). No update was involved. The same build had started normally the day before.
  • Windows 11 26100 x64, local Windows backend on 127.0.0.1:3773, no WSL backend.

Timeline (UTC, 2026-11-13):

  • 09:01:17 desktop.backendInstance.start. /.well-known/t3/environment gives transport errors, then 503s, and is ready at 09:01:24 (about 6.9 s).
  • 09:01:22 server server.startup has "server.mode":"web", "server.host":"default". The two later launches that worked report "server.mode":"desktop", "server.host":"127.0.0.1".
  • 09:01:24 to 09:01:44: all 20 desktop POST /oauth/token attempts fail with PairingGrantStore.consume → UnknownBootstrapCredentialError. No desktopBootstrapToken.isValid span appears, so the server had no secret.

Reproduction of the dropped envelope

A child process calls the real readBootstrapEnvelope(schema, 3) from apps/server/src/bootstrap.ts. The parent passes fd 3 as a pipe and writes one envelope line, as the desktop does. Windows 11, Node 26.12.1, six runs per row:

Default timeoutScenarioEnvelope read
1000 msno delay6/6
1000 msenvelope already in the pipe, event loop blocked 1.5 s right after the read starts2/6
1000 msenvelope already in the pipe, event loop blocked 3 s right after the read starts3/6
1000 msparent writes the envelope 1.5 s late0/6
30000 msevent loop blocked 3 s4/4
30000 msparent writes 1.5 s late4/4
30000 msparent writes 5 s late4/4

So the hypothesis in the issue holds on Windows: with the envelope already sitting in the pipe, a synchronous stall longer than the timeout makes the read resolve to Option.none(). The runs that still succeed under a stall are the ones where the pipe read completed before the stall began.

The stall is injected like this, right after the read is started:

js
1const promise = Effect.runPromise(readBootstrapEnvelope(schema, 3));
2setImmediate(() => { const end = Date.now() + stallMs; while (Date.now() < end) {} });
3const result = await promise; // Option.none() when the stall outlasts the timeout

What this does not show: that the 09:01 launch above hit this exact path. That launch left no readBootstrapEnvelope span or log line, so only the outcome (a desktop-spawned backend in web mode) is confirmed.

Produced with Claude Code (Claude Fable 5.2).