# fix\(desktop\): wait for the local server to exit when quitting · PR \#22999 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22999)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: fix/issue-22983

Target branch: main

Head commit: 424fe021a8815e3b8cd80d365c8477c77d1ebfbf

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/22999)

[Commits](https://git.cafe/pingdotgg/t3code/pull/22999/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/22999/diffs?format=markdown)

## Description

---

## Problem

Quitting desktop could finish while its local backend still held the T3 home's single-writer lock. The five-second outer wait expired at the same deadline as SIGKILL escalation, leaving relaunch unable to start a server.

## Change

Wait for the existing scoped process teardown and exit acknowledgement even after the warning threshold. This preserves the spawner's SIGTERM → five-second grace → SIGKILL → final one-second wait, without adding another signal path. Normal quit and updater quit share a seven-second threshold: 7s > 5s grace + 1s kill wait. A concurrent unregister cannot propagate its cleanup interruption into sibling backend stops. Cooperative shutdown returns immediately. An OS-uninterruptible backend can keep quit waiting rather than leaving it alive and claiming shutdown completed.

## Scope and approval

Fixes #22983
GitCafe: https://git.cafe/pingdotgg/t3code/issues/22983
Triaged bug via the automated issue flow.

Existing process coverage is unchanged: POSIX teardown signals the backend's process group; Windows uses taskkill /T /F while the leader is alive, not job objects. Descendants in separate groups/scopes depend on their server-side teardown; this does not claim to kill detached provider trees or the Linux side of WSL through a Windows process-group signal.

Validation:
- Fake-process baseline: 3 failed, 11 passed; stop and pool quit resolved with the backend still alive.
- Targeted manager/pool suites after the fix: 15 passed. Coverage includes ignored SIGTERM, exit acknowledgement, cooperative shutdown without SIGKILL, repeat stop, budget relationship, pending output, pool scope cleanup, and concurrent unregister preserving sibling stops.
- Related manager/pool/lifecycle/updates suites: 47 passed. Both code reviewers: MERGE. Both evidence auditors: SUFFICIENT for the authorized shutdown scope.
- Targeted desktop lint and desktop typecheck passed (existing advisory suggestions remain).
- No wall-clock sleeps or real user state used. No headless real-server integration harness exists in desktop; its smoke harness launches Electron. cannot-verify macOS live.

## Verification

Only the shutdown half of [#22983](https://github.com/pingdotgg/t3code/issues/22983) is addressed. Startup reuse/recovery is deferred: server-owner.sqlite stores no data, and server-runtime.json carries pid, a server start timestamp, and a server-generated ownerId but no persisted desktop provenance. A follow-up needs desktop-owned identity tied to OS process birth plus authenticated same-home health checking before safely reusing/recovering an owner. The lock and ownership protocol are unchanged; no data or settings are reset.

<!-- gc2-pull-create:e2a9deb2a4dd2d1ef0ffe85444b2137b8cfb0dbf727263a4b14a9d71af1211cf -->

Model/harness: GPT-6.1-Sol through the Codex harness in T3 Code; independent reviews and proof audits by GPT-6.1-Sol/Codex and Claude Opus 5.5/Claude Agent SDK.


## Evidence

Real Electron app on Linux, isolated `T3CODE_HOME`/XDG per run. Quit = SIGTERM to the Electron main process. "Lock" is a `BEGIN EXCLUSIVE` probe on `server-owner.sqlite` right after Electron exits; "relaunch OK" means a new app on the same home gets the lock. Before = main [8d3b061](https://github.com/pingdotgg/t3code/commit/8d3b061121fe65669d5d43d011b48cd3027a4108), After = [efd97c3](https://github.com/pingdotgg/t3code/commit/efd97c35c6b27eb65ea06ca1814c60ee1d0ed01e) (app code; [424fe02](https://github.com/pingdotgg/t3code/commit/424fe021a8815e3b8cd80d365c8477c77d1ebfbf) only adds the driver). Driver: `apps/desktop/scripts/quit-lock-evidence.sh`. Fixture matrix: [matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0f2940d496608777/matrix.md).

| Row | Before 8d3b061 | After efd97c3 |
|---|---|---|
| F1 idle quit | 0.56s, lock free, relaunch OK ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2420bd99a7dc57da/before-F1.txt)) | 0.35s, lock free, relaunch OK, no timeout warning ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9146f5502dbe34fc/after-F1.txt)) |
| F3 backend ignores SIGTERM (SIGSTOP), run 1 | 5.21s, SIGKILL won the race, lock free ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/88cc36a0985a5144/before-F3-run1.txt)) | 5.67s, backend group gone before Electron exit, lock free, relaunch OK ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5dba3cc28ca9a33d/after-F3.txt)) |
| F3 run 2 | **5.16s, backend 658073 orphaned (stopped, reparented) and still holding the lock; relaunched backend never got it in 60s** ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b010a0867a41ac63/before-F3-run2.txt), [notes](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/37ecfb0d9df7cd32/before-F3-run2.manual-notes.txt)) | 5.18s, lock free, relaunch OK ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/43098494f9785984/after-F3-run2.txt)) |
| F3 run 3 | 6.41s, SIGKILL won the race, lock free ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/efb0d7e794530f38/before-F3-run3.txt)) | 5.23s, lock free, relaunch OK ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6ea0d582d31a342c/after-F3-run3.txt)) |
| F4 backend slow, resumes at 3s | 4.71s, lock free ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9216877a4d1d4672/before-F4.txt)) | 4.38s, lock free ([log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/fb64379562de60c9/after-F4.txt)) |

App running under Xvfb (still loading under host load): [before](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/eb6007ac0712b7ff/before-app-running.png), [after](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b2d79cbcd6980484/after-app-running.png). Summary: [summary.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f0f26aee134001e4/summary.md).

**Environment:** fs-29, Linux 6.1.102 x86_64, 4 CPUs, Node v24.22.1, Electron 44.9.3, Xvfb 1920x1080x24 via headless-desktop, `--no-sandbox --disable-gpu`. Host load average was ~27 during the main runs (shared box), so timings are noisy; the extra After F3 runs and the auditor's rebuild ran at load ~3–6.

**What else changed:** `git diff --stat 8d3b061 efd97c3` is only this PR's 6 desktop files; main was merged into the branch, so both sides share the same main base. Both sides were built the same way: `build-browser-secret.mjs` fails without `libsecret-1-dev`, so the desktop bundle was packed by hand (`build-preview-annotation-css.mjs && vp pack`) on both; that helper only affects browser cookie import.

<details><summary>Verification log</summary>

- Proof audit pass 1 (Claude Opus 5.5): 12-fixture matrix; F1/F3/F4 required both sides.
- Proof audit pass 2 (Claude Opus 5.5): **SUFFICIENT** for the shutdown half on Linux. Its own rebuild of After F3 from the committed driver took 5.20s, lock free, relaunch OK. Gaps it noted: Before fails 1 in 3 because its 5s give-up races the spawner's 5s SIGKILL (After is clean 4 of 4); SIGKILL in After is inferred from timing; a backend stuck in uninterruptible sleep now keeps quit waiting indefinitely, by design.
- Tests after merging main: `DesktopBackendManager`, `DesktopBackendPool` and `src/updates` suites passed, 79/79 (the first cold run failed to import one file and passed on rerun). Desktop typecheck passed.
- CI on 424fe02 at report time: 26 passed, 0 failing, 3 pending (Test, CodeRabbit, Macroscope approvability).
- Unverified: F2 (quit from the UI), F5 (quit during a running turn or long request), F12 (backend already dead) were not run, because of the time budget and host load. F6 (backend in uninterruptible sleep past 7s) can't be triggered without root FUSE/NFS; unit tests cover it. F7 (updater quit-and-install) needs two packaged builds and a mock update server. F8 (in-app relaunch) is covered by the manual relaunch. F9 (concurrent unregister) is WSL-only, F10 Windows `taskkill`, F11 macOS Cmd+Q / Squirrel.
- Model/harness: Claude Opus 5.5 via the Claude Agent SDK in T3 Code (evidence lead, capturer, auditors).

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

