t3 service install silently rebinds an existing service to a different base dir, orphaning its data#23858

opened1d agobyFfinchandfriends

What happened

I was working with t3 in headless mode for weeks, connecting to this machine from my other laptop, tablet, phone. Today I decided to install desktop app on this machine. Once I did connect to the service I noticed the pop up saying the t3 server can be updated so I clicked update. All was fine and dandy but I noticed I can't open my files anymore. So I went to terminal to check the service status which said service is corrupted and can be fixed with t3 service install. Once I installed the service all my sessions and projects disappeared.

Diagnosis

The user's long-lived service was installed with --base-dir /mnt/workspace/Projects, so the unit's T3CODE_HOME pointed there and all state (685 threads, 6 projects, 704MB statev2.sqlite) lived under that base dir. The default ~/.t3 held only leftovers from first-day experiments. The server's self-update (0.0.46-nightly.20261005.2689 → 0.1.2-nightly.20261022.3526) completed fine.

The data "disappeared" because of two compounding behaviors when a t3 CLI is run without --base-dir against a unit installed with one:

  1. t3 service status reports a healthy foreign-base-dir unit as needing repair. In apps/server/src/cloud/bootService.ts, status compares the on-disk unit against detectedManager.render(plan) where plan is rendered for the CLI's own input.baseDir (default ~/.t3). A unit installed for a different base dir can never match, so current is false and the CLI prints "needs an update or repair / Next: Run t3 service install to repair it." status even computes installedBaseDir from the unit, but neither uses it in the current comparison nor surfaces it in formatServiceStatus.

  2. t3 service install overwrites the unit without comparing base dirs, and its downgrade guard reads the wrong state file. install reads previousStateText from statePath under the CLI's input.baseDir (~/.t3/runtime/service-state.json, which said 0.0.46-nightly.20261005.2702 — equal to the CLI, so no refusal) instead of the unit's actual base dir (/mnt/workspace/Projects/runtime/service-state.json, which said 0.1.2-nightly.20261022.3526). It then stops the running service and rewrites the unit for ~/.t3. The real instance is silently replaced by a server bound to an empty state dir — to the user, total data loss.

Notably, restart and uninstall both already guard on this exact condition (bootServiceBaseDirOf(unit) vs path.resolve(input.baseDir)) and refuse to touch a foreign unit. install — the only destructive one of the three — has no such guard. Verified at tag v0.0.46-nightly.20261005.2702 (cfa4f765) and still present on main as of 2026-10-22.

Suggested direction: give install the same installedBaseDir comparison and refuse (or require an explicit flag) when it differs; read the downgrade-guard state from the unit's base dir; and print installedBaseDir in t3 service status so the mismatch is visible instead of being reported as corruption.

Steps to reproduce

  1. t3 service install --base-dir=/some/other/dir and use the server (create a thread).
  2. Install a second t3 CLI (e.g. a distro package) and run t3 service status with no flags → reports "needs an update or repair" although the service is healthy.
  3. Run t3 service install with no flags, as the status output suggests.
  4. The unit is rewritten for the default ~/.t3; the original instance stops and the service now serves an empty state dir. All sessions/projects appear gone. No downgrade or mismatch warning is shown even when the replaced service was newer than the CLI.

Version

Service that was clobbered: 0.1.2-nightly.20261022.3526 (self-updated from 0.0.46-nightly.20261005.2689). CLI that clobbered it: 0.0.46-nightly.20261005.2702 (AUR t3code-nightly-bin). Missing guard confirmed on main.

Environment

Linux x64 (CachyOS, kernel 7.2.9-2), Node v26.8.2, systemd user service.

Evidence

1# Unit after `t3 service install` (no --base-dir) — rebound to default home:
2Environment=T3CODE_HOME=~/.t3
3ExecStart=~/.t3/runtime/versions/0.0.46-nightly.20261005.2702/t3 __service-launcher
4
5# Real instance, cleanly shut down at the moment of the reinstall:
6# /mnt/workspace/Projects/userdata/logs/boot-service.log
7[21:22:41.924] INFO: Server update prepared; handing off to the service launcher.
8[21:22:47.756] INFO: Listening on http://127.0.0.1:3773
9[22:14:57.696] INFO: V2 orchestration shutdown reconciliation completed
10
11# /mnt/workspace/Projects/runtime/service-state.json (ignored by the downgrade guard):
12{ "protocol": 3, "activeVersion": "0.1.2-nightly.20261022.3526",
13 "update": { "fromVersion": "0.0.46-nightly.20261005.2689",
14 "targetVersion": "0.1.2-nightly.20261022.3526", "status": "committed" } }
15
16# Replacement server booting on the stale default-home state:
17# ~/.t3/userdata/logs/boot-service.log
18[22:14:59.774] WARN: Database migration history diverges from this build; recorded
19 migration ids are skipped, not reconciled by name.

Related issues

#5749 (SSH reconnect can replace an external T3 service with a competing managed runtime) is the same family — one T3 install clobbering another's service ownership — but a different entry point and mechanism; this one is driven by the service status → service install repair suggestion ignoring installedBaseDir.

Fix applied or workaround

Recovered on the user's machine with no data loss: removed the manual systemd override, then ran the pinned runtime's own CLI — /mnt/workspace/Projects/runtime/versions/0.1.2-nightly.20261022.3526/t3 service install --base-dir=/mnt/workspace/Projects. Workaround until fixed: always pass --base-dir to t3 service ... commands when the service was installed with one.

Filed by

Claude Fable 5 (claude-fable-5) via t3 triage (Claude Code)