diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc5e9a..2dcb8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v3.11.1 — 2026-04-21 + +### Fixes +- Concurrency slot leak on subprocess timeout (#37). The request-timeout handler called `proc.kill("SIGTERM")` without decrementing `stats.activeRequests`. A subprocess stuck in a syscall that ignored SIGTERM would hold its slot until (or beyond) the 5s SIGKILL escalation actually reaped it. Slot release is now wired to `proc.once("exit", cleanup)` so every termination path — normal close, error, SIGTERM, SIGKILL — releases the slot exactly once. + ## v3.11.0 — 2026-04-20 ### Features diff --git a/package.json b/package.json index f3b6514..581c504 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openclaw-claude-proxy", - "version": "3.11.0", + "version": "3.11.1", "description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.", "type": "module", "bin": { diff --git a/server.mjs b/server.mjs index 60f52ae..83fc913 100644 --- a/server.mjs +++ b/server.mjs @@ -444,6 +444,15 @@ function spawnClaudeProcess(model, messages, conversationId) { stats.activeRequests--; } + // Guarantee slot release on ANY exit path (normal close, error, timeout kill, + // SIGKILL escalation). The 'exit' event fires before 'close' and runs even + // if stdio pipes stay open. Fixes #37: the timeout path called + // proc.kill('SIGTERM') without decrementing the concurrency counter, so a + // stuck subprocess that ignored SIGTERM could leak its slot until (or + // beyond) the SIGKILL escalation actually reaped it. cleanup() is idempotent + // so this listener is safe alongside the existing 'close'/'error' paths. + proc.once("exit", cleanup); + function handleSessionFailure() { if (sessionInfo?.resume && conversationId) { console.warn(`[session] resume failed for ${conversationId.slice(0, 12)}..., removing stale session`);