OpenHands / OpenHands/software-agent-sdk
ACP subprocess leaf process orphaned on shutdown (POSIX) — process-group teardown gap, separate from #4409's Windows fix
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
Summary
On POSIX (Linux), ACPAgent._shutdown_runtime's process.terminate()/.kill() (a single-PID os.kill, since _process is an asyncio.subprocess.Process with no process-group handle) reliably kills the top 1-2 levels of the spawned ACP command chain but leaves deeper descendants running indefinitely as orphans, reparented to init.
This is the same root cause #4409 addresses for Windows (no process-group equivalent for a bare PID kill), but #4409's fix (a Win32 Job Object) is Windows-only by design and does not help here — this is filed separately because the correct mechanism on POSIX is different (process groups + killpg, not a Job Object).
Repro
Command shape: ["npx", "-y", "@agentclientprotocol/claude-agent-acp@<version>"], spawned via _start_acp_server's asyncio.create_subprocess_exec (no start_new_session=True/preexec_fn=os.setsid), so the whole resulting tree shares the parent agent-server's own PGID/SID.
Resulting live tree (ps -eo pid,ppid,pgid,sid,cmd), all sharing one PGID/SID:
agent-server (python)
└─ npm exec @agentclientprotocol/claude-agent-acp <- process.pid (the only PID ever captured)
└─ sh -c claude-agent-acp
└─ node .../claude-agent-acp <- survives
└─ claude ... <- survives
When the conversation is torn down (idle-eviction or explicit delete → ACPAgent.close() → _shutdown_runtime()), process.terminate() targets only the top PID. npm's own signal handling tears down its immediate child (sh -c) reliably, but the grandchild node process (and its own claude child) is never referenced anywhere in _shutdown_runtime/ACPAgent/the caller — nothing waits on it, signals it, or enumerates it. It's simply left running once its immediate parent exits.
Note: unlike the assumption in #4409's description ("On POSIX this is mostly masked, because children die on the broken stdio pipe when the parent goes"), that did not hold in this repro — the node leaf keeps running indefinitely (confirmed alive for hours across many real conversations in a production deployment), because the pipe's write end is held by the asyncio parent (the agent-server process itself), not by the immediately-killed child, so the leaf's inherited fd is never actually broken.
Proposed fix (POSIX-specific, small)
- At the spawn site (
_start_acp_server'screate_subprocess_execcall), addstart_new_session=Trueso the whole spawned tree gets its own process group. - At the two teardown sites in
_shutdown_runtime(currentlyprocess.terminate()/process.kill()), target the process group instead of the bare PID:os.killpg(os.getpgid(process.pid), signal.SIGTERM)(falling back toSIGKILLthe same way the existing code already falls back), guarded forProcessLookupError/PermissionErrorin case the group is already gone.
Happy to open a PR for this if useful — wanted to file the issue first since #4409 already exists for the Windows side of the same underlying gap and I didn't want to duplicate that discussion, just note the POSIX side needs a different, separate fix.
Environment
openhands-sdk1.44.0 and 1.45.0 (confirmed identical in the relevant code — the gap is present in both; checked viacreate_subprocess_exec/process.terminate()call sites, nostart_new_session/killpg/process-group handling anywhere inacp_agent.pyin either version).- Linux (WSL2),
agent-server --import-modules canvas_ui_tool.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in acp_agent.py at _start_acp_server's asyncio.create_subprocess_exec call and follow _shutdown_runtime's two process termination paths. Verify the POSIX process-group behavior and existing fallback handling, then confirm that closing an ACPAgent terminates the spawned descendants rather than leaving the node and claude processes alive.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100