# PR #22996 quit-lock runtime evidence

- Before = `8d3b061121fe65669d5d43d011b48cd3027a4108` (main merge-base), worktree /tmp/ev-22996/before
- After = `efd97c35c6b27eb65ea06ca1814c60ee1d0ed01e` (PR head, main merged in), ~/wt/pr-22996
- Driver: `apps/desktop/scripts/quit-lock-evidence.sh`, committed afterwards as `424fe021a8` (script only; app code identical to efd97c35, so captures stay valid). Runs used the pre-commit copy; the only later edit was the screenshot DISPLAY lookup, used just for the `*-F1-shot` runs.
- Quit trigger: SIGTERM to the Electron main PID (quitFromSignal → stopAllPoolInstances). Time = SIGTERM → Electron main gone.
- "Lock" = BEGIN EXCLUSIVE probe on `<home>/userdata/server-owner.sqlite` right after Electron exits. "Relaunch" = same home, new Electron; OK means the new backend PID is the `lsof` holder of the lock.

## Environment
fs-29, Linux 6.1.102 x86_64, 4 CPUs, node v24.22.1 (probe), Electron 44.9.3, Xvfb 1920x1080x24 via headless-desktop (xvfb-run + dbus-run-session + gnome-keyring), `--no-sandbox --disable-gpu`. **Host load average ~27 (1-min) on 4 CPUs at 12:05 UTC, 15-min avg 16.6 at 12:15, from other agents on the box. Timings are noisy.**

## Results

| Row | Before (8d3b0611) | After (efd97c35) |
|---|---|---|
| F1 idle, SIGTERM | 0.559s; lock FREE; relaunch OK (shot run: 1.713s) | 0.353s; lock FREE; relaunch OK (shot run: 0.831s); no `exceeded timeout` warning |
| F3 backend SIGSTOPped, run 1 | 5.210s; backend `T` at t+4.74s, gone by exit (spawner SIGKILL won the race); lock FREE; relaunch OK | — |
| F3 run 2 | **5.158s; backend 658073 survived as orphan, stat `Tsl`, ppid 1458 (subreaper); lock HELD; relaunch backend 658856 never acquired the lock in 60s (holder: 658073)**. Relaunched Electron then wedged in bootstrap and ignored SIGTERM for 2m20s (no `before-quit received`); orphan and relaunch main were SIGKILLed by hand, see `before-F3-run2.manual-notes.txt` | — |
| F3 run 3 | 6.408s; backend gone by exit (SIGKILL won); lock FREE; relaunch OK | — |
| F3 (After, runs 1-3) | — | run 1 5.672s, run 2 5.179s, run 3 5.232s (runs 2-3 at load avg ~3-6). Every run: backend group gone by t+5.2s sample (before Electron exit); lock FREE; relaunch OK; no orphan; no `exceeded timeout` warning |
| F4 SIGSTOP, SIGCONT at t+3s | 4.708s; lock FREE; relaunch OK | 4.378s; lock FREE; relaunch OK |

Before F3 reproduced the orphan + held lock in **1 of 3** runs. The other two lost the race that the matrix describes (outer 5s timeout vs. the spawner's 5s SIGKILL). After F3: 3 of 3 runs released the lock before Electron exited, and the relaunch acquired it every time.

## Unverified
- F2 (UI quit via CDP), F5 (quit during a long request), F12 (backend already dead): time budget and host load (~27 on 4 CPUs) cut them. The driver supports F2 and F12 but they were not run.
- F6: needs a backend in uninterruptible sleep (D-state); can't trigger here without root FUSE/NFS. Unit-covered only.
- F7: needs packaged AppImage builds of two versions plus the mock update server; out of budget.
- F8: app-internal relaunch isn't scriptable from outside. P2 (manual relaunch on the same home) covers the property.
- F9: Windows + WSL only. F10: Windows only. F11: macOS only.

## What else changed between sides
- `git diff --stat 8d3b0611 efd97c35`: only the PR's 6 desktop files (DesktopApp.ts, DesktopBackendManager.ts + test, DesktopBackendPool.test.ts, testUtils/FakeBackendProcess.ts, DesktopUpdates.ts). Both sides share main at 8d3b0611; the 112 main commits merged into After (vs bd63fde) are all in Before too.
- Build mode is identical on both sides: `vp run build:desktop` built the server (`apps/server/dist`). The desktop step failed on both sides at `scripts/build-browser-secret.mjs` (libsecret-1-dev not installed), so `build-preview-annotation-css.mjs && vp pack` were run by hand. The Linux browser-cookie import helper is absent on both sides; it's unrelated to quit.
- Both sides use the same Electron 44.9.3 binary version.
- Screenshots show the real T3 Code Electron window (After shows the T3 splash; Before is still blank while loading under load), not a fully loaded UI.
