Skip to content

fix(acp_client): on macOS and Windows an ACP agent outlives a host that dies without closing it #928

Closed

Description

@LivXue

Summary

An ACP agent process is ended only by AcpClient.close(). When the Raven process that launched it exits without closing the connection pool -- killed, crashed, or ended by the OOM killer -- the agent keeps running with no host left to talk to, and keeps its memory until it is killed by hand.

A Linux-only change is in preparation: a parent-death signal on the launched process, and a sweep at the next ACP launch that ends the process groups recorded by hosts that are no longer running. This issue tracks macOS and Windows, which that change does not cover.

Evidence

Observed on a Linux host on 2026-10-12: two third-party ACP agents (codebuddy --acp, qodercli --acp) started by the connection test (ping_agent) on 2026-09-20 were still running 22 days later, reparented to init, with 523 MiB PSS combined. Before #554 the connection test held its connection in the host's shared pool, and the host's exit did not close it.

Reproduced on Linux at main (182daca) with a stand-in host that calls AcpClient.launch and a stand-in agent that does not exit on stdin EOF:

1. host:  await AcpClient.launch(name="stub", command="python agent.py")
2. shell: kill -9 <host pid>
3. agent: still running, ppid 1, pgid == sid == its own pid

Root cause

  • raven/acp_client/client.py starts every agent with start_new_session=True, so the agent receives none of the host's signals, and ends it only in close() (os.killpg(pgid, SIGKILL)).
  • Hosts close the pool on their teardown paths (close_pool() in the gateway, TUI and serve teardowns). A host that dies before reaching them leaves every live ACP agent running.
  • An agent that exits on stdin EOF still ends when the host's pipe closes; one that does not, stays.

Platform notes

  • macOS: there is no PR_SET_PDEATHSIG, so the Linux parent-death signal does not apply, and a sweep needs a process start time from somewhere other than /proc (for example sysctl with KERN_PROC_PID) to tell a recorded group leader from a reused PID.
  • Windows: start_new_session is ignored and there is no os.killpg. Closing a connection cleanly is addressed in fix: restore bundled ACP agent startup on Windows #908 (taskkill /T /F while the launcher is alive). For a host that exits without closing, the platform mechanism is a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE assigned at launch: every process in the job is terminated when the last handle to the job closes, which includes the owning process exiting.

Expected behavior

An ACP agent does not outlive the Raven process that launched it, whether that process exits cleanly or not, on every supported platform.

Environment

Code path; affects macOS and Windows. The observation and reproduction above are from Linux, Python 3.13, main at 182daca.

Activity

  1. akushonkamen commented on Oct 13, 2026

    @akushonkamen
    Contributor

    Confirmed the root cause: AcpClient.launch() at raven/acp_client/client.py:261 starts the agent with start_new_session=True, and the only teardown path is close() (client.py:301-315) which calls os.killpg(..., SIGKILL) — so any host death that bypasses close_pool() (kill -9, OOM, crash) orphans the agent. I'm working on a macOS/Windows orphan sweep (record pgid + owner pid + process start time at launch, sweep stale agents before the next launch; Job Object on Windows). Disclosure: this work is assisted by an AI coding agent.

  2. akushonkamen commented on Oct 13, 2026

    @akushonkamen
    Contributor

    Confirmed the root cause at main (182daca): raven/acp_client/client.py:261 launches every agent with start_new_session=True, so the agent receives none of the host's signals, and the only kill path is os.killpg in close() (client.py:315); close_pool() (raven/acp_client/pool.py:615) is only reached on graceful teardown, so a host that is killed or crashes leaves orphaned agents — matching the 22-day, 523 MiB observation. Working on the macOS half (a start-time-guarded sweep via sysctl KERN_PROC_PID at the next launch); Windows (Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) will be included as far as CI allows. Assistance from Claude (AI) acknowledged per repo convention.

  3. akushonkamen commented on Oct 13, 2026

    @akushonkamen
    Contributor

    PR opened: #930 (includes the test evidence and the Windows untested declaration). Claim + AI disclosure in #928 (comment)

  4. akushonkamen commented on Oct 13, 2026

    @akushonkamen
    Contributor

    已修复并通过独立验收:修复分支推为 PR,包含根因分析、按进程启动时间判定的孤儿清扫与 Darwin 红绿端到端证据,详见 #931

  5. akushonkamen commented on Oct 13, 2026

    @akushonkamen
    Contributor

    Opened #932 with a launch-time orphan sweep: every launch records (pgid, owner pid, process start time) into a registry under the raven data directory, and the next launch reaps recorded groups whose owner is dead and whose start time still matches (darwin via libproc PROC_PIDTBSDINFO, linux via /proc//stat field 22, so recycled pids are not killed). Record/sweep are best-effort and never block launch. Windows is not implemented — the Job Object design is in the module docstring. Verified red/green on darwin arm64: a kill -9'd host leaves the agent reparented to init without the fix; with the fix, hostB's next launch kills the orphaned group.

  6. LivXue commented on Oct 15, 2026

    @LivXue
    MemberAuthor

    Resolved on main by #951 (Windows Job Object lifetime management) and #953 (macOS host-exit watcher and leftover-agent sweep). Both pull requests were merged on 2026-10-15 and include verification of agent cleanup after abrupt host termination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions