Skip to content

hand the windows update off to a detached installer - #3302

Closed
kevinjosethomas wants to merge 1 commit into
mainfrom
lane/windows-update-handoff
Closed

kevinjosethomas wants to merge 1 commit into
mainfrom
lane/windows-update-handoff

Conversation

@kevinjosethomas

@kevinjosethomas kevinjosethomas commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

On Windows, prime-agent update can never publish: the running CLI is the payload binary under <prefix>\share\prime-agent, and Windows will not let the installer rename that directory while a process that lives in it is still running — so the funnel's spawn-and-wait wedges the installer mid-publish forever. The funnel now hands off on Windows when the caller IS that payload binary: it spawns the installer detached without waiting, prints one handoff line, and exits so the publish can land; the TUI's /update does the same and hands the terminal back before it exits. Unix keeps its deterministic spawn-and-wait (and its e2e tests unchanged).

  • pa-core update::installer: RunOutcome::Handoff — the gate is cfg(windows) plus "this exe is <prefix>/share/prime-agent/prime-agent.exe in a marked tree" (canonicalized path compare + the install marker as the ownership proof); the installer spawns with the product's detached-spawn contract and is never waited on. The gate sits in execute_script, so fix update rollback and archive on installer installs #3278's bundled-installer paths inherit it on rebase (its Macroscope thread on installer.rs:251).
  • The channel switch stays a completed-run persist (not persisted on handoff — the detached installer carries the channel, and the publish marker it writes is the durable record).
  • install.ps1 check: the Rust funnel never shells out to install.ps1 on Windows (it runs the fetched install-rust.sh through the trusted Git Bash), so no ps1-side handoff exists to fix.

Parity-diff evidence: no TS counterpart exists — the TypeScript product never shipped a Windows build, so this surface has nothing to diff; the unix funnel is unchanged and its e2e (installer_update_e2e) reran green on this branch.

Ownership: pa-core owns the funnel (the handoff is one more outcome of its exec step); the two pa-cli surfaces own printing and exiting; pa-tui exposes its existing one process-exit restore to the composition root; dependency direction unchanged.

Testing (Prime sandbox, rust pinned to 1.98.1, python 3.12 first on PATH):

  • cargo fmt --all --check and cargo clippy -p pa-cli -p pa-core -p pa-tui --all-targets -- -D warnings green.
  • Green: cargo test -p pa-core --lib update:: (36), -p pa-cli --lib -- update installer (27), -p pa-tui --lib -- update_command exit_restore , --test installer_update_e2e (3), --test installer_platform_map (3), --test release_workflow (6, no skips), --test windows_update_handoff_e2e (compiles, 0 tests on unix).
  • cargo check --target x86_64-pc-windows-gnu -p pa-cli --all-targets green; pa-core lib green on that target (pa-core's kernel_startup_join test red on windows-gnu is pre-existing — identical on main).
  • New windows-only e2e crates/pa-cli/tests/windows_update_handoff_e2e.rs rides the windows battery (windows-runtime-triage): the payload-binary CLI exits while the installer is still running, and the detached installer survives the exit and lands its payload.

Note

Hand off Windows update to a detached installer instead of waiting

  • On Windows, when the running process is the installed payload binary, execute_script in installer.rs now spawns the installer detached with a null stdin and its own process group, then returns without waiting. This lets the caller exit before the installer renames the payload directory, which would otherwise fail while the payload is running.
  • Adds a RunOutcome enum with separate Installed and Handoff variants, so callers can distinguish a completed update from a detached handoff.
  • The handoff gate (caller_is_payload_binary / is_payload_binary_of) only triggers when the current executable canonically matches the marked payload executable under the install prefix. A Windows unit test covers marker and path matching.
  • Both update surfaces — the TUI (run_update in client_update.rs) and the CLI (run in installer_update.rs) — restore the terminal, print a shared handoff message, and exit successfully. Only a completed install persists the explicit channel. restore_terminal in exit_restore.rs is now public for the CLI to call.
  • Adds a Windows-only e2e test in windows_update_handoff_e2e.rs using a loopback HTTP server and a mock installer to verify the update exits within budget and the detached installer finishes afterward.
  • Behavioral Change: Unix installer tests and result handling are adapted to the new RunOutcome enum; a handoff result is treated as unexpected on Unix. Windows handoff runs skip the launcher version probe and do not persist the requested channel.

Macroscope summarized d07134f.

Windows keeps a running executable's directory un-renameable, and the
running CLI is the payload binary the installer replaces (the .cmd and sh
launchers exec <prefix>/share/prime-agent/prime-agent.exe), so the
spawn-and-wait channel funnel could never let the installer's publish
(the rename of that directory) happen.

When the caller IS the install's payload binary (cfg(windows) + the
marked-tree gate), the funnel now spawns the installer detached without
waiting and reports a Handoff outcome; the CLI and the TUI /update print
the handoff line and exit, so the unlocked directory lets the publish
land. Unix keeps the deterministic spawn-and-wait (and its e2e tests
unchanged).
@snimu

snimu commented Oct 2, 2026

Copy link
Copy Markdown
Contributor
  • Draft CI skipped everything including the required windows cross-check, and the base predates let the windows test targets compile #3306 — undraft and rebase before this is assessable.
  • It collides with fix update rollback and archive on installer installs #3278 rather than stacking: both define PAYLOAD_BINARY, two different gates for the same "am I the running installer" fact, different spawn modes (detached vs same-console), incompatible execute_script signatures. "fix update rollback and archive on installer installs #3278 inherits it on rebase" is not true for fix update rollback and archive on installer installs #3278's own handoff branch — pick one owner for the handoff.
  • The detached installer writes to the caller's console while HANDOFF_LINE says "this window can close"; under set -eu a write to a closed console can abort the install invisibly. Write to a log file and name it in the handoff line.
  • The TUI calls process::exit from a tokio::spawn task while the renderer is live; route a handoff note through the normal TUI exit.
  • "Wedges forever" in the body is wrong: mv under set -e fails, it doesn't hang.

[written by prime-agent, reviewed by snimu]

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants