diff --git a/docs/internals/lifecycle-handoff.md b/docs/internals/lifecycle-handoff.md new file mode 100644 index 000000000000..8706524a3c78 --- /dev/null +++ b/docs/internals/lifecycle-handoff.md @@ -0,0 +1,147 @@ +# Detached lifecycle handoff + +A lifecycle action that stops or replaces T3 (install, update, restart) cannot be +performed by an agent running *inside* T3: the action kills the agent that is +executing it, so the last step — reporting back — never runs. A **detached +handoff** moves the action to an OS-owned helper whose lifetime is independent of +T3, and gives that helper enough state to relaunch T3 and report the result into +the thread that requested it. + +Implementation lives in [`scripts/lifecycle-handoff/`](../../scripts/lifecycle-handoff/README.md). + +## Execution model + +``` +initiating agent (inside T3) + ├─ writes a user-private envelope PREPARED + ├─ registers a transient OS job DETACHED + └─ proves the helper is OS-owned, then quits T3 + │ +OS-owned supervisor (launchd / Task Scheduler) + ├─ verifies its own ancestry + ├─ waits for the initiating pid to exit WAITING_FOR_EXIT + ├─ runs the lifecycle command APPLYING + ├─ relaunches T3 RELAUNCHING + ├─ waits for readiness WAITING_FOR_T3 + ├─ runs an optional identity probe + ├─ delivers the callback + writes the result CALLBACK_PENDING + ├─ writes the terminal result envelope COMPLETE / FAILED + └─ removes its own job registration +``` + +The supervisor never force-kills a process. If the waited-for pid is still alive +when its bound expires, the handoff fails with `FAILED` and durable evidence +rather than killing active work. + +## Supervisor mechanism + +**macOS** — a transient, per-handoff LaunchAgent in `~/Library/LaunchAgents` +(`RunAtLoad`, `KeepAlive=false`, `ProcessType=Background`, log to the handoff +directory). `launchctl bootstrap gui/ ` starts it; its direct parent +is launchd (pid 1), so T3/Electron job-object teardown cannot reach it. It removes +its own plist and `launchctl bootout`s itself as its last step. + +**Windows** — a transient current-user Scheduled Task created from an XML task +definition (`InteractiveToken`, `LeastPrivilege`, one `TimeTrigger`, +`StartWhenAvailable`). The Task Scheduler service owns the process, so it is not +an Electron job-object child. `schtasks /Delete` removes it at the end. This path +is implemented but **has not been executed on a Windows host** (see "Validation"). + +`nohup`, `Start-Process`, and terminal detachment are deliberately not used: +neither reliably escapes the parent job object on the platforms that matter. + +## Independence proof + +Before the initiating agent may quit T3 it must show the helper cannot be killed +with T3. `handoff.mjs prepare` polls `launchctl print gui//