Context
PR #124 ships descendant-tree teardown on Windows via taskkill /F /T /PID (kill_tree_windows in crates/agent-tui-daemon/src/pty.rs). That was a deliberate pragmatic call, recorded here so it is a decision rather than an accident.
The purist alternative: one Job Object per pane — CreateJobObject + AssignProcessToJobObject at spawn, TerminateJobObject on teardown. No exec, no external binary dependency, atomic tree kill.
Why it is not a drop-in
- Assignment race — a job captures future children of assigned processes, so correct usage is spawn-suspended → assign → resume. portable-pty owns the ConPTY
CreateProcessW; the daemon can only assign post-spawn via Child::as_raw_handle(), and a fast child can spawn a grandchild in that window, escaping the job.
- conhost is outside the child's tree — the ConPTY host is spawned by the console subsystem, not the pane child, so no tree-kill mechanism reaches it by parentage.
ClosePseudoConsole (see PtyChild::close_master) is required regardless; a job would not replace the whole teardown.
- Nested-job compat —
AssignProcessToJobObject fails when the child is already in a non-nestable job (CI agents, Windows Terminal, EDR sandboxes run processes under jobs). Windows 8+ nests, but the matrix needs a documented stance and test coverage.
- The in-process alternative is literally taskkill — a Toolhelp32 snapshot walk + per-PID
TerminateProcess is taskkill's own implementation, except in-crate we would own the PID-reuse TOCTOU races. (The root PID is stable either way: the daemon retains the child handle, and Windows does not reuse a PID while a handle to the process object is open.)
Also verified during review
- portable-pty 0.9's Windows kill path is unusable:
win/mod.rs do_kill inverts the TerminateProcess success BOOL (returns Err on success), and WinChild::kill swallows it into an unconditional Ok(()). It also never assigns children to a job and does not pass CREATE_NEW_PROCESS_GROUP.
Possible shape
- Per-pane job created at pane spawn; child assigned post-spawn via
as_raw_handle() (accepting the small assignment race, or closing it later by taking over the ConPTY spawn the way spawn_headless_windows already owns the headless spawn).
TerminateJobObject in PtyChild::kill / reap_all_panes; conhost still via ClosePseudoConsole.
- Fallback to the current taskkill path when assignment fails (already-in-a-job).
Non-urgent: the taskkill path is hardened (absolute %SystemRoot% path, CREATE_NO_WINDOW, exit-128 = already-gone).
Context
PR #124 ships descendant-tree teardown on Windows via
taskkill /F /T /PID(kill_tree_windowsincrates/agent-tui-daemon/src/pty.rs). That was a deliberate pragmatic call, recorded here so it is a decision rather than an accident.The purist alternative: one Job Object per pane —
CreateJobObject+AssignProcessToJobObjectat spawn,TerminateJobObjecton teardown. No exec, no external binary dependency, atomic tree kill.Why it is not a drop-in
CreateProcessW; the daemon can only assign post-spawn viaChild::as_raw_handle(), and a fast child can spawn a grandchild in that window, escaping the job.ClosePseudoConsole(seePtyChild::close_master) is required regardless; a job would not replace the whole teardown.AssignProcessToJobObjectfails when the child is already in a non-nestable job (CI agents, Windows Terminal, EDR sandboxes run processes under jobs). Windows 8+ nests, but the matrix needs a documented stance and test coverage.TerminateProcessis taskkill's own implementation, except in-crate we would own the PID-reuse TOCTOU races. (The root PID is stable either way: the daemon retains the child handle, and Windows does not reuse a PID while a handle to the process object is open.)Also verified during review
win/mod.rsdo_killinverts theTerminateProcesssuccess BOOL (returnsErron success), andWinChild::killswallows it into an unconditionalOk(()). It also never assigns children to a job and does not passCREATE_NEW_PROCESS_GROUP.Possible shape
as_raw_handle()(accepting the small assignment race, or closing it later by taking over the ConPTY spawn the wayspawn_headless_windowsalready owns the headless spawn).TerminateJobObjectinPtyChild::kill/reap_all_panes; conhost still viaClosePseudoConsole.Non-urgent: the taskkill path is hardened (absolute
%SystemRoot%path,CREATE_NO_WINDOW, exit-128 = already-gone).