# Desktop updater evidence — takeover 15386

PR [#15386](https://git.cafe/pingdotgg/t3code/pulls/15386), mirrored [#15386](https://github.com/pingdotgg/t3code/pull/15386). Captured 2026-10-25 UTC in `/home/ubuntu/wt/pr-15386`, branch `takeover/pr-15386`, capture HEAD [d8a48b84a8](https://github.com/pingdotgg/t3code/commit/d8a48b84a87ce6a59628b2da2a095647a5862618). Before source [0840bb209c](https://github.com/pingdotgg/t3code/commit/0840bb209c8a503ba74715167e4ae04c4c0e0c19). Original submitted [42344734a2](https://github.com/pingdotgg/t3code/commit/42344734a24db6a315ecbff45abc5d23089b52a9) was not the capture target.

## Result

The real Electron sidebar shows “Could not install update” on the FIRST confirmed install request after the change. Before, the same synchronous missing-path error goes through global recovery, but the returned action result has no message, and there is no first-request failure toast. Both recover the backend and retain the downloaded version.

Actual IPC contract is `{accepted, completed, state}`. It does NOT expose `failed`; Schema encoding strips the service's internal field. The after result is accepted=true, completed=false, state.message containing the typed install error, state.errorContext=install, downloadedVersion=0.1.3, canRetry=true, status=downloaded. Before the returned result's message/errorContext are null; its global recovery completes afterward. Do not copy the earlier matrix's failed=true API claim or status=error claim into a PR.

Settings remains visually unchanged: its handler ignores a resolved install result. It receives the same improved error-bearing result but shows no resolved-result failure toast. The settled tooltip remains the ordinary downloaded wording because status remains downloaded. These are pre-existing surface behaviors, not changes made in this takeover.

## Environment and fixture boundary

Linux x86_64; Electron 44.9.3, electron-updater 6.8.9; app version 0.1.2, fixture target 0.1.3. Real full T3 desktop windows, renderer, preload, IPC, update service, backend processes and recovery. Xvfb 1920x1080 through headless-desktop; ffmpeg x11grab at 15fps on each run's own DISPLAY, not preview. App window is 1100x780 centered on the display. Light theme. --no-sandbox --disable-gpu; no claim about sandbox, signing or policy.

scripts/fixture-main-capture.cjs preserves the main capture fixture; scripts/fixture.cjs adds extra-run marker/process instrumentation, throw/multi modes and native popup. All fixture/automation/capture code is outside the worktree under /tmp/ev-15386/scripts. No tracked source content changed, no checkout changed, no commits/push/comments/watch. The before build replaces only ElectronUpdater.ts at bundle load time with git show of the exact base. Those two revisions differ in production only in this file; all other production code, renderer and preload match. The build plugin is retained. after uses the ordinary desktop build. A loader hook immediately after the bundled electron-updater singleton is created attaches the fixture; it does not replace services, IPC responses, preload or renderer.

Explicit overrides: app.isPackaged=true, appPath=worktree, getVersion=0.1.2, APPIMAGE=nonexistent disposable path, mockUpdates=true with loopback port 39186, and a controlled updater check boundary. This is an APP-LEVEL fixture, not a packaged update/download test. No feed server/artifact was used: downloaded state comes from update-available and update-downloaded events on the actual bundled updater singleton. checkForUpdates is a no-network seam. Initial runs return null from it; the extra run also emits update-not-available. Consequently periodic checks can leave the initial run checking until the next downloaded fixture event; those setup states are retained in raw video and not represented as install behavior.

Synchronous missing-path mode keeps the REAL installed BaseUpdater.quitAndInstall/install implementation and sets only its in-memory downloadedUpdateHelper=null. It emits the installed library's real missing-path error synchronously. Async mode returns normally then emits with setImmediate. Success mode returns normally and suppresses OS handoff/quit. Throw/multi modes are explicit controlled updater-boundary variants. Success proves HANDOFF ONLY, not installation or relaunch.

Fresh T3 homes /tmp/ev-15386/<exact SHA>/t3, isolated HOME/config/data/cache below the same run, T3CODE_DEV_SEED=0. No ~/.t3/userdata access of any kind. No import of projects. Provider update checks switched off through real Settings before app update clicks; no provider update was invoked. A mistaken Settings click during a periodic-check state installed only the CLI shim into the isolated test HOME, never the host HOME; it is visible in Settings and noted as an unrelated difference.

## First-click pair (timestamps and steps BEFORE images)

Before: recording starts 00:27:51.266; first updater call at 00:28:58.931 UTC, result 00:28:58.934. After: recording starts 00:27:47.466; first updater call 00:28:57.625, error .626, result .637. Steps: establish downloaded fixture, disable provider version checks, click sidebar downloaded icon ONCE, see confirmation, click Confirm ONCE. Source frames are about 350ms after the respective updater calls; after source recording offset 70.51s, before 68.02s. Frame quantization is 1/15s. Clipped recordings include the call and recovery; full recordings and action timestamps are in the archive.

BEFORE — no first-click toast:

![Before first confirmed click](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/fd85804bcac7dd44/before-sync-first.png)

AFTER — first-click failure toast:

![After first confirmed click](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/631418745cc05a72/after-sync-first.png)

Absolute difference, contrast x3 (derived):

![First-click absolute difference](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/97d1ab90e45dd894/diff-absolute-sync-first.png)

50% overlay (derived):

![First-click overlay](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/1f0bff0ec1a4ccdb/diff-overlay-sync-first.png)

What else changed: mouse position and icon animation frame can differ; backend transition can briefly change the empty-state subtitle. No layout/style change in the patch. Raw full-display frames retain black Xvfb margins. Derived diff images are never presented as raw captures.

## Settings pair (timestamps and steps BEFORE images)

Before confirmed Settings install at 00:32:13.649 UTC (updater call); after at 00:32:12.165. Steps: re-establish downloaded state independently, Settings > General > About > Install, accept confirmation, wait for backend recovery, hover Install. Settings end stills taken about 00:32:20 UTC; exact shot actions are in actions.jsonl. Both show Update available / Install and no resolved-result failure toast. After action result carries the install message. The isolated CLI shim path contains each SHA, so that row is an unrelated pixel difference. The accidentally named settings-confirm.png files from an earlier checking-state attempt are NOT evidence of an update confirmation; use settings-confirm-actual.png and the settings clip.

![Before Settings recovery](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/596b794a45ad52ce/before-settings.png)

![After Settings recovery](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/a5f2eb74b68a9436/after-settings.png)

![Settings absolute difference](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/ad0a532c79b4fc6f/diff-absolute-settings.png)

![Settings overlay](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/f3e9ec40358eec41/diff-overlay-settings.png)

## Coverage / precise limits

| Row | Evidence and outcome |
|---|---|
| S1 / B1 | First-call before/after video, pair above, real missing-path branch, standard diagnostics. After typed error returned in state; base result precedes global recovery. Both downloaded/retryable afterward. |
| S2 | Independent Settings downloaded fixture and confirm in both runs. No failure toast in either; action result improved after. |
| S3 | extra run enables real legacy sidebar through Settings. First confirmed click gets typed install error and backend recovery; video and stills retained. |
| A1 | Both revisions: initial action result has no message; setImmediate emission reaches global DesktopUpdaterReportedError operation=install and backend recovery. No synchronous error relabeling. |
| A2 | Main run after recovery re-emits exact Error object and then distinct error; both produce one background diagnostic each. Error listener count is baseline 2, versus 3 temporarily during after install. |
| OK1 | Both success modes call quitAndInstall(true,true), return accepted=true/completed=false/no message, no synthetic error/recovery. OS handoff suppressed. |
| OK2 | NOT RUN: no genuine packaged download/install/relaunch. |
| R1 | after calls 1,2,3 repeat the same real missing-path failure, one call per confirmation. Listener count stays 3 during and 2 afterward. Later success mode handoff works. |
| C1 | Single Cancel in main after run invokes no updater; subsequent confirmed request works. Forced same-tick JS b.click();b.click() in extra opens sequential confirmations: canceling first leaves second, eventually one confirmed updater call. Do NOT claim full duplicate-confirmation protection. Native user double-click was not separately proven. |
| T1 | Sidebar first-click failure toast proven. Update-downloaded toast from actual download API path NOT PROVEN: fixture establishes events directly. No invented install action in a toast. |
| M1 | Actual native Help submenu captured in extra/native-help-menu.png: Check for Updates only, no Install. Real menu click callback runs updates.check reason=menu before/after recovery. Downloaded-state checks preserve the download, so no up-to-date dialog is claimed. Screenshot filenames containing check-dialog do not establish such a dialog. |
| D1 | Removed diagnostic-parser requirements not applicable. Standard diagnostic captures typed errorTag, cause, channel, isSilent=true, isForceRunAfter=true. Exactly one typed install-failure diagnostic per synchronous request; no duplicate service global error for that same emission after. |
| D2 | extra throw and two synchronous emissions. Both recover, remove both markers and return error-bearing state. Multi diagnostic retains First synchronous installer error, not second. Baseline listener count restored to 2. |
| P1/P2 | NOT RUN: Windows/macOS signing, packaging, native install/relaunch. |
| X1 | Local Electron preload/IPC is exercised. Remote/relay/tunnel and web/mobile runtime coverage NOT claimed. |

Extra run direct marker observations: after each sync/throw/multi call, restart=false and install=false; backend child exits and a new backend PID starts; backend-ready log follows. This indirectly demonstrates quitting reset via working subsequent calls and menu checks; no direct Ref introspection claim. Main-run first-call marker status wasn't instrumented; extra reproduces the same real missing-path case with logging added.

## Diagnostics and tests

After first synchronous call: ElectronUpdaterQuitAndInstallError, cause “No update filepath provided, can't quit and install”, channel latest, silent=true, force-run=true. Before: DesktopUpdaterReportedError, operation install, same underlying cause. Async remains DesktopUpdaterReportedError operation install in both. Full stdout/stderr and trace chronology are in the bundle. Focused `vp test run apps/desktop/src/electron/ElectronUpdater.test.ts`: 1 file, 2 tests passed. No repo-wide checks.

## Cleanup and audit handoff

All launched Electron apps and ffmpeg recorders closed. Every recorded Electron/recorder/backend PID is absent in process-cleanup.json. Displays were owned by headless-desktop sessions; no other display/process/worktree was touched. Known build output created in the worktree is moved to /tmp/ev-15386/built-after for audit replay. No browser preview used. Artifacts remain under /tmp/ev-15386 for auditor pass 2. Disk remained above 16GB free (5GB stop threshold never reached).

## FRICTION

category | event | prevention
--- | --- | ---
tool | ghdrop rejects empty files; four empty log files cannot be uploaded individually | preserve them in the archive and list them explicitly; upload nonempty files individually
tool | electron-updater is bundled, so requiring the installed package creates a different singleton | hook the bundled singleton; attempt0 kept separate
brief | original matrix expected failed in IPC and status=error | inspect actual contract/state and report returned message instead
runbook | periodic check seam returned null without completion event; a Settings click hit isolated CLI Install | emit matching completion event in extra fixture; verify button/state before click
other | same-tick forced DOM double click queued two confirmations | record partial C1 result; don't claim duplicate protection from a cancel-only test
infra | none | isolated displays, homes and recorded PIDs; disk stayed above threshold
forge | none | no forge mutation or PR polling
policy | none | explicit computer-use/ffmpeg authorization; no live userdata access
model | none | no delegation

Work performed by gpt-6.1-sol through the Codex harness in T3 Code.

Empty logs preserved in archive: extract.log, launch-before.log, launch-after.log, launch-extra.log. ghdrop rejected these zero-byte files; they contain no omitted evidence.

Revision boundary at handoff: worktree was advanced externally after captures to [23b5eac74f](https://github.com/pingdotgg/t3code/commit/23b5eac74f449671961774456fe3570cfbb51da4). No reset or source edit was performed. Retained sourcemaps match the pinned before/after adapter source byte-for-byte (source-map-verification.json); captured bundles were moved out before this later advancement.

## Uploaded recordings

[before-sync-first.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/659407df0d75871d/before-sync-first.mp4)

[after-sync-first.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/7542e2986759c538/after-sync-first.mp4)

[before-settings.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/0dc35d3acad81dca/before-settings.mp4)

[after-settings.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/be96bb91cef4e31d/after-settings.mp4)

[before-async.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/2e47c1d63f0797ca/before-async.mp4)

[after-async.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c8585728f569c66f/after-async.mp4)

[before-success.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/352d446f31badc4d/before-success.mp4)

[after-success.mp4](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/91fcea9ded64cc58/after-success.mp4)

[Standard diagnostic transcript](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/5a77ac1368c22d75/DIAGNOSTICS.txt)

Actual native Help submenu (extra run):

![Native Help menu: Check for Updates only](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/41392081c979b958/native-help-menu.png)

Verbatim ghdrop output for all individual uploads: [UPLOADS.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/42f73ed319c475d4/UPLOADS.md)

