feat: add adopt API for externally spawned child processes - #98
Conversation
|
Hey @masonc15 ! Thanks for this PR! Note that I'm moving some stuff around (see the open prs/stack) and I think this will get some conflicts. Could you explain in your words + with some real time examples how this would work? I'm not yet sure of the use case where this would be needed, in most of my use, Pi uses either bash or processes and rarely needs to have its process adopted? However, something I'm investigating is doing something like Codex and Claude Code (see #56 and #81) where it's a unified tool that always runs the process in the bg (codex) or an override on the Bash tool that optionally can be started in the background (Claude). Would either match what you have in mind? |
|
Hi @aliou, sorry if this created any noise for you - I was brainstorming a sort of extension of pi-processes that allowed for similar behavior to Claude Code where you can press ctrl-b to background any currently running job. I had left it in draft mode to sort of iterate on, sorry if it's in a bit of a "slop"py state as a PR. Even with pi-processes, I've run into a bunch of cases using pi where the first thing it does is a grep, find, mdfind, or something along those lines over a large root that blocks pi and takes over 200 seconds sometimes when the task itself should be finished much quicker. My ideal use case for this would be noticing a job like that, being able to press a key to have pi-processes adopt it, and then pi being able to continue and not be blocked. I think the Claude-style Bash override mentioned in #81 would probably cover this, but I would want to make sure it handles the case where a command is unpredictably slow and can be backgrounded. Having a I validated this branch with a small bash override extension I built. Press Looking at #81, adopt shouldn't be needed for that path since the manager should be aware of every command/process already. I'm happy to close this and contribute promotion logic to #81 if that would make more sense. Or if this adopt primitive still makes sense, seeing the conflicts with your #88 stack, I can wait until that lands and rebase after that. Just let me know, thanks! |
no worries, not at all!
I should have everything shipped this weekend, i'll ping you and you can rebase, and i'll play with your branch a bit to see how it behaves day to day :) Could you also share your bash tool override so I can see the full "workflow"? Thanks! |
|
@masonc15 Merged the full stack so feel free to go for it! |
Motivation
Extensions that run foreground child processes (most notably bash-tool overrides that let a user move a slow command to the background instead of killing it, Claude Code ctrl+b style) need somewhere to put the still-running child. pi-processes already has everything such a process needs — log capture, liveness watching, kill/stop, notifications, dock and
/psvisibility — butProcessRuntimeController.start()is the only entry point, and it insists on spawning the child itself.This PR adds an adopt path so an already-running child can be handed over to the manager.
What's included
ProcessManager.adopt(name, command, cwd, child, opts?)— registers an externally spawnedChildProcessas a managed process.start()is refactored into spawn + sharedregister(); adopted and started processes go through identical registration, stdio wiring, and lifecycle handling.opts.initialOutput— output the adopter captured before handover, appended to the logs ahead of any future stdio so nothing is lost across the transition.opts.startTime— backdates the record to when the command actually began.processes:command:adopt(CommandAdoptPayload/CommandAdoptResult) so other extensions can adopt cross-extension. Payloads cross the bus by reference in-process, so the liveChildProcesshandle arrives intact. The handler registers default notification config, matching started processes.closeevent fired before adoption (streams destroyed), the record is finalized immediately with the correct exit classification; if the child exited but streams are still open, the pendingclosefinalizes it through the normal path.Contract
The child must be spawned the way
spawnCommandspawns: detached process group, piped stdio. This keeps group kill (killProcessGroup) and liveness polling (isProcessGroupAlive) working identically for adopted processes. Documented on the payload type and both adopt methods.Testing
src/manager/index.test.ts(running adoption, backdated startTime,process_startedemission, initialOutput ordering, exit detection, both already-exited variants, kill, missing-pid).tests/e2e/adopt.e2e.tswith real detached children: output captured across handover (pre-handover marker viainitialOutput, post-handover marker via wired stdio), and process-group kill of an adopted child.pnpm typecheck,pnpm lint,pnpm test(603),pnpm test:e2e(12) all green.processes:command:adopt: user-triggered and timeout-triggered backgrounding both landed in the dock//ps, output was preserved across the handover, and exit notifications reached the agent.Notes