diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 3abceac..a6be9d3 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -958,6 +958,11 @@ export class ClaudeAcpAgent implements Agent { delete this.sessions[sessionId]; } + /** Tear down all active sessions. Called when the ACP connection closes. */ + async dispose(): Promise { + await Promise.all(Object.keys(this.sessions).map((id) => this.teardownSession(id))); + } + async unstable_closeSession(params: CloseSessionRequest): Promise { if (!this.sessions[params.sessionId]) { throw new Error("Session not found"); @@ -2239,7 +2244,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) { diff --git a/src/index.ts b/src/index.ts index d45d290..88efcdf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,8 +24,18 @@ if (process.argv.includes("--cli")) { console.error("Unhandled Rejection at:", promise, "reason:", reason); }); - runAcp(); + const { connection, agent } = runAcp(); - // Keep process alive + // Exit cleanly when the ACP connection closes (e.g. stdin EOF, transport + // error). Without this, `process.stdin.resume()` keeps the event loop + // alive indefinitely, causing orphan process accumulation in oneshot mode. + connection.closed.then(async () => { + await agent.dispose().catch((err) => { + console.error("Error during cleanup:", err); + }); + process.exit(0); + }); + + // Keep process alive while connection is open process.stdin.resume(); }