Skip to content

Desktop adopts a long-running local T3 service as its primary environment #38

Closed as not planned

Description

@tannerpolley

Goal

Let agents and remote clients keep running while the desktop app's window or process restarts, by running the T3 server as a long-lived local service that the desktop app adopts as its primary environment. This replaces the 2026-10-01 → 10-06 attempt (t3code-fork.service), which the owner dropped as "causing more issues than it helps".

Owner request (2026-10-14). The smaller step, keeping the app running when its window closes, is being built separately (feat/desktop-keep-running).

Why the first attempt failed (evidence)

Sources:

  • the service journal, Oct 1–6;
  • the transcripts from those days;
  • ~/.local/opt/t3-code/server/*.
  1. No primary environment. The desktop's built-in server was turned off (localEnvironmentEnabled: false), and the service was saved as a remote bearer connection to 127.0.0.1:3790.
    • The web client only treats a PrimaryConnectionTarget as primary (apps/web/src/state/primaryEnvironment.ts). So every feature gated on the primary environment ran in remote mode. About 40 web files reference it, including the browser host, previews, open-in, folder drop, settings scope and sidebar layout.
    • Bearer tokens also had to be minted and stored. That later produced 401s on the MCP tools bridge and left stale authorized clients behind.
  2. Two servers on one database (2026-10-01). After a reboot, both the service and the app's built-in server opened statev2.sqlite and resumed the same interrupted work. The result was duplicate runs, failed threads and a lost Claude session.
  3. Version skew. Every build had to be unpacked into server/vX and the current symlink repointed; otherwise the service ran older code than the window. Interface and server compatibility had to be checked by hand. The idle-gated installer gave up ("agents never all idle within 7200s") and didn't survive reboots.
  4. No actual benefit for upgrades. Every server upgrade still killed every agent: 14 service starts in 5.5 days.
  5. Not caused by the split. The "reconnecting" and lag reports that week came from server overload: a 9 GB database, 100–160% CPU on the main thread, and a Codex connection without a timeout. Those were fixed in v2.8.34.16 and v2.9.x.

Proposed design

  • The desktop adopts, not connects. On launch, the desktop's backend manager (apps/desktop/src/backend/DesktopBackendManager.ts, DesktopBackendConfiguration.ts) first looks for a healthy T3 service for the same T3 home. It uses server-runtime.json plus a health and identity check.
    • If one exists, the desktop uses it as its primary backend instead of spawning a child, and the window loads the UI from it, same-origin as today.
    • If none exists, the desktop starts the service, for example a systemd user unit on Linux, rather than a child process.
    • Either way the renderer sees a PrimaryConnectionTarget, so every primary-only feature keeps working.
  • One owner per database. The server takes an exclusive lifetime lock (an OS file lock next to statev2.sqlite) before migrating or opening the database, and refuses to start if another live server holds it. This closes the 2026-10-01 failure for every launcher, not just the CLI.
  • Local authentication without pairing. The desktop's bootstrap secret (getOrCreateBootstrapSecret, in memory today) is persisted in the T3 home secrets, with 0600 permissions. The desktop can then authenticate to a service it didn't spawn. No bearer tokens are stored in the connection catalog.
  • The UI always matches the server. The window loads the UI the service serves, so the interface and server versions can't drift.
  • Updates.
    • Window crashes, reloads and app restarts no longer touch agents.
    • Installing a new server version still restarts the service and interrupts agent turns. Reuse upstream's service launcher (apps/server/src/serviceLauncher.ts: trial child, commit or rollback, restart-pending). Install when the owner chooses, from a visible "restart to update" prompt rather than an idle timer.
  • Quitting. Quitting the desktop leaves the service running. A separate "Stop T3 server" action stops it, with a warning that running agents are interrupted.

Open questions

  • Whether upstream would take this (t3 service exists for headless hosts, but the desktop can't attach to it). Worth an upstream Ideas discussion before a large fork-only change.
  • Windows has no service support upstream. This would be Linux and macOS only.
  • The interaction with feat/desktop-keep-running: once the service outlives the app, the tray only needs to manage the window.

Done when

  • The desktop app attaches to an already-running local service as its primary environment, with every primary-only feature working.
  • A second server can't open the same database.
  • Closing or restarting the desktop app never interrupts agents. Server updates happen only on the owner's explicit restart.
  • A real-machine check covers a reboot, a desktop restart while agents run, and an update.

Activity

  1. tannerpolley commented on Oct 14, 2026

    @tannerpolley
    OwnerAuthor

    Upstream now provides the database lock this plan needs (2026-10-14 sync, pending merge): pingdotgg#16102, 570542662a "enforce one database owner and guard update rollback", plus 94ca023bf4 (ownership UUID fix).

    The One owner per database item in this plan should reuse that ownership mechanism rather than add a fork lock. Read it before designing the adopt-a-running-service path.

    Option A, keep running in the tray on window close, is on feat/desktop-keep-running and in its second review.

  2. tannerpolley commented on Oct 15, 2026

    @tannerpolley
    OwnerAuthor

    Closed as not planned (owner decision, 2026-10-14). Closing the window already keeps T3 running in the tray (caa0f14). If this comes back, start from upstream's one-owner-per-database lock (pingdotgg#16102), as noted above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions