Skip to content

feat: add adopt API for externally spawned child processes - #98

Merged
aliou merged 3 commits into
aliou:mainfrom
masonc15:feat/adopt-external-children
Aug 27, 2026
Merged

feat: add adopt API for externally spawned child processes#98
aliou merged 3 commits into
aliou:mainfrom
masonc15:feat/adopt-external-children

Conversation

@masonc15

@masonc15 masonc15 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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 /ps visibility — but ProcessRuntimeController.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 spawned ChildProcess as a managed process. start() is refactored into spawn + shared register(); 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.
  • New event-bus channel processes:command:adopt (CommandAdoptPayload / CommandAdoptResult) so other extensions can adopt cross-extension. Payloads cross the bus by reference in-process, so the live ChildProcess handle arrives intact. The handler registers default notification config, matching started processes.
  • Already-exited edge case: if the child's close event 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 pending close finalizes it through the normal path.

Contract

The child must be spawned the way spawnCommand spawns: 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

  • 10 focused unit tests: 9 in src/manager/index.test.ts (running adoption, backdated startTime, process_started emission, initialOutput ordering, exit detection, both already-exited variants, kill, missing-pid) and 1 in extensions/processes/handlers/commands.test.ts for default notification registration.
  • 2 e2e tests in tests/e2e/adopt.e2e.ts with real detached children: output captured across handover (pre-handover marker via initialOutput, post-handover marker via wired stdio), and process-group kill of an adopted child.
  • pnpm typecheck, pnpm lint, pnpm test (650), pnpm test:e2e (12) all green.
  • Validated live in a Pi session with a bash-override extension emitting 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

  • Windows is unaffected (extension already bails on Windows; the manager remains POSIX-only).
  • Marked as draft to discuss the API surface — happy to adjust naming, validation strictness, or move the channel handler elsewhere.

@aliou

aliou commented Aug 19, 2026

Copy link
Copy Markdown
Owner

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?

@masonc15

Copy link
Copy Markdown
Contributor Author

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 background: true flag relies on the slowness being predicted ahead of time, and I've found that having a universal ability to background commands is useful for the unpredictable cases.

I validated this branch with a small bash override extension I built. Press ctrl+shift+b in the middle of a command and the tool call resolves immediately with a message naming the adopted process ID, the process shows up in /ps and the dock (if the user has dock enabled), and the agent gets the normal completion notification later as standard. Output from before resolution comes along with initialOutput.

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!

@aliou

aliou commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Hi @aliou, sorry if this created any noise for you

no worries, not at all!

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!

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!

@aliou

aliou commented Aug 22, 2026

Copy link
Copy Markdown
Owner

@masonc15 Merged the full stack so feel free to go for it!

@masonc15
masonc15 force-pushed the feat/adopt-external-children branch from 6eef545 to ebc09c8 Compare August 24, 2026 16:46
@masonc15

masonc15 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@aliou Rebased onto the newly merged stack! I also added the full Bash tool override example.

It keeps Bash in the foreground, shows the ctrl+shift+b to background action, and then adopts the running child without losing its output. The clip below shows the handoff and the normal completion notification.

pi-backgrounding-demo

@masonc15
masonc15 marked this pull request as ready for review August 24, 2026 16:49
@aliou

aliou commented Aug 25, 2026

Copy link
Copy Markdown
Owner

@masonc15 thanks for this! Taking a deeper look today or tomorrow but looks good to me so far!

378-kaiabot Bot and others added 3 commits August 26, 2026 15:28
Deduplicate the private copy from process-log-store and process-output
into src/utils/buffer.ts. Add clampToTail and unit tests.
Add ProcessManager.adopt() so extensions can hand an already-running
child to the manager. The child must be in a detached process group
with piped stdio. Pre-handover output is split into initialStdout and
initialStderr, each routed to the correct log file and combined.log
stream tag, and clamped to MAX_TAIL_READ_BYTES (tail kept).

The new processes:command:adopt event-bus channel exposes adopt
cross-extension. Adopted processes get the full managed lifecycle:
log capture, liveness watching, kill/stop, notifications, and dock
visibility.

Co-authored-by: Kaiabot <kaiabot@378labs.dev>
Co-authored-by: Aliou Diallo <code@aliou.me>
Example extension that overrides the bash tool to move long-running
commands to pi-processes via adopt. Uses separate OutputBuffer instances
per stream and resolves the shell via getShellConfig instead of
hardcoding /bin/bash (fixes NixOS).

Co-authored-by: Kaiabot <kaiabot@378labs.dev>
Co-authored-by: Aliou Diallo <code@aliou.me>
@kaiabot
kaiabot force-pushed the feat/adopt-external-children branch from ebc09c8 to 53f8923 Compare August 26, 2026 15:56
@kaiabot
kaiabot self-requested a review August 26, 2026 15:57
@378-kaiabot
378-kaiabot Bot requested a review from aliou August 26, 2026 15:59
@aliou

aliou commented Aug 26, 2026

Copy link
Copy Markdown
Owner

@masonc15 had the agents do some changes after some of my testing, just need to redo a last manual pass before releasing this, should be tonight!

Thanks again for this!

@aliou
aliou merged commit c13654e into aliou:main Aug 27, 2026
3 checks passed
@aliou

aliou commented Aug 27, 2026

Copy link
Copy Markdown
Owner

@masonc15 This should be available in a few minutes in v0.12.0!

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