fix: exit process when ACP connection closes (#530)

## Summary

- Return `AgentSideConnection` from `runAcp()` instead of discarding it
- Listen for `connection.closed` and call `process.exit(0)` on
resolution
- Fixes orphan process accumulation in oneshot mode (e.g. via ACPX)

## Problem

When used in oneshot mode, `claude-agent-acp` does not exit after the
client closes the stdin pipe. `process.stdin.resume()` keeps the Node.js
event loop alive indefinitely. Over time, orphan processes accumulate on
the host (~97MB RSS each), eventually exhausting system resources.

## Root cause

`runAcp()` creates an `AgentSideConnection` but discards the reference.
The `Connection.#receive()` loop detects stdin EOF and fires
`abortController.abort()`, resolving `connection.closed` — but no code
listens for it to trigger `process.exit()`.

## Fix

1. **`acp-agent.ts`**: `runAcp()` now returns the `AgentSideConnection`
instance
2. **`index.ts`**: Awaits `connection.closed` and calls
`process.exit(0)`

## Impact

- **Oneshot sessions**: Process now exits cleanly when the client closes
stdin (EOF)
- **Persistent sessions**: Unaffected — the client keeps stdin open, so
`connection.closed` does not fire until explicit disconnect

## Test plan

- [x] Verified oneshot: process exits within 1s after stdin EOF
- [x] Verified persistent: process remains alive while stdin is open
- [x] Measured idle RSS: ~97MB → freed on exit
- [x] Tested with ACPX runtime (OpenClaw gateway) in production

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: WayChan <dove.h14@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
Sway Chan
2026-04-14 00:40:45 +08:00
committed by GitHub
parent de37577eaf
commit 5c81e99fe5
2 changed files with 23 additions and 3 deletions

View File

@@ -991,6 +991,11 @@ export class ClaudeAcpAgent implements Agent {
delete this.sessions[sessionId];
}
/** Tear down all active sessions. Called when the ACP connection closes. */
async dispose(): Promise<void> {
await Promise.all(Object.keys(this.sessions).map((id) => this.teardownSession(id)));
}
async unstable_closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
if (!this.sessions[params.sessionId]) {
throw new Error("Session not found");
@@ -2285,7 +2290,12 @@ export function runAcp() {
const output = nodeToWebReadable(process.stdin);
const stream = ndJsonStream(input, output);
new AgentSideConnection((client) => new ClaudeAcpAgent(client), stream);
let agent!: ClaudeAcpAgent;
const connection = new AgentSideConnection((client) => {
agent = new ClaudeAcpAgent(client);
return agent;
}, stream);
return { connection, agent };
}
function commonPrefixLength(a: string, b: string) {