Repository navigation
[Bug]: background service self-update fails permanently against a pre-#11510 service-launcher.mjs #11934
Description
Activity
Triage
Confirmed on
main@7235701de0. This is a real bug, not a duplicate of #8345 / #11767 / #5196 / #9370.In-app Update server never rewrites the boot-service unit or
~/.t3/runtime/service-launcher.mjs. That is intentional: docs/internals/server-updates.md says children request a handoff over IPC and do not replace their service definition. A layout change therefore has to tripSERVICE_LAUNCHER_PROTOCOL, or an old launcher will keep supervising new runtimes with a staleentryPath.What the code does today
- Pre-feat(server): manage runtimes as release archives only, never from npm #11510 launchers resolve
<versionDir>/node_modules/t3/dist/bin.mjs(reporter’s Aug 10service-launcher.mjs). - Current
runtimePathsinserviceLauncher.ts(changed in af6c138 / feat(server): manage runtimes as release archives only, never from npm #11510) requires<versionDir>/t3. After feat(release): publish npx t3 as a launcher over per-platform executable packages #11607 / chore(server): keep the legacy service entry point to the npm package only #11770 the archive has not3package, soruntimeExists()is false and the launcher repliesupdate-rejectedwith “The requested target runtime is missing or incomplete.” runServicePreflightalready has the right message (“This release requires a newer T3 Code service launcher…”), butSERVICE_LAUNCHER_PROTOCOLis still2(last bumped in fix(server): allow remote updates with database migrations #5374 for SQLite snapshots).selfUpdate.tspasses the running server’s protocol into the staged binary, so both sides report2and preflight returnsready.- Current
bootService.tsnever writesservice-launcher.mjs. New installs useExecStart=<versionDir>/t3 __service-launcher. Those hosts are fine. Anyone who has only used in-app Update server since before feat(server): manage runtimes as release archives only, never from npm #11510 is stuck.
.1722was the last version that installed because #11732 wrote a legacynode_modules/t3/dist/bin.mjsinto archives. #11770 removed that the same day, on the assumption that “an old launcher never installs an archive.” That is wrong for this path: the running server downloads the archive; the persisted launcher only has to accept the handoff..1735/.1766therefore look complete to the installer and incomplete to the old launcher.The npm-only shim in
legacyCliLauncher.tsdoes not help archive self-update. Its comment that “the first server started this way rewrites the service unit” is also false for the in-app path.Workaround (confirmed by reporter)
On the server machine, from a current on-disk binary:
~/.t3/runtime/versions/<newest>/t3 update --channel nightly --yes
That rewrites the unit to
ExecStart=<versionDir>/t3 __service-launcherand unblocks later in-app updates.Suggested fix
- Bump
SERVICE_LAUNCHER_PROTOCOLto3and comment that anyruntimePaths/ installed-tree change is a launcher compatibility break. The next nightly’s preflight will then block with the existing actionable message instead of the false “runtime missing” error. Users still need a localt3 updateto actually move — that is the designed escape hatch. - Do not treat restoring fix(release): preserve updates from npm-based services #11732’s archive
ensureLegacyEntryas the durable fix. It would let old.mjslaunchers succeed once more, but it leaves them in place and fights chore(server): keep the legacy service entry point to the npm package only #11770.
boot-service.logvanishing while systemd still holds the fd looks separate: nothing in-repo unlinks that file.No open PR covers this. Accepting as a bug.
- Pre-feat(server): manage runtimes as release archives only, never from npm #11510 launchers resolve
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 15, 2026 Independent confirmation on the stable
0.0.42release, with an additional systemd drop-in trigger that may not be covered by #11940.Environment:
Ubuntu 24.04.5 LTS x86_64 kernel 6.8.0-139-generic Node v24.21.0 systemd user service command: npx --yes t3@latest service update --base-dir ~/.t3Observed sequence:
-
service updateinstalled the complete standalone0.0.42runtime and rewrote the base unit to:ExecStart=~/.t3/runtime/versions/0.0.42/t3 __service-launcher -
An existing local drop-in still contained:
ExecStart= ExecStart=%h/.local/bin/node %h/.t3/runtime/service-launcher.mjs
-
Consequently,
systemctl --user show t3code.service -p ExecStartstill resolved to the legacy Node launcher after the update.service-state.jsonselected0.0.42, whose valid standalone layout intentionally has nonode_modules/t3/dist/bin.mjs. -
The legacy launcher rejected the active runtime as missing/incomplete. systemd attempted five starts and left the service failed with no listener. The update command nevertheless logged completion.
-
Rolling
activeVersionback to0.0.40, resetting the failed unit, and restarting restored the service immediately.
This confirms the compatibility failure against stable
0.0.42. It also leaves a question beyond the in-app preflight fixed by #11940: does a localservice updatevalidate the effective systemdExecStartafterdaemon-reload? In this case the base unit was correct, but anExecStart=drop-in silently defeated it and the CLI still reported success.A possible hardening would be for
service install/updateto inspect the effectiveExecStartafter writing/reloading the unit and either refuse activation or report the overriding drop-in path when the expected standalone launcher is not effective.-
Hit this too, from a slightly different angle. I run a small script on a cron that connects to the server over a WebSocket, and it was borrowing
wsstraight out of~/.t3/runtime/versions/<newest>/node_modules. After the auto-update to 0.0.42 that folder no longer hasws, so the script died withCannot find module .../0.0.42/node_modules/ws.Makes sense in hindsight now that the server ships as a single binary with
wsbundled inside. I've fixed my side by installingwsas a normal dependency of my own script instead of reaching into the runtime folder. No action needed from me, just mentioning it in case other folks have external scripts that resolve packages from the runtime directory. Might be worth a line in the release notes if it isn't already.
Area
apps/server
Summary
In-app self-update never rewrites
~/.t3/runtime/service-launcher.mjs, so every host running the background service is still using whichever launcher its last CLI-driven install wrote. #11510 changed how the launcher resolves a runtime's entry point, and #11607 changed what the installed runtime tree contains — together they make those persisted launchers reject every new release.The compatibility gate for exactly this case already exists (
runServicePreflightblocks on alauncherProtocolmismatch with "This release requires a newer T3 Code service launcher"), butSERVICE_LAUNCHER_PROTOCOLwas left at2through #11510, so it never fires. The user gets a misleading error instead.Steps to reproduce
service-launcher.mjswas written 2026-08-10) and let it self-update through the app for a few weeks. The launcher file is never replaced.0.0.41-nightly.20260914.1722— the last release whose runtime archive still containednode_modules/t3/dist/bin.mjs.0.0.41-nightly.20260915.1735or newer.Expected behavior
Either the update lands, or it is blocked up front with the actionable message that
servicePreflight.tsalready contains: "This release requires a newer T3 Code service launcher. Update it on the server machine."Actual behavior
The update fails, identically and permanently, on every attempt:
ready.Server update failed: The requested target runtime is missing or incomplete.That message is wrong in a way that costs debugging time: the target runtime is complete and was just successfully executed by the same code path. What is actually stale is the launcher.
The mechanism:
entryPathas<versionDir>/node_modules/t3/dist/bin.mjs.serviceLauncher.tson main resolves<versionDir>/t3(changed in af6c138 / feat(server): manage runtimes as release archives only, never from npm #11510).t3package at all — on my box,0.0.41-nightly.20260915.1766/node_modules/has nine entries andt3is not one of them.runtimeExists()returns false and the launcher repliesupdate-rejected(serviceLauncher.ts:509).0.0.41-nightly.20260914.1722shipped both layouts, which is why it was the last version to install successfully.Impact
Major degradation or frequent failure
Silent and self-perpetuating: the update looks like it is working (the version installs fully), the error suggests a corrupt download, and nothing points at the real fix. It should hit any background-service host whose launcher predates #11510, which is effectively all of them, since in-app self-update never replaces that file.
Version or commit
Server stuck on
0.0.41-nightly.20260914.1722; failing to reach0.0.41-nightly.20260915.1735and.1766. Analysis againstmain@ 7235701.Environment
Ubuntu 24.04 arm64, systemd user service, nightly channel, headless (no desktop app).
Logs or stack traces
Note:
~/.t3/userdata/logs/boot-service.loghad been deleted on disk while systemd still held the fd, so the launcher-side log was only reachable via/proc/<launcher-pid>/fd/1. Possibly worth a separate look — nothing in the repo appears to unlink it.Workaround
Run
t3 updateon the server machine from a current on-disk binary. This rewrites the unit toExecStart=<versionDir>/t3 __service-launcher, which hosts the launcher inside the versioned executable and fixes it permanently:Confirmed working — the service came back on
.1766, with drop-ins and Tailscale Serve intact.Suggested fix
Bump
SERVICE_LAUNCHER_PROTOCOLto3. The preflight then blocks before anything downloads and emits the correct message. Worth considering alongside it: any layout change toruntimePathsis a launcher compatibility break by definition, since that file outlives every self-update, so a comment on the constant tying the two together would help the next person.