# Proof audit, pass 1 (round 2): [#23005](https://github.com/pingdotgg/t3code/pull/23005) / [#23007](https://git.cafe/pingdotgg/t3code/pull/23007)

Reviewed [87ac5b3a34](https://git.cafe/pingdotgg/t3code/commit/87ac5b3a34fe2c12561bf16f6f823e0376fda004) against [8d3b061121](https://git.cafe/pingdotgg/t3code/commit/8d3b061121fe65669d5d43d011b48cd3027a4108), in `/home/ubuntu/wt/pr-23005`. Read-only audit: no source edits, builds, desktop/browser launches, test execution, pushes, or comments. This document is a recipe, not new runtime proof.

## Findings

1. **The requested proof gate remains incomplete:** F10, reachable F11 error-event recovery, real AppImage replacement/relaunch, and F14 packaged adoption have no existing capture. An AppImage can exercise them on this VM; development-mode disabling is not a platform impossibility. Do not promote compositional F2+F13 proof into a real installation result.
2. **A checksum mismatch cannot prove F11.** It fails while downloading, before `installDownloadedUpdate` publishes the marker. A corrupt executable with a matching checksum is also a poor cleanup fixture: replacement can succeed and the detached relaunch can fail after the old app quits. Use a valid download followed by an unwritable current-image parent directory to reach the native install `error` event, or explicitly injected synchronous throw.
3. **Existing F8b graceful fallback is contradicted:** the retained capture shows 677537 ms, not immediate fallback. It is an artificial version-skew fixture; production archive selection pins its version to the client. Keep the evidence and the limitation visible.
4. **F14 must not reuse the old backend.** Start the launcher only after that backend exits and before updated readiness. Use `HOME=<run>/home` and `T3CODE_HOME=<run>/home/.t3` for both processes; the launcher's default-home lookup ignores a desktop-only custom home. Require packaged GET `/` to return 2xx with redirects disabled.
5. **The revision carryover is valid.** `git diff 9f5ac28ef0 87ac5b3a34 --name-only` contains only the committed evidence driver. All twelve earlier runtime/test/hygiene files match the earlier audited runtime. Build and mock-feed scripts are byte-identical between base and head. I downloaded and read all 26 linked text transcripts, checked their recorded SHAs and the driver, and inspected the full diff and update/recovery call paths. I did not replay the videos; their retained links accompany the text proof.

Sources for installer-specific conclusions: the locally installed `electron-updater` 6.8.9 `out/AppImageUpdater.js` (`doInstall`: unlink old image, move downloaded image, spawn replacement) and `out/BaseUpdater.js` (`install`: catch exception, dispatch `error`, return false). The app adapter catches only exceptions escaping `autoUpdater.quitAndInstall`. They are dependencies of this exact checkout, not assumptions about a newer updater.

## Fixture matrix

“SUFFICIENT (unit only)” preserves a valid branch proof, and does not claim packaged evidence. “INSUFFICIENT” means missing evidence under this round's stricter gate, not a newly proven runtime bug.

| Row | Fixture | Status at reviewed head | Existing capture/source | Assessment / remaining proof |
|---|---|---|---|---|
| F1 | No marker; node and archive modes launch without update delay | SUFFICIENT | [F1-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7114f4bbf6c9ec6f/F1-before.txt), [F1-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6e66a5e363a8b3c0/F1-after.txt) | Same behavior; real node 7.1→6.5 s and seeded archive 25.8→22.6 s. Archive runner uses real source CLI, not a downloaded release binary. |
| F2 | Fresh marker; replacement arrives across an eight-second gap | SUFFICIENT | [F2-before-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/be1d9502414835d2/F2-before-after.txt), [F2-before-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/29064f88c0ce8c4e/F2-before-after.mp4) | Before managed server wins ownership; after adopts replacement as external at 14.6 s. One surviving server. |
| F2b | Readiness removes marker between launcher polls | SUFFICIENT | [F2b-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/50a2f0dab74e7aaa/F2b-before.txt), [F2b-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e53e39a1012cb4b4/F2b-after.txt) | After final re-adopt succeeds, external at 14.7 s, marker absent. |
| F3 | Fresh marker expires without replacement | SUFFICIENT | [F3-before-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/51f3f1386e6c8de4/F3-before-after.txt), [F3-before-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f2527511db0f3ada/F3-before-after.mp4) | Before 6.6 s with marker retained; after 65.9 s, removed, exactly one server. |
| F3b | Explicit short expiresAt | SUFFICIENT | [F3b-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/abef8bddb0be9dd7/F3b-before.txt), [F3b-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/15f396eaa572f71b/F3b-after.txt) | One-second marker adds about one second; no new sixty-second window. |
| F3c | Long host TTL; bounded client wait; preserve marker | SUFFICIENT | [F3c-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/b294d4ba07eb4481/F3c-before.txt), [F3c-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7464a7c616566860/F3c-after.txt) | After 73.1 s including helper/probe/start overhead. Bound is sixty sleep iterations, not a strict sixty-second wall-clock guarantee. |
| F3d | Forward/backward host-clock jump | SUFFICIENT (unit only) | tunnel.test.ts: bounds a host clock jump to -30000 / 120000 | No physical clock-change capture. Keep deterministic fake-clock proof; do not change this shared VM clock. |
| F4 | Expired/stale marker is removed without waiting | SUFFICIENT | [F4-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/04352100286f9204/F4-before.txt), [F4-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/374c0d13a4409104/F4-after.txt) | Real before retained / after removed. Reproduced independently in the prior audit. |
| F5 | Malformed/future/missing-field/numeric-string marker | SUFFICIENT | [F5-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/cc3e86e13f4679d0/F5-before.txt), [F5-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/547a2c5ad83b0c49/F5-after.txt) | Ignored and preserved; four real-process inputs agree with unit cases. |
| F6 | Already ready default server or recorded managed/external server | SUFFICIENT | [F6-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/025f40ee267ea4dd/F6-before.txt), [F6-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/80a08ef62754ef62/F6-after.txt) | Real default external reuse 503→272 ms despite fresh marker; recorded modes covered by unit tests. |
| F7 | No-marker reuse after adopt_default_server extraction | SUFFICIENT | [F7-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d39b885b22a5864b/F7-before.txt), [F7-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7e2ae1e9b1174ac4/F7-after.txt) | Same managed PID reused in about 300 ms; unit tests cover stale-runtime/refetch branches. |
| F8 | Real CLI dispatch and archive update-remaining path | SUFFICIENT | [F8-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9a3c5cdbdf812287/F8-before.txt), [F8-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/44935975e1a43077/F8-after.txt), [F8-archive-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/28c385610e13c38e/F8-archive-before.txt), [F8-archive-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1126e7dcd9ba7df0/F8-archive-after.txt) | Real helper parser: before unknown command; after fresh/partial numeric output, expired removal, invalid preservation. Archive launcher seeded with real source CLI, not a no-host-Node release archive. |
| F8b | Artificial old remote helper should fall back immediately | CONTRADICTED; follow-up | [F8b-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/9d19d0b707ecc35c/F8b-after.txt) | After waits 677.5 s because usage stdout is nonempty. Do not carry the old graceful-fallback claim. Packaged archiveVersion is pinned to client version, so this requires a deliberately mismatched seeded cache; outside supported matching-version flow. |
| F9 | Node launch timeout 90→150 s; archive remains 900 s | SUFFICIENT (source) | [packages/ssh/src/tunnel.ts:57](https://git.cafe/pingdotgg/t3code/blob/87ac5b3a34fe2c12561bf16f6f823e0376fda004/packages/ssh/src/tunnel.ts#L57) | Arithmetic and call-site inspection; no claim that an actual SSH command survived 150 s. |
| F10 | Install atomically publishes {startedAt,expiresAt} before backend stop | INSUFFICIENT (packaged missing) | [apps/desktop/src/updates/DesktopUpdates.ts:602](https://git.cafe/pingdotgg/t3code/blob/87ac5b3a34fe2c12561bf16f6f823e0376fda004/apps/desktop/src/updates/DesktopUpdates.ts#L602) | Unit proof exists. Run recipe A+C+D with after-old image; collect rename→backend SIGTERM ordering plus JSON TTL=60000. |
| F10b | Atomic marker publication fails; temp cleaned, install continues | SUFFICIENT (unit only) | [apps/desktop/src/updates/DesktopUpdates.test.ts:458](https://git.cafe/pingdotgg/t3code/blob/87ac5b3a34fe2c12561bf16f6f823e0376fda004/apps/desktop/src/updates/DesktopUpdates.test.ts#L458) | Optional real recipe G: destination directory forces rename failure. The earlier claim that no real filesystem fixture exists is too strong. |
| F11-sync | quitAndInstall throws out to adapter; cleanup and backend recovery | INSUFFICIENT for real packaged proof; unit covered | [apps/desktop/src/updates/DesktopUpdates.test.ts:510](https://git.cafe/pingdotgg/t3code/blob/87ac5b3a34fe2c12561bf16f6f823e0376fda004/apps/desktop/src/updates/DesktopUpdates.test.ts#L510) | No natural stock-AppImage fixture identified: upstream catches doInstall exceptions and emits error. Optional main-inspector fault injection in recipe F proves adapter catch in a packaged process; label it injected. |
| F11-event | Native installer error event during active install; cleanup and recovery | INSUFFICIENT (reachable) | [apps/desktop/src/updates/DesktopUpdates.ts:964](https://git.cafe/pingdotgg/t3code/blob/87ac5b3a34fe2c12561bf16f6f823e0376fda004/apps/desktop/src/updates/DesktopUpdates.ts#L964) | Existing error-event recovery test lacks marker assertion. Recipe E uses valid downloaded update plus unwritable current AppImage directory. A bad feed checksum alone is download failure, not F11. |
| F12 | Install fiber interrupted while stopping; cleanup | SUFFICIENT (unit only) | [apps/desktop/src/updates/DesktopUpdates.test.ts:485](https://git.cafe/pingdotgg/t3code/blob/87ac5b3a34fe2c12561bf16f6f823e0376fda004/apps/desktop/src/updates/DesktopUpdates.test.ts#L485) | Deferred/Fiber.interrupt proves cleanup. No exposed UI cancel for this fiber. SIGKILL is not an equivalent fixture. |
| F13 | Primary backend readiness clears marker; startup retains it | SUFFICIENT | [F13-before.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/abde961b895cd553/F13-before.txt), [F13-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/d64c4da949dea32b/F13-after.txt) | Real native dev readiness clears hand-written marker after fix; native/WSL configuration variants covered by unit tests. Not proof of installer-created marker or real Windows/WSL integration. |
| F13-install | Real quitAndInstall replacement, relaunch, new backend ready, marker gone | INSUFFICIENT (reachable) | [apps/desktop/src/updates/DesktopUpdates.ts:794](https://git.cafe/pingdotgg/t3code/blob/87ac5b3a34fe2c12561bf16f6f823e0376fda004/apps/desktop/src/updates/DesktopUpdates.ts#L794) | New explicit subrow for previously implicit gap. A+C+D: old hash replaced by target hash; old PID exits; new version/runtime PID ready; marker removal follows new startup. |
| F14 | Launcher adopts updated packaged desktop backend during real update gap | INSUFFICIENT (reachable) | [F14-before-after.txt](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3b727954122e7f7a/F14-before-after.txt), [F14-before-after.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/711f8e114b30015c/F14-before-after.mp4) | Existing capture is PARTIAL: after exits 1 after 92.9 s because dev root is 302. Recipe H uses packaged / response 2xx and identical isolated HOME/.t3 paths; require external + exact new backend port/PID, no managed fallback. |

The remaining diff files add test harnesses/coverage, ignore generated SSH-test scratch directories, and commit the evidence driver. They introduce no additional shipped runtime fixture: `.gitignore`, `launcherTestHarness.ts`, and `updatesTestHarness.ts` are test/evidence hygiene; the driver is assessed as a capture utility and must not be mistaken for product behavior.

## A. Build the three images needed for an honest before/after comparison

Two images are not enough if the head image is both the thing being tested at Install and the higher-version replacement. Build baseline-old at `90.23005.0`, head-old at `90.23005.0`, and head-target at `90.23005.1`. Run separate fresh homes: baseline-old→head-target for Before, head-old→head-target for After. This makes the target identical while testing whether the *installing* version writes the marker. Merely updating baseline-old→head-target never exercises the new marker writer in the old process.

Use source archives in scratch, not another lead's checkout. These are build snapshots, not git worktrees; the builder will label their commit `unknown`, so retain the archive SHA/source SHA manifest and image SHA256 as provenance. Do not misrepresent that label as a release commit. The builder temporarily aligns manifest versions and restores them; all such writes stay in these disposable source snapshots. No real data is needed: create a scratch project if the UI needs one, with no agent or scheduled task running.

The following is a **prospective execution recipe**, not commands run in this audit:

```bash
set -euo pipefail
RUN="/tmp/lead-23005-r2/audit1/run-$(date -u +%Y%m%dT%H%M%S.%N)"
mkdir -p "$RUN" "$RUN/src-before" "$RUN/src-after" "$RUN/tmp" \
  "$RUN/build-home" "$RUN/build-t3" "$RUN/build-cache" "$RUN/feed" \
  "$RUN/artifacts/before-old" "$RUN/artifacts/after-old" "$RUN/artifacts/target"
printf '%s\n' "$RUN" > /tmp/lead-23005-r2/audit1/latest-run.txt
NODE_BIN=/home/ubuntu/.local/share/vite-plus/js_runtime/node/26.12.1/bin/node
VP_BIN="$(readlink -f /home/ubuntu/.local/share/vite-plus/bin/vp)"
RUST_BIN="$(rustc --print sysroot)/bin"
TASK_PATH="$(dirname "$NODE_BIN"):$(dirname "$VP_BIN"):$RUST_BIN:$PATH"

git -C /home/ubuntu/wt/pr-23005 archive 8d3b061121fe65669d5d43d011b48cd3027a4108 > "$RUN/before.tar"
git -C /home/ubuntu/wt/pr-23005 archive 87ac5b3a34fe2c12561bf16f6f823e0376fda004 > "$RUN/after.tar"
tar -xf "$RUN/before.tar" -C "$RUN/src-before"
tar -xf "$RUN/after.tar" -C "$RUN/src-after"
printf '%s\n' 'before=8d3b061121fe65669d5d43d011b48cd3027a4108' \
  'after=87ac5b3a34fe2c12561bf16f6f823e0376fda004' > "$RUN/provenance.txt"
sha256sum "$RUN/before.tar" "$RUN/after.tar" >> "$RUN/provenance.txt"

build_env() {
  env -u VITE_DEV_SERVER_URL -u VITE_HTTP_URL -u VITE_WS_URL \
    HOME="$RUN/build-home" T3CODE_HOME="$RUN/build-t3" \
    TMPDIR="$RUN/tmp" XDG_CACHE_HOME="$RUN/build-cache" \
    XDG_CONFIG_HOME="$RUN/build-home/.config" \
    XDG_DATA_HOME="$RUN/build-home/.local/share" \
    CARGO_HOME="$RUN/cargo" CARGO_TARGET_DIR="$RUN/cargo-target" \
    npm_config_store_dir="$RUN/pnpm-store" PATH="$TASK_PATH" \
    T3CODE_DESKTOP_UPDATE_REPOSITORY= GITHUB_REPOSITORY= "$@"
}
(cd "$RUN/src-before" && build_env "$VP_BIN" i --frozen-lockfile) > "$RUN/install-before.log" 2>&1
(cd "$RUN/src-after" && build_env "$VP_BIN" i --frozen-lockfile) > "$RUN/install-after.log" 2>&1
(cd "$RUN/src-before" && build_env "$VP_BIN" run dist:desktop:linux \
  --mock-updates --mock-update-server-port 19305 --build-version 90.23005.0 \
  --output-dir "$RUN/artifacts/before-old" --verbose) > "$RUN/build-before-old.log" 2>&1
(cd "$RUN/src-after" && build_env "$VP_BIN" run dist:desktop:linux \
  --mock-updates --mock-update-server-port 19305 --build-version 90.23005.0 \
  --output-dir "$RUN/artifacts/after-old" --verbose) > "$RUN/build-after-old.log" 2>&1
(cd "$RUN/src-after" && build_env "$VP_BIN" run dist:desktop:linux \
  --mock-updates --mock-update-server-port 19305 --build-version 90.23005.1 \
  --output-dir "$RUN/artifacts/target" --verbose) > "$RUN/build-target.log" 2>&1
sha256sum "$RUN/artifacts/before-old/T3-Code-90.23005.0-x64.AppImage" \
  "$RUN/artifacts/after-old/T3-Code-90.23005.0-x64.AppImage" \
  "$RUN/artifacts/target/T3-Code-90.23005.1-x64.AppImage" >> "$RUN/provenance.txt"
```

Before starting, check `ss -H -ltn 'sport = :19305'` (and the debug ports below); if occupied, choose a free port consistently in build, feed and runtime arguments. Check `df -BG --output=avail /` before each large step; stop below **5 GB** and report the exact result. Do not use `--skip-build` across versions or revisions, do not reuse the prior one-line web stub, and do not stub the browser-secret helper. The real packaged web bundle must ship.

On this VM I confirmed Node 26.12.1, Cargo/rustc and the x86_64 GNU target directory, gcc/make/pkg-config, `libsecret-1` pkg-config, ImageMagick, Xvfb wrapper, strace, xdotool, ffmpeg and `/dev/fuse` exist. `inotifywait` is absent; use the poller below or Python inotify. Presence of `/dev/fuse` alone does not prove mounting works. If mounting fails, set `APPIMAGE_EXTRACT_AND_RUN=1` on **both** sides and preserve it through relaunch; retain that qualification and the mounted/extracted executable provenance. Do not replace the installed-image test with an unpackaged `linux-unpacked/t3code` launch.

Estimates, **not measured build timings**: about 10–25 minutes for the first image, 5–15 minutes per subsequent image with shared caches, roughly 20–55 minutes total plus 5–15 minutes for captures; a cold dependency/network/native-toolchain build can exceed one hour. Budget about 12–20 GiB additional peak space for two independent dependency trees, shared Cargo/package caches, one staging tree at a time, artifacts and video. Each image is likely a few hundred MiB; measure actual outputs. There were **29 GB available** at audit start and **31 GB** on the latest disk check, existing checkout node_modules 3.6 GiB, existing Electron caches about 267 MiB. Do not keep staging trees, parallelize builds, or delete prior evidence to make room. Native dependency installation/network errors are infra friction, not proof of the bug.

## B. Serve a higher version locally

The generated generic-provider feed uses `latest-linux.yml`. Stable artificial versions avoid preview/pr/nightly disabling and channel ambiguity. Both build and runtime mock configuration matter: the build flag writes feed configuration, whereas DesktopConfig reads the runtime environment. Empty repository variables prevent ambient release-repository settings from taking priority over the mock publish configuration.

```bash
cp "$RUN/artifacts/target/T3-Code-90.23005.1-x64.AppImage" "$RUN/feed/"
cp "$RUN/artifacts/target/latest-linux.yml" "$RUN/feed/latest-linux.yml"
# Copy the target .blockmap too if one was emitted/referenced by the manifest.
# A full-file fallback is valid: the mock server does not implement differential range responses.
build_env env T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT="$RUN/feed" \
  T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT=19305 \
  "$NODE_BIN" "$RUN/src-after/scripts/mock-update-server.ts" > "$RUN/feed.log" 2>&1 &
FEED_PID=$!
printf '%s\n' "$FEED_PID" > "$RUN/feed.pid"
curl -fsS http://localhost:19305/latest-linux.yml > "$RUN/served-latest-linux.yml"
```

Verify `version: 90.23005.1`, artifact filename/size, and base64 SHA512 against the **actual served bytes** before downloading. The builder also emits `.deb` artifacts because the Linux AppImage target includes deb; the updater selects the AppImage. There is no need to install a system package. Feed logs must show localhost requests for metadata and the selected artifact. Preserve their timestamps and checksums.

## C. Launch the actual AppImage, and keep Xvfb/D-Bus alive through relaunch

Each row/side gets a new `CASE` directory under `RUN` (for example `after-success-<timestamp>`). Set `CASE` to that absolute path and create `home/.t3/runtime`, `config`, `cache`, `data`, `tmp`, and `installed`. Copy the chosen old AppImage to `CASE/installed/T3-Code.AppImage`, chmod 755, and retain its SHA256. This versionless filename makes electron-updater replace the same path instead of renaming it to the target's versioned name. **Never run the update from the shared build artifact or feed copy.**

Save the following as `RUN/desktop-session.sh`; all log/capture paths expand inside CASE. Pass the already-absolute CASE as `$1`. Create a FIFO `CASE/stop-session` before launching. The FIFO keeps the wrapper running after the original process exits; otherwise Xvfb/dbus-run-session can disappear underneath the detached replacement. Keep the session wrapper and recorder running until the replacement is ready, rather than waiting only for the original PID.

```bash
#!/usr/bin/env bash
set -eu
CASE="$1"
printf '%s\n%s\n' "$DISPLAY" "$XAUTHORITY" > "$CASE/display.env"
strace -ff -ttt -yy -s 512 \
  -e trace=%process,%signal,rename,renameat,renameat2,unlink,unlinkat,write \
  -o "$CASE/strace" \
  "$CASE/installed/T3-Code.AppImage" --no-sandbox --disable-gpu \
  --remote-debugging-address=127.0.0.1 --remote-debugging-port=19391 \
  --inspect=127.0.0.1:19392 > "$CASE/desktop-stdio.log" 2>&1 &
printf '%s\n' "$!" > "$CASE/strace.pid"
IFS= read -r stop < "$CASE/stop-session"
```

Start it from the controller, retaining the PID of the session you create:

```bash
mkfifo "$CASE/stop-session"
env -u ELECTRON_RUN_AS_NODE -u VITE_DEV_SERVER_URL -u VITE_HTTP_URL -u VITE_WS_URL \
  HOME="$CASE/home" T3CODE_HOME="$CASE/home/.t3" TMPDIR="$CASE/tmp" \
  XDG_CONFIG_HOME="$CASE/config" XDG_CACHE_HOME="$CASE/cache" XDG_DATA_HOME="$CASE/data" \
  T3CODE_DISABLE_AUTO_UPDATE=false T3CODE_DESKTOP_MOCK_UPDATES=true \
  T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT=19305 ELECTRON_DISABLE_SANDBOX=1 \
  PATH="$TASK_PATH" \
  setsid /home/ubuntu/.local/bin/headless-desktop bash "$RUN/desktop-session.sh" "$CASE" \
  > "$CASE/session.log" 2>&1 &
SESSION_PID=$!
printf '%s\n' "$SESSION_PID" > "$CASE/session.pid"
ps -o pid=,pgid= -p "$SESSION_PID" > "$CASE/session-process.txt"
```

Use `ELECTRON_DISABLE_SANDBOX=1` for this testing session because AppImageUpdater relaunches with an empty argv and loses `--no-sandbox`; inherited environment survives. [Electron's startup source](https://github.com/electron/electron/blob/main/shell/app/electron_main_delegate.cc) checks this environment variable before creating Chromium processes. This is fixture configuration, not an application patch. Do not set `ELECTRON_RUN_AS_NODE` for the desktop. All HOME, XDG and TMP paths remain isolated through the updater's inherited environment; do not export a replacement HOME in the controller shell.

Wait for the initial primary backend's “backend ready” log and `CASE/home/.t3/userdata/server-runtime.json`. Save that JSON plus the backend PID, parent PID and `/proc/<pid>/cmdline` / executable link before installation. PID discovery from *this fresh home's* runtime file gives a concrete child to observe; do not kill processes by matching names or paths. Confirm the current version is `90.23005.0` and update state is enabled. The server-version field and desktop update state must agree with the selected old image. The app's persistent desktop logs are under `CASE/home/.t3/userdata/logs`; after relaunch those logs are essential because the replacement's stdio is detached.

Start the recording inside this retained display (read the two lines from `display.env` into task-specific variables):

```bash
DISPLAY_ID="$(sed -n '1p' "$CASE/display.env")"
AUTH_FILE="$(sed -n '2p' "$CASE/display.env")"
env XAUTHORITY="$AUTH_FILE" ffmpeg -nostdin -y -f x11grab \
  -framerate 15 -video_size 1920x1080 -i "$DISPLAY_ID" \
  -c:v libx264 -preset ultrafast -pix_fmt yuv420p "$CASE/update.mp4" \
  > "$CASE/recording.log" 2>&1 &
RECORDING_PID=$!
```

Record F10+F13-install and F14 as before/after flows; F11-event should show Install→error→usable recovered window, with the marker timeline alongside it. F11-sync is a separate, explicitly injected recording if run. F10b/F12/F3d do not require a cosmetic UI recording when supported only by their existing deterministic tests. Every video must include or accompany revision/image hashes and an epoch timestamped transcript; a window recording alone cannot establish atomic publication or which backend was adopted.

## D. Invoke the shipped update action and observe the handoff

The production route is `window.desktopBridge.checkForUpdate()` → `downloadUpdate()` → `installUpdate()`. These are exposed by preload and call the same typed IPC methods as SidebarUpdatePill; there is no need to add an HTTP endpoint or patch `isPackaged`. The visible alternative is the sidebar “Check for updates” action, followed by Download and the install/restart action. For reliable automation, use Chromium CDP against the already-running Electron renderer and call that bridge.

A small generic CDP client, saved as `RUN/cdp.mjs`, is sufficient (no new package/browser installation). The page endpoint is port 19391; main-process Node inspector is port 19392:

```js
const [endpoint, expression] = process.argv.slice(2);
const targets = await (await fetch(endpoint + '/json/list')).json();
const target = targets.find(t => t.type === 'node') ??
  targets.find(t => t.type === 'page' && /^https?:/.test(t.url));
if (!target) throw new Error('No ready main T3 page/Node target; inspect target list and retry after backend ready');
const ws = new WebSocket(target.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
  ws.addEventListener('open', resolve, { once: true });
  ws.addEventListener('error', reject, { once: true });
});
const answer = new Promise((resolve, reject) => {
  ws.addEventListener('close', () => reject(new Error('CDP target disconnected')), { once: true });
  ws.addEventListener('error', reject, { once: true });
  ws.addEventListener('message', event => {
    const message = JSON.parse(event.data);
    if (message.id !== 1) return;
    if (message.error || message.result?.exceptionDetails) reject(new Error(JSON.stringify(message)));
    else resolve(message.result?.result);
  });
});
ws.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: {
  expression: `(async () => (${expression}))()`, awaitPromise: true, returnByValue: true
} }));
try { console.log(JSON.stringify(await answer)); } finally { ws.close(); }
```

If multiple http pages exist, choose the main T3 renderer with `typeof window.desktopBridge?.getUpdateState === 'function'` before acting. The default simple selector is for a fresh, empty fixture with no browser panel. Log each result:

```bash
"$NODE_BIN" "$RUN/cdp.mjs" http://127.0.0.1:19391 \
  'window.desktopBridge.getUpdateState()' > "$CASE/update-state-initial.json"
"$NODE_BIN" "$RUN/cdp.mjs" http://127.0.0.1:19391 \
  'window.desktopBridge.setUpdateChannel("latest")' > "$CASE/set-channel.json"
"$NODE_BIN" "$RUN/cdp.mjs" http://127.0.0.1:19391 \
  'window.desktopBridge.checkForUpdate()' > "$CASE/check.json"
"$NODE_BIN" "$RUN/cdp.mjs" http://127.0.0.1:19391 \
  'window.desktopBridge.downloadUpdate()' > "$CASE/download.json"
```

Only proceed when `getUpdateState()` reports `status: "downloaded"` and `downloadedVersion: "90.23005.1"`; a refused action/background check is not a successful download. For an event-driven waiter, subscribe to `desktopBridge.onUpdateState` *before* starting the download, resolve at `downloaded`, reject at `error`, and unsubscribe when done. Inspect/feed-log any failure, rather than manufacturing an `update-downloaded` event.

Before Install, arm a watcher. `inotifywait` is absent on this VM; this 50 ms poller uses stdlib only. Save as `RUN/watch.py` and run it with the CASE path, logging stdout to `CASE/timeline.jsonl` and retaining its PID. Start it before the update action. It records marker text/mtime, runtime JSON, image identity and the old backend's liveness; the syscall trace is the stronger ordering proof if publication and cleanup happen between polls.

```python
import pathlib, sys, time, json, os
case = pathlib.Path(sys.argv[1])
marker = case/'home/.t3/runtime/desktop-update-in-progress'
runtime = case/'home/.t3/userdata/server-runtime.json'
image = case/'installed/T3-Code.AppImage'
old_pid = json.loads(runtime.read_text())['pid']
def read(p):
    try: return {'text': p.read_text(), 'mtime_ns': p.stat().st_mtime_ns}
    except FileNotFoundError: return None
previous = object()
while True:
    try: os.kill(old_pid, 0); old_alive = True
    except ProcessLookupError: old_alive = False
    stat = image.stat() if image.exists() else None
    state = {'marker': read(marker), 'temp': read(pathlib.Path(str(marker)+'.tmp')),
             'runtime': read(runtime), 'old_pid': old_pid, 'old_alive': old_alive,
             'image': None if stat is None else [stat.st_ino,stat.st_size,stat.st_mtime_ns]}
    if state != previous:
        print(json.dumps({'wall_ns': time.time_ns(), 'monotonic_ns': time.monotonic_ns(), **state}), flush=True)
        previous = state
    time.sleep(.05)
```

Then invoke `window.desktopBridge.installUpdate()` through the same CDP client and save its result/disconnection. On success the old renderer disconnects during normal quit, so an IPC result alone is not the success criterion. Retain syscall traces, disk logs and the poller through updated backend readiness.

**F10 acceptance:** in the after case, observe a nonempty valid marker whose `expiresAt-startedAt == 60000`; trace write to `.tmp`, atomic rename to final path, then first desktop→old-backend SIGTERM. Filter by the exact recorded backend PID; do not mistake renderer signals for backend stop. The marker must remain through the actual installation gap. A before case must show installation/shutdown without publication. The unit test establishes the exact internal call order; syscall ordering establishes real file publication before termination. If the poller misses the JSON, the `write` trace includes its bytes and pathname (`-yy`) instead of guessing that absence means publication failed.

**F13-install acceptance:** compute installed image SHA256 after replacement and require equality with head-target; require old backend PID exit, new PID running from the replacement AppImage, runtime `serverVersion: "90.23005.1"`, GET `/` response 2xx with no `devUrl`, “backend ready” in the new desktop session, then marker absence. Confirm the removal is from new readiness, not launcher expiry or recovery: in this no-launcher case use the trace to associate unlink with the replacement process after backend startup, and show it happens before the recorded `expiresAt`. Do not manually relaunch after success: that would conceal a broken force-relaunch path. Do not require the debugger ports to survive: AppImageUpdater drops command-line switches, so the replacement need not expose CDP.

## E. F11: real updater error event during Install

Use a fresh **after-old** case, valid target feed, and successful completed download. Just before invoking Install:

```bash
chmod 0555 "$CASE/installed"
```

The current VM user is unprivileged `ubuntu` (uid 1000). A read/execute-only parent directory prevents AppImageUpdater's `unlinkSync(APPIMAGE)` from deleting the current image, without preventing the earlier download into the separate writable XDG cache. BaseUpdater catches EACCES, emits `error`, and returns false; it does not quit. This reaches the app's active-install error handler and `recoverFailedInstall`.

Acceptance: publication → backend termination → native installer EACCES error → marker unlink **before recovery backend becomes ready** → same desktop process retained → recovered backend ready → install error state/canRetry. Require both update markers and the install record to be cleared, and only the formerly desired-running backend restarted. The event handler can run asynchronously after the IPC result, so wait for the state event/log milestone; do not infer failure cleanup from the immediate Install return. The timeline and trace must distinguish recovery removal from the common readiness deletion. Restore this fixture's directory mode to 0755 after capture. Run baseline-old the same way for a before comparison of recovery behavior, expecting no new in-progress marker.

A feed with an intentionally wrong SHA512 is a useful **download-error control**, not this row: clone the feed into a new CASE-specific feed directory/port, replace only the checksum with an invalid value, check/download, and require errorContext download and **no marker ever published**. Never mutate a shared feed during another run. Preserve its erroneous manifest and download error log. A checksum-valid junk executable would instead test failed relaunch, which can leave a marker until TTL; do not call that a successful F11 recovery test.

## F. F11: synchronous throw (explicit fault injection only)

Stock `electron-updater` 6.8.9 catches the ordinary AppImage filesystem and install exceptions inside `BaseUpdater.install`, emitting `error`; neither a corrupt feed nor EACCES naturally escapes as the app adapter's synchronous throw. The existing unit case already proves that typed-error cleanup branch. There is no ordinary UI-only recipe for forcing it without modifying runtime behavior.

If packaged-process branch coverage is required, use a separate fresh after-old case, download the valid artifact normally, then evaluate this expression through the **main-process inspector** (19392), immediately before the normal renderer Install action:

```js
(() => {
  const original = console.info;
  globalThis.__auditOriginalConsoleInfo = original;
  console.info = function (...args) {
    if (args[0] === 'Install on explicit quitAndInstall') {
      console.info = original;
      throw new Error('audit-F11-sync-native-refusal');
    }
    return Reflect.apply(original, console, args);
  };
  return 'one-shot quitAndInstall entry fault armed';
})()
```

The updater uses `console` as its default logger in this checkout; its entry log occurs in `BaseUpdater.quitAndInstall` **outside** the native `install` try/catch. This one-shot logger fault therefore escapes to the app adapter after marker publication and backend stop. It also works when electron-updater is bundled into `main.cjs`; do not require a second copy of the module from the ASAR. Then call the normal `desktopBridge.installUpdate()` via the renderer, and require marker creation, stop, caught typed installer failure, marker cleanup before recovery readiness, and restored usable backend. This changes no source file or artifact but **is runtime injection**; label its evidence accordingly, never as an unmodified native installation failure. The one-shot fault restores `console.info` before throwing. If Install is never invoked, restore `console.info = globalThis.__auditOriginalConsoleInfo` in the inspector or terminate this throwaway session. If the packaged inspector is unavailable, retain unit-only coverage and state this injected capture was not reachable; do not claim a checksum error substituted for it.

## G. F10b: optional real filesystem failure

After valid download and before Install, make a **directory** at `CASE/home/.t3/runtime/desktop-update-in-progress` (it must be absent before this fixture). Writing the `.tmp` file succeeds; renaming that file over a directory fails with EISDIR. Require the warning “Could not write the update-in-progress marker.”, no `.tmp` after failure, and continued backend stop/actual installer invocation. This drives the same publication-failure catch without a mocked filesystem. It deliberately makes the final path unreadable as a JSON file; do not confuse its directory existence with a valid marker. Do not use a read-only whole T3 home: that can block unrelated state writes and invalidate the fixture. This optional capture is additional corroboration; the existing deterministic rename-failure test remains sufficient for this row.

## H. F14: adopt the updated packaged backend over the exact launcher path

1. Generate the launcher scripts from each source snapshot before timing the update, using the committed driver's capture helper (it stubs the *capture transport* only). Never run the full prior driver against an existing lead's worktree: it writes/deletes a helper alongside tunnel.ts and starts development desktops. Extract only its preamble/capture function into `RUN/capture-only.sh` by copying the text **before** `STARTED_PIDS=()`, omitting the helper-cleanup line `rm -f "$helper"`, and appending `capture_scripts` on its own line. Leave the temporary helper in the disposable source snapshot; this avoids variable-path deletion. Invoke it as `bash "$RUN/capture-only.sh" "$RUN/src-before" capture "$RUN/capture-before"` and similarly for src-after/capture-after. All helper writes then stay in the new source snapshots. This produces exact node and archive scripts without manually editing the launcher logic.
2. Start separate before and after packaged cases using A–D, arm recording/strace/poller, and complete valid download. Retain `oldPid` from the initially ready backend, not from a later runtime-file read.
3. Launch a controller armed **before** Install. On the after side it must see the installer-created fresh marker, then wait for the recorded old backend to exit before starting the launcher, while the replacement is not ready. On the before side there is no marker, so trigger on old backend exit alone. Use a 50 ms process/HTTP watcher or the exact SIGTERM/exit trace; log the chosen instant. Do not add a hand-written marker to the actual-update run. If the replacement is already ready when the launcher starts, the run only proves ordinary reuse (F6), so repeat a fresh case and leave F14 open.
4. In the controller, run the exact **node-mode** captured launch script against the shared isolated home:

   ```bash
   env HOME="$CASE/home" T3CODE_HOME="$CASE/home/.t3" PATH="$TASK_PATH" \
     sh -s -- audit-k1 < "$RUN/capture-after/launch-node.sh" \
     > "$CASE/launcher.out" 2> "$CASE/launcher.err" &
   LAUNCHER_PID=$!
   ```

   Use capture-before for Before. Keep the real source CLI backend available as a potential fallback; do not stub `nohup`, `sleep`, node, or the readiness probe, and do not change the packaged backend's root route. Pin PATH to the installed Node so the isolated HOME cannot cause a version manager download.
5. For **real SSH transport**, run the same script over the VM's existing loopback sshd, first verifying `ssh -o BatchMode=yes localhost true`. Pass the absolute isolated HOME in the *remote command*, for example `ssh -o BatchMode=yes localhost "env HOME='$CASE/home' T3CODE_HOME='$CASE/home/.t3' PATH='$TASK_PATH' sh -s -- audit-k1" < ...`. Do not rely on local HOME being forwarded; do not invoke a login shell that resets it. This tests a real SSH connection on one machine; it is not a two-machine macOS capture. If localhost key auth is unavailable, record that transport limitation and retain direct-shell launcher proof, without modifying sshd or credentials.
6. Require launcher exit 0 with exactly `{"remotePort":<new desktop backend port>,"serverKind":"external"}`. Match this port to the **new** runtime JSON, desktop parent/child process provenance, `serverVersion: 90.23005.1`, and packaged `/` 2xx response without redirects. Require marker removal before expiry, one surviving ownership holder, and no managed server launch at any point. Check `ssh-launch/audit-k1/managed` is `external`, with no managed PID. The old PID cannot be counted as the replacement. Do not count “no second surviving server” alone: a competing fallback can fail ownership and exit, as the prior partial F14 did.
7. The shipped packaged client's normal runner is **archive mode**, pinned to its app version. A further matching-version archive run is useful, but the prior F8 archive capture uses a seeded source-CLI wrapper and does not prove release extraction without host Node. To avoid fetching a nonexistent artificial `90.*` release, explicitly preseed `HOME/.t3/runtime/versions/9.9.9/t3` and `.install-complete` using the same real-CLI wrapper as the retained driver, then use the captured archive script (which requests 9.9.9). Label this as a **seeded archive launcher**, not a real published-release or no-host-Node test. Do not claim node-mode F14 proves archive download/bootstrap. The main missing F14 row here is host update+launcher adoption; neither the feed nor installer needs to be faked to prove it.

If a normal installer replacement is too fast to observe a gap, a *separate explicitly stalled fixture* may SIGSTOP the new desktop PID as soon as strace reports its exec, start the launcher, then SIGCONT it; retain the actual native update run separately and disclose the stall. Signal only PIDs observed as new children in this run's trace. Do not add a production sleep or change the host clock. A no-stall successful trace is preferred; don't preemptively modify the flow.

## Reachability, recordings, cleanup, and scope

- **Reachable on this VM:** F10, F11-event, actual AppImage replacement/relaunch, and F14 packaged-backend adoption. F10b also has a real rename-failure fixture. They remain unproved until the recipes are executed; lack of a packaged artifact today is a missing build step.
- **No natural unmodified path identified:** F11-sync with this AppImageUpdater (ordinary exceptions become events). The inspector recipe covers it only as an injected fault. F12 cancellation has no exposed UI action for interrupting the install fiber; process SIGKILL bypasses finalizers. Keep its deterministic interruption test.
- **Other platforms/physical conditions:** actual Windows/WSL backend integration, Windows installer and macOS replacement cannot run on a Linux AppImage VM. Native+WSL-shaped unit configuration is not a Windows integration capture. A real system-clock jump is inappropriate on a shared VM; deterministic fake-clock proof suffices. This review does not demand those as new Linux gate blockers.
- **Recording required:** F10+real replacement+F13-install can share one uncut before/after video with timestamped syscall/JSON evidence; F14 needs an integrated update/launcher recording with the selected backend identity; F11-event needs its separate error/recovery recording. Existing F2/F3 videos remain valid; do not regenerate unchanged rows merely for the new driver commit.
- **Stop only what the future executor starts.** Track feed, poller, recorder, controller/SSH/launcher, strace and session PIDs at spawn; also record the original/replacement desktop and backend child PIDs from this run's trace/runtime metadata. On completion stop recording with SIGINT and wait for flush, terminate the launcher/controller and any managed fallback created in this scratch home, quit/terminate both desktops/backends, then stop strace/poller/feed. Release the FIFO to let the wrapper close its D-Bus/Xvfb session; if necessary terminate only the session process group captured at spawn, and verify its members have exited. Never infer a kill target from a process-name/path search. Restore chmod fixtures first. Do not run a cleanup that waits on strace while its desktop children remain alive.
- Upload all videos, transcripts, provenance and relevant raw logs with ghdrop before any deletion. Split trace archives/video to stay within its 100 MB per-file limit. No source or PR changes are authorized here. If deleting later, use one literal absolute path per command, only a path this executor created, and never delete evidence before upload. No deletion occurred in this audit.

Coverage of this audit: full 13-file PR diff and existing fixture inventory; all 26 existing text captures checked; updater adapter/native failure routing; packaged build/feed flags, production preload/IPC route, base-dir selection and readiness probe; VM build prerequisites and space. No runtime rows were newly executed, no tests rerun, no browser or server started, and no other lead worktree touched.

FRICTION:
- tool | Python urllib received HTTP 403 while fetching existing ghdrop captures; curl with a browser User-Agent fetched all 26 successfully | Use the working User-Agent for evidence downloads.
- runbook | Prior matrix suggested corrupt downloads could exercise failed-install cleanup; checksum validation runs before the marker is written | Separate download, native install, and detached relaunch failures in the fixture recipe.

VERDICT: INSUFFICIENT F10, F11-event, real AppImage replacement/relaunch (F13-install), F14; F11-sync packaged capture remains unit-only or explicitly injected
