Skip to content

Failed service install leaves a dead unit that status reports as healthy #8476

Description

@ErickCReis

What happened

t3 service install failed at enable-linger on a host with no polkit authority. It left a unit that was enabled but never started, and nothing cleaned up.

t3 service status then reported the service as installed and current. Re-running t3 service install printed "already installed" and did nothing. The only way out was systemctl --user restart t3code.service, which is not mentioned anywhere in the error.

Until then the server ran unmanaged, so it advertised no serverSelfUpdate capability and clients offered "Copy update command" instead of a one-click update.

Diagnosis

Two changes in d3037064e (#5181) combine to produce this.

The install rollback was removed. v0.0.31 wrapped activation in Effect.tapError(() => rollbackFailedInstall(previousUnit)), which on a fresh install ran systemctl --user disable --now, removed the unit file and reloaded systemd. Its comment stated the reason:

If any activation step fails, remove the unit again: a leftover file would make service status report it as installed even though it was never enabled or lingered.

Current code has no equivalent. The only handler is at bootService.ts:668-670:

Effect.tapError(() =>
  installed ? runSteps(manager.restart).pipe(Effect.ignore) : Effect.void,
)

installed means a unit file existed before the launcher, state and unit writes. Repairing an existing install gets a best-effort restart. A first install gets Effect.void, so nothing happens.

enable-linger moved before the start. v0.0.31 ordered activation daemon-reload, enable, restart, enable-linger, so a linger failure came after a successful start. Current order is daemon-reload, enable, enable-linger, restart (bootService.ts:225-243), which puts a step that can fail on an unrelated precondition directly between "enabled" and "started".

status cannot see the problem. It reports installed from the unit file existing, and computes current from file contents, the launcher, the pinned runtime entry, the sentinel and the state file (bootService.ts:698-723). It never runs is-enabled or is-active. Every one of those files was written correctly before activation failed, so status reports installed and current.

reconcileService then short-circuits on exactly that (cli/service.ts:35-38), so t3 service install and t3 service update both refuse to do anything. The CLI insists the service is fine while it is not running.

This is the failure the removed rollback comment predicted, word for word.

Steps to reproduce

  1. On a Linux host, make enable-linger fail. Running with no polkit authority does it, and so does removing loginctl from PATH.
  2. Run t3 service install with no service previously installed.
  3. Run t3 service status, and observe it reports the service as installed and current.
  4. Run t3 service install again, and observe it reports the service as already installed.
  5. Check systemctl --user is-active t3code.service.

Version

0.0.36-nightly.20260827.1207

Environment

Arch Linux x86_64, systemd 260, Node 26.7.0 via mise. No polkit authority running.

Evidence

$ t3 service install
ERROR: BootServiceCommandError: Background setup failed while enabling lingering for this user (exit code 1).

$ systemctl --user is-enabled t3code.service
enabled
$ systemctl --user is-active t3code.service
inactive

$ ls ~/.t3/runtime
service-launcher.mjs  service-state.json  versions

$ cat ~/.t3/runtime/service-state.json
{ "protocol": 2, "activeVersion": "0.0.36-nightly.20260827.1207" }

The unit file, launcher, pinned version and state file were all present. Only the start had not run.

Related issues

#4894 is the failure that exposed this. Its report of a clean rollback was accurate for t3@0.0.31, before #5181.

Fix applied or workaround

systemctl --user restart t3code.service

The service started at once and the server came up launcher-managed, advertising serverSelfUpdate: "boot-service". Re-running the installer does not work, because status already considers the install complete.

Filed by

Claude Opus 5 in Claude Code, from a t3 triage run. Codex (gpt-5.6-sol) analysed the source and fact-checked the write-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions