Summary
On macOS 15, launchctl bootout --wait <service-target> is a usage error, so the boot service's stop step never runs. The old job keeps the label, and the strict bootstrap that follows fails with EIO. The result is that t3 service install, t3 update, and the post-update restart all fail on this OS, leaving the previous server running.
Environment
- macOS 15.7.9 (build 24G830), Darwin 24.6.0
Darwin Bootstrapper Version 7.0.0
- t3
0.0.41-nightly.20260915.1752
Reproduction
$ launchctl bootout --wait gui/501/com.t3tools.t3code.service
Unrecognized target specifier.
Usage: launchctl bootout <domain-target> [service-path1, service-path2, ...] | <service-target>
<service-target> takes a form of <domain-target>/<service-id>.
Please refer to `man launchctl` for explanation of the <domain-target> specifiers.
$ echo $?
64
$ launchctl bootout gui/501/com.t3tools.t3code.service
$ echo $?
0
With a service already installed, t3 update <version> --yes then reports:
CliUpdateError: t3@<version> is installed but the background service could not be
updated: Background setup failed while starting the service (exit code 5).
and ~/.t3/userdata/logs/boot-service.log shows the two steps in order:
Background setup failed while stopping the installed launch agent (exit code 64).
Background setup failed while starting the service (exit code 5).
Background setup failed while restarting the service after a failed update (exit code 5).
Cause
launchdManager in apps/server/src/cloud/bootService.ts passes --wait to bootout in both its stop and deactivate steps:
args: ["bootout", "--wait", serviceTarget],
The accompanying comment reads "--wait (present on modern macOS, absent from the man page)". It is not present on macOS 15: launchctl rejects the whole invocation as a usage error rather than ignoring the flag. Because the step is optional: true, the failure is logged and the flow continues to bootstrap, which is exactly the EIO case the flag was added to avoid.
Suggested fix
Try the flag, fall back to a plain bootout, and wait for the label to leave the domain either way, so both old and new launchctl behave:
launchctl bootout --wait "$target" 2>/dev/null || launchctl bootout "$target" 2>/dev/null
i=0
while launchctl print "$target" >/dev/null 2>&1 && [ "$i" -lt 90 ]; do sleep 1; i=$((i + 1)); done
I am running that locally as a patch and t3 update now installs and restarts the service cleanly, including a downgrade back to the previous version. Happy to send it as a PR if you would like it in this shape.
Summary
On macOS 15,
launchctl bootout --wait <service-target>is a usage error, so the boot service's stop step never runs. The old job keeps the label, and the strictbootstrapthat follows fails withEIO. The result is thatt3 service install,t3 update, and the post-update restart all fail on this OS, leaving the previous server running.Environment
Darwin Bootstrapper Version 7.0.00.0.41-nightly.20260915.1752Reproduction
With a service already installed,
t3 update <version> --yesthen reports:and
~/.t3/userdata/logs/boot-service.logshows the two steps in order:Cause
launchdManagerinapps/server/src/cloud/bootService.tspasses--waittobootoutin both itsstopanddeactivatesteps:The accompanying comment reads "
--wait(present on modern macOS, absent from the man page)". It is not present on macOS 15:launchctlrejects the whole invocation as a usage error rather than ignoring the flag. Because the step isoptional: true, the failure is logged and the flow continues tobootstrap, which is exactly the EIO case the flag was added to avoid.Suggested fix
Try the flag, fall back to a plain
bootout, and wait for the label to leave the domain either way, so both old and newlaunchctlbehave:I am running that locally as a patch and
t3 updatenow installs and restarts the service cleanly, including a downgrade back to the previous version. Happy to send it as a PR if you would like it in this shape.