Repository navigation
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
Description
Activity
Closing as a duplicate of #11934 — same root cause: a pre-#11510 / pre-SEA
service-launcher.mjsstill resolvesnode_modules/t3/dist/bin.mjs, so a complete standalone runtime (./t3) looks “missing or incomplete,” and the unit can hitstart-limit-hit.That was fixed on
mainby #11940 (bumpSERVICE_LAUNCHER_PROTOCOLto 3 so staged preflight blocks activation with the actionable “update the launcher on the server machine” message). Tagv0.0.42points at719a76ca(2026-09-15 ~21:29Z), which is before #11940 merged (~22:21Z), so stable0.0.42does not include that gate yet.Workaround (same as #11934 / your rollback): stay on / roll back to a bundle-layout runtime the on-disk launcher can start, or rewrite the launcher/unit with a current local
t3 updatefrom a new enough binary, thensystemctl --user reset-failed+ restart.- addedduplicateThis issue or pull request already existsThis issue or pull request already exists
on Sep 16, 2026 Retracting this — the fault is local to my host, not t3code. Sorry for the noise.
I filed this against the wrong component. The 0.0.42 update behaved
correctly. On closer inspection of the systemd unit:0.0.42 installed 2026-09-15 22:09:23 base unit rewritten 2026-09-15 22:09:25 <- two seconds later, by the updateThe updater did rewrite the service unit for the new layout:
ExecStart=/home/jwest/.t3/runtime/versions/0.0.42/t3 __service-launcherand the shipped binary does accept
__service-launcher. Nothing was stranded.What actually broke was a local systemd drop-in on my host, written on
12 September to bind the server to the LAN, which also overrode the launch
command:# ~/.config/systemd/user/t3code.service.d/lan-server.conf ExecStart= ExecStart=/usr/.../node /home/jwest/.t3/runtime/service-launcher.mjs
ExecStart=clears the base unit's value, so my three-day-old override pinned
the oldnode + service-launcher.mjsinvocation and defeated the update. The
failing process in the journal is that override, not anything t3code installed.So the premise of this report — "the update ships no replacement launcher" — is
false. It ships a rewritten unit; my drop-in discarded it.My error, and worth naming precisely: I read the drop-in early in the
investigation, read the base unit only after the update had rewritten it, and
never re-read the effectiveExecStartthat systemd was actually running. The
evidence that would have caught it —systemctl show -p ExecStart— was one
command away throughout.The one suggestion I would still make, weakly
A local
ExecStart=override silently defeating a unit rewrite is easy to do
and gives a confusing failure ("runtime is missing or incomplete" rather than
"your unit is launching the wrong thing"). If the launcher can cheaply detect
that it was started against a runtime whose layout it cannot read, a message
naming the effective command and pointing at drop-ins would have saved me
several hours. Entirely your call whether that is worth the code.Closing. Apologies for the misdirected report.
Correcting my own retraction above — I over-corrected, and @juliusmarminge is right.
Both things are true, and they compose:
- [Bug]: background service self-update fails permanently against a pre-#11510 service-launcher.mjs #11934 is genuine: a pre-SEA
service-launcher.mjsresolves
node_modules/t3/dist/bin.mjs, so a complete./t3runtime reads as
"missing or incomplete" and the unit reachesstart-limit-hit. Exactly what
happened here. - What put my host on that path is a local drop-in with
ExecStart=+
ExecStart=node .../service-launcher.mjs, written before the SEA change. The
0.0.42 update did rewrite the base unit to
ExecStart=.../0.0.42/t3 __service-launcher; my override cleared it and kept
invoking the old launcher.
So the upstream defect is real and my drop-in is the local trigger for it. My
retraction said "not t3code", which was wrong — please disregard that line.
A host with noExecStartoverride would take the rewritten unit and never
reach the stale launcher.Nothing needed here; #11934 / #11940 cover it. Noted for anyone who lands on
this issue: if you hit this on a release predating #11940, check
systemctl --user show t3code.service -p ExecStartfor a drop-in override
before assuming the update failed.- [Bug]: background service self-update fails permanently against a pre-#11510 service-launcher.mjs #11934 is genuine: a pre-SEA
Summary
Upgrading the background-service runtime to 0.0.42 leaves the service
permanently unable to start.
0.0.42replaced the JS bundle entrypoint with acompiled Node SEA binary, but
~/.t3/runtime/service-launcher.mjsis notreplaced by the update and still resolves the old bundle path. The launcher
reports the new runtime as corrupt, systemd restarts it until the start limit is
hit, and the service ends in
failed.The runtime itself is fine. Only the launcher's path assumption is stale.
Impact
On this host the service went from healthy to permanently down with no user
action, and stayed down:
Because
Restart=alwayswithStartLimitBurst=5, the unit exhausts its budgetin ~5 minutes and then requires a manual
systemctl --user reset-failed. It doesnot recover on its own. Any deployment that takes 0.0.42 with a pre-0.0.42
launcher on disk will hit this.
Root cause
service-launcher.mjshardcodes the bundle entrypoint:runtimeExists()statsentryPath, fails, and#startChild()throwsSelected t3@${version} runtime is missing or incomplete.The two layouts differ:
node_modules/t3/dist/bin.mjs(8.8 MB JS)./t3(compiled ELF, Node SEA)node_modules,package.json,package-lock.jsont3,client,resource-monitor,node_modulesnode_modulesholdst3packageffi-rs,msgpackr-extract,@ff-labs,detect-libc)0.0.42/.install-completecorrectly contains0.0.42, and the install iscomplete — the sentinel check would pass if the entry path resolved.
The update ships no replacement launcher: nothing matching
*launcher*exists anywhere under
versions/0.0.42/.Reproduction
Suggested fix
Detect the layout rather than assume it, so one launcher starts either
generation and a rollback to a bundle release still works:
and at the spawn in
#startChild():The
"ipc"stdio channel needs no change — the 0.0.42 binary is a Node SEA(
NODE_CHANNEL_FDandnode:internal/child_processare present in the image),so it still speaks the Node IPC protocol, and
./t3 --helpshows it acceptsserve.This fix is written but not yet verified in situ on this host — applying it
requires modifying a live service file and I have not done so. The analysis
above is verified; the patch is not yet proven by a successful start.
Two adjacent notes
A user-side wrapper has the same assumption. Any script that resolves the
active runtime by reading
runtime/service-state.jsonand joiningnode_modules/t3/dist/bin.mjsbreaks identically. If that pattern is documentedanywhere, it needs the same detection.
Consider making the launcher self-updating or version-gated. The underlying
hazard is that the launcher and the runtime can drift, and the launcher is the
component that cannot be fixed by the update mechanism it supervises. A
minLauncherProtocolfield in the runtime manifest — refusing to select aruntime the on-disk launcher is too old to start, rather than selecting it and
failing — would turn this class of failure into a clean refusal plus a stay on
the previous version.
Environment
Workaround
Roll the active runtime back and reset the failed unit: