Skip to content

Server-update preflight blocks recovery, but t3 service update reports success from a stale CLI that shadows the updated one #13239

Description

@mdmdj

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-bin AUR package via yay -S and ran t3 service update, which replied T3 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 t3 CLI earlier on PATH, 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's launcherProtocol against the target release's SERVICE_LAUNCHER_PROTOCOL (3) and blocks with the "requires a newer T3 Code service launcher" message shown in the client, naming nothing about which t3 it found.
  • apps/server/src/cli/service.ts:156 (also ~L165 for the t3 service update deprecated-alias handler) — t3 service update and t3 service install reconcile against packageJson.version of the CLI running the command. On a machine whose intended CLI (~/.local/bin/t3 from the documented install.sh) never got installed, a month-old globally-npm-installed t3@0.0.40 under nvm's bin dir (~/.nvm/versions/node/v26.8.2/bin/t3) was earlier on PATH than 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.
  • The Arch AUR package [t3code-nightly](https://github.com/usr/bin/t3code-nightly)-bin (verified with pacman -Ql) ships only the desktop app (/usr/bin/t3code-nightly, electron files) — no t3 CLI binary. Docs tell users to install the CLI via curl | sh which puts t3 in ~/.local/bin; nothing warns that a different CLI install method (global npm from an earlier attempt, e.g. under a version manager) shadows it.
  • Independently, t3 update with no --channel flag defaulted to stable 0.0.42 even though the machine tracks the nightly train (client and AUR package are on 0.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

  1. 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

  2. Update the desktop app (e.g. yay -S t3code-nightly-bin) to a release whose
    launcher 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."

  3. 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."

  4. 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

$ which -a t3
~/.nvm/versions/node/v26.8.2/bin/t3   # → .../lib/node_modules/t3/dist/bin.mjs, t3 v0.0.40

$ cat ~/.t3/runtime/service-state.json    # before
{"protocol": 2, "activeVersion": "0.0.40"}

$ t3 service update
T3 Code service is already using t3@0.0.40.   # app/cli/service.ts — matches *its own* packageJson.version

# same cycle repeats every time the client asks; ~/.t3/runtime/versions already
# contains the unused staged runtime 0.0.43-nightly.20260921.2044 before the fix

Related issues

#12020 ( CLOSED, duplicate) — stale service-launcher.mjs on disk with a new runtime layout; here the launcher file was fine, the CLI was the thing that never updated. #12627 — t3 update across 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)

Activity

  1. mdmdj commented on Sep 23, 2026

    @mdmdj
    Author

    From human perspective, I would generally expect a yay -S to 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~

  2. juliusmarminge commented on Sep 23, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed on main at f5ef0ddb9. 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 update printed T3 Code service is already using t3@0.0.40. and the notice came back. which -a t3 showed an nvm global t3@0.0.40 ahead of any ~/.local/bin/t3. The service state stayed {"protocol": 2, "activeVersion": "0.0.40"}, with a staged 0.0.43-nightly.20260921.2044 runtime left unused. Installing a nightly CLI and running t3 service install moved 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 t3 ran, 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-preflight with its own launcher protocol, which is 2:

    https://github.com/pingdotgg/t3code/blob/v0.0.40/apps/server/src/cloud/selfUpdate.ts#L225-L229

    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:

    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.",
    };

    // 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.1795 onward. The staged 0.0.43-nightly.20260921.2044 tree 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 update only repairs the CLI that ran it.

    v0.0.40 has no t3 update command and no deprecation warning. t3 service update is the real command, and its description says it uses this CLI version (npx t3@latest service update for a newer one). When the unit, sentinel, and service-state.json already 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 on PATH is invisible. The deprecated-alias text on current main was not what ran: that copy landed in #11702 and is in v0.0.42+, not in 0.0.40.

    // 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 main prints the deprecation, then the same version-only success line, still with no path. status.current is "this process's version already matches the unit and state," not "this machine has the release the client asked for":

    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@latest and t3 update with no --channel both 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:

    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,

    const currentVersion = packageJson.version;
    const channel = input.channel ?? cliReleaseChannelOf(currentVersion);

    /** 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.sh does the same (T3CODE_CHANNEL defaults to stable). A nightly desktop package does not change that. t3code-nightly-bin is the AppImage only; it does not install a t3 binary or touch the systemd user service:

    # 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

    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:

    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, and main has no older-state adoption. The workaround that worked here avoids that: put a protocol-3 CLI first on PATH, then t3 service install.

    Not a duplicate

    No open issue covers the preflight telling you only to "update on the server machine," or t3 service update reporting success from whichever t3 is first on PATH.

    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> on https://t3.codes/install.sh, or t3 update <client-version> once that CLI is what t3 is), then t3 service install. Say that t3 service update, npx t3@latest, and t3 update with no version stay on the CLI that is first on PATH, and that a desktop or AUR upgrade does not replace it. Point at t3 --version and which -a t3 when 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 t3 binary" as a defect.

    Accepting as a recovery-message bug. The protocol block stays.

  3. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    on Sep 23, 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

    acceptedfeature request acceptedbugSomething 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