Repository navigation
Server-update preflight blocks recovery, but t3 service update reports success from a stale CLI that shadows the updated one #13239
Description
Activity
From human perspective, I would generally expect a
yay -Sto update everything. The separation between 't3' and 't3code' is difficult to reason about. I don't understand if I'm using t3code-nightly, do I also need to be on a 'nightly' of t3 and if so, how do I do that? Would using npx updates and yay updates step on each other? It seems consistent with the design philosophy so far to make the whole thing more clear, self-diagnosing and self-healing. Thank you~Triage
Confirmed on
mainatf5ef0ddb9. The nightly in this report (0.0.43-nightly.20260923.2135) already contains it. Not already fixed.What you reported. The desktop client blocked a server update with "This release requires a newer T3 Code service launcher. Update it on the server machine." Updating the Arch desktop package and running
t3 service updateprintedT3 Code service is already using t3@0.0.40.and the notice came back.which -a t3showed an nvm globalt3@0.0.40ahead of any~/.local/bin/t3. The service state stayed{"protocol": 2, "activeVersion": "0.0.40"}, with a staged0.0.43-nightly.20260921.2044runtime left unused. Installing a nightly CLI and runningt3 service installmoved the service to protocol 3 and cleared the notice.The block itself is the protocol gate from #11940. The bug is that the recovery path reports success without saying which
t3ran, and the only release that satisfies this client is a nightly CLI. Stable latest is still protocol 2.What the code does
1. The in-app update is supposed to stop here.
The running 0.0.40 server calls the staged binary's
__service-preflightwith its own launcher protocol, which is 2:https://github.com/pingdotgg/t3code/blob/v0.0.40/apps/server/src/cloud/serviceProtocol.ts#L4
The staged nightly requires protocol 3 and returns that exact sentence. Nothing in the sentence names a command, a protocol number, or a path:
t3code/apps/server/src/cloud/servicePreflight.ts
Lines 23 to 29 in f5ef0dd
if (input.launcherProtocol !== SERVICE_LAUNCHER_PROTOCOL) { return { status: "blocked", version, reason: "This release requires a newer T3 Code service launcher. Update it on the server machine.", }; t3code/apps/server/src/cloud/serviceProtocol.ts
Lines 3 to 5 in f5ef0dd
// Protocol 3 requires the standalone executable layout. Bump when runtimePaths // or the installed runtime tree changes incompatibly; launchers survive self-updates. export const SERVICE_LAUNCHER_PROTOCOL = 3 as const; #11940 added this on 2026-09-15 so an old launcher would not activate a standalone runtime. It is not in stable v0.0.42 (still protocol 2). It is in nightlies from
v0.0.41-nightly.20260916.1795onward. The staged0.0.43-nightly.20260921.2044tree is the preflight failing before the launcher is asked to switch. That part of the report matches the code.2. On 0.0.40,
t3 service updateonly repairs the CLI that ran it.v0.0.40 has no
t3 updatecommand and no deprecation warning.t3 service updateis the real command, and its description says it uses this CLI version (npx t3@latest service updatefor a newer one). When the unit, sentinel, andservice-state.jsonalready match that CLI, it prints the line you saw and exits 0:https://github.com/pingdotgg/t3code/blob/v0.0.40/apps/server/src/cli/service.ts#L136-L146
"Already using t3@0.0.40" was true for that process. It does not print
execPath, so an nvm shim earlier onPATHis invisible. The deprecated-alias text on currentmainwas not what ran: that copy landed in #11702 and is in v0.0.42+, not in 0.0.40.t3code/apps/server/src/cli/service.ts
Lines 142 to 157 in f5ef0dd
// Kept one release for muscle memory and old docs. It did what `t3 service // install` does; the way to move to a newer release is `t3 update`. const serviceUpdateCommand = Command.make("update", serviceReconcileFlags).pipe( Command.withDescription("Deprecated. Run `t3 update` to move to a newer release."), Command.unlisted, Command.withHandler((flags) => runServiceCommand( flags, Effect.gen(function* () { yield* Console.log( "`t3 service update` is deprecated: run `t3 update` to move to a newer release, or `t3 service install` to repair the service. Repairing now.", ); const result = yield* reconcileService({ allowDowngrade: flags.allowDowngrade }); if (!result.changed) { yield* Console.log(`T3 Code service is already using t3@${packageJson.version}.`); return; Current
mainprints the deprecation, then the same version-only success line, still with no path.status.currentis "this process's version already matches the unit and state," not "this machine has the release the client asked for":t3code/apps/server/src/cloud/bootService.ts
Lines 975 to 982 in f5ef0dd
current: problems.length === 0 && normalizeUnit(unit) === normalizeUnit(detectedManager.render(plan)) && runtimeEntryExists && Option.isSome(runtimeSentinel) && runtimeSentinel.value.trim() === input.cliVersion && state?.activeVersion === input.cliVersion && state?.update?.status !== "pending", 3. Following the old help text still cannot clear a nightly client.
npx t3@latestandt3 updatewith no--channelboth follow stable. Latest stable is v0.0.42, which is still protocol 2, so the preflight blocks again. The channel flag defaults to the channel of the CLI that ran, not the desktop app:t3code/apps/server/src/cli/update.ts
Lines 228 to 232 in f5ef0dd
channel: Flag.Literals("channel", CLI_RELEASE_CHANNELS).pipe( Flag.withDescription( "Release channel to follow. Defaults to the channel this t3 was published on.", ), Flag.optional, t3code/apps/server/src/cli/update.ts
Lines 354 to 355 in f5ef0dd
const currentVersion = packageJson.version; const channel = input.channel ?? cliReleaseChannelOf(currentVersion); t3code/packages/shared/src/cliRelease.ts
Lines 89 to 92 in f5ef0dd
/** The release train a version was published on, derived from its prerelease tag. */ export function cliReleaseChannelOf(version: string): CliReleaseChannel { const channel = /^[^-+]+-(nightly|preview)\.\d{8}\.\d+$/.exec(version)?.[1]; return channel === "nightly" || channel === "preview" ? channel : "stable"; install.shdoes the same (T3CODE_CHANNELdefaults to stable). A nightly desktop package does not change that.t3code-nightly-binis the AppImage only; it does not install at3binary or touch the systemd user service:t3code/packaging/aur/README.md
Lines 1 to 7 in f5ef0dd
# AUR packaging This directory maintains the [`t3code-bin`](https://aur.archlinux.org/packages/t3code-bin) and [`t3code-nightly-bin`](https://aur.archlinux.org/packages/t3code-nightly-bin) packages. Both repackage the official x86_64 AppImage from GitHub Releases. ## Publishing Lines 59 to 66 in f5ef0dd
or use a package manager: | Platform | Install | | ------------------ | ------------------------------- | | Windows | `winget install T3Tools.T3Code` | | macOS | `brew install --cask t3-code` | | Arch Linux | `yay -S t3code-bin` | | Arch Linux nightly | `yay -S t3code-nightly-bin` | The host command in the user docs is
t3 update <client-version>. The preflight string never says that, and v0.0.40 cannot run it:Lines 34 to 38 in f5ef0dd
On the host, run: ```sh t3 update <client-version> ``` t3 update <nightly>from a protocol-2 CLI that does have the command (v0.0.42) is the crash-loop in #12627, still open. #12629 is not merged, andmainhas no older-state adoption. The workaround that worked here avoids that: put a protocol-3 CLI first onPATH, thent3 service install.Not a duplicate
- [Bug]: background service self-update fails permanently against a pre-#11510 service-launcher.mjs #11934, closed by fix(server): block updates under legacy service launchers #11940 — the preflight did not fire, so the update failed with a misleading launcher error. Here the preflight fired, as designed.
- Service runtime 0.0.42 cannot be started by a pre-0.0.42 service-launcher (layout changed to Node SEA; launcher not replaced) #12020, closed duplicate — a stale
service-launcher.mjscrash-looped on a 0.0.42 runtime. This service stayed up on 0.0.40. - [Bug]:
t3 updateacross a service-launcher protocol bump leaves the service crash-looping ("Service state is invalid or unsupported") while reporting success #12627, open, and fix(server): the service no longer crash-loops after t3 update crosses a launcher protocol #12629 —t3 updateacross the protocol bump writes protocol 2 next to the new version and crash-loops while reporting success. This host never left 0.0.40 and did not crash-loop.
No open issue covers the preflight telling you only to "update on the server machine," or
t3 service updatereporting success from whichevert3is first onPATH.Fix direction
The string users see is produced by the staged nightly, so a change there reaches hosts still on protocol 2.
Say which protocol was offered and which this release requires. Name the host command that actually moves the launcher: install this exact version (
T3CODE_VERSION=<client-version>onhttps://t3.codes/install.sh, ort3 update <client-version>once that CLI is whatt3is), thent3 service install. Say thatt3 service update,npx t3@latest, andt3 updatewith no version stay on the CLI that is first onPATH, and that a desktop or AUR upgrade does not replace it. Point att3 --versionandwhich -a t3when that version is not the one in the notice.On current CLIs, include the executable path in the "already using" / "already installed" lines. That does not rewrite 0.0.40; the preflight string is what those hosts see.
Leave the channel default as it is. A stable CLI updating to stable is correct. Do not treat "AUR has no
t3binary" as a defect.Accepting as a recovery-message bug. The protocol block stays.
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request accepted
on Sep 23, 2026
What happened
The desktop client told me the server release required a newer service launcher ("This release requires a newer T3 Code service launcher. Update it on the server machine."). On the server machine I updated the
t3code-nightly-binAUR package viayay -Sand rant3 service update, which repliedT3 Code service is already using t3@0.0.40.— every command reported success, yet the update notice kept coming back indefinitely.Full user error on my part for having a second, stale
t3CLI earlier onPATH, but I'd like to report it because the tooling kept telling me I had updated, with no way to spot that the command was operating on the wrong CLI.Diagnosis
Grounded in the installed source for 0.0.40-nightly:
apps/server/src/cloud/servicePreflight.ts:28— the preflight compares the running (old, protocol-2) CLI'slauncherProtocolagainst the target release'sSERVICE_LAUNCHER_PROTOCOL(3) and blocks with the "requires a newer T3 Code service launcher" message shown in the client, naming nothing about whicht3it found.apps/server/src/cli/service.ts:156(also ~L165 for thet3 service updatedeprecated-alias handler) —t3 service updateandt3 service installreconcile againstpackageJson.versionof the CLI running the command. On a machine whose intended CLI (~/.local/bin/t3from the documentedinstall.sh) never got installed, a month-old globally-npm-installedt3@0.0.40under nvm's bin dir (~/.nvm/versions/node/v26.8.2/bin/t3) was earlier onPATHthan both, so the command ran against 0.0.40, found state already matching 0.0.40, and reported success. This explains how a user can keep "updating" and still be blocked.[t3code-nightly](https://github.com/usr/bin/t3code-nightly)-bin(verified withpacman -Ql) ships only the desktop app (/usr/bin/t3code-nightly, electron files) — not3CLI binary. Docs tell users to install the CLI viacurl | shwhich putst3in~/.local/bin; nothing warns that a different CLI install method (global npm from an earlier attempt, e.g. under a version manager) shadows it.t3 updatewith no--channelflag defaulted to stable 0.0.42 even though the machine tracks the nightly train (client and AUR package are on0.0.43_nightly); the channel only followed the CLI's own channel after an explicit--channel nightly. Easy other-version confusion this way too.Steps to reproduce
Install t3 into a node-version-manager global dir and leave it first on PATH
nvm use 26 && npm i -g t3@0.0.40
never run the official installer (curl -fsSL https://t3.codes/install.sh | sh),
so ~/.local/bin/t3 does not exist
Update the desktop app (e.g.
yay -S t3code-nightly-bin) to a release whoselauncher protocol > 2, pair, and let the client ask for a server update.
Client shows: "This release requires a newer T3 Code service launcher.
Update it on the server machine."
On the server machine, follow what looks like the update procedure:
pacman -S t3code-nightly-bin # or yay/pacman equivalent
t3 service update # → "T3 Code service is already using t3@0.0.40."
Reconnect — the same launcher-protocol error appears again.
There is no error from step 3: it succeeded against the shadowed CLI.
Version
0.0.40 (server, pre-fix) → 0.0.43-nightly.20260923.2135 (client and server after fix)
Environment
Linux x64 WSL2 (Arch, kernel 6.18.33.2-microsoft-standard-WSL2), Node v26.8.2 via nvm, systemd user service, desktop client `t3code-nightly-bin 0.0.43_nightly.20260923.2135-1
Evidence
Related issues
#12020 ( CLOSED, duplicate) — stale
service-launcher.mjson disk with a new runtime layout; here the launcher file was fine, the CLI was the thing that never updated. #12627 —t3 updateacross a protocol bump crash-loops the service while claiming success; different failure mode (no crash loop/no state corruption here, just a silent no-op loop). #11934 — self-update against a pre-#11510 launcher; again a launcher-file problem, not a stale/CLI-shadowed-CLI problem.Fix applied or workaround
npm uninstall -g t3 # remove the shim shadowing the newer CLI
T3CODE_CHANNEL=nightly curl -fsSL https://t3.codes/install.sh | sh
t3 update --channel nightly --yes # explicit channel needed, defaults to the running CLI's channel otherwise
t3 service install # rewrote service-state.json to protocol 3 + active nightly
$ cat ~/.t3/runtime/service-state.json # after
{"protocol": 3, "activeVersion": "0.0.43-nightly.20260923.2135"}
Service came up healthy on the new version; the update notice cleared.
Filed by
t3 triage (agent: opencode, model: glm-5.3-flash)