From cdd6b41261f9bf29d0d372f631e0adababa979c0 Mon Sep 17 00:00:00 2001 From: dtzp555-max Date: Tue, 21 Apr 2026 20:21:36 +1000 Subject: [PATCH] fix(concurrency): release slot on subprocess SIGTERM failure (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per forensic analysis in #37, the timeout handler at server.mjs:471 called `proc.kill('SIGTERM')` without decrementing the concurrency counter (`stats.activeRequests`). If the subprocess was stuck in a syscall (e.g. MCP I/O) and ignored SIGTERM, its slot was not freed until — and only if — the `proc.on('close')` handler eventually fired after the 5s SIGKILL escalation successfully reaped the child. Real-world observation (#37) shows 3 stuck claude subprocesses running 20 minutes to 2h 45m, exhausting the 8-slot pool and returning `500 concurrency limit reached (8/8)` on every subsequent request. Fix: attach `cleanup` to `proc.once('exit', ...)`. The 'exit' event fires before 'close' and runs on every termination path — normal completion, error, SIGTERM, SIGKILL, crash — even if stdio pipes stay open. `cleanup()` is already idempotent (guarded by `cleaned` flag), so this listener is safe alongside the existing 'close' and 'error' paths that also call it. ALIGNMENT: cli.js has no equivalent subprocess-pool / concurrency- limit logic — cli.js is a single-user CLI, the process being pooled, not the pool itself. Per ALIGNMENT.md Rule 2, this fix is proxy- internal (subprocess lifecycle management) with no cli.js equivalent to cite; documented here instead. No wire-protocol, HTTP surface, or response shape change (Rule 3 preserved). Release kit: bumps version to 3.11.1 and adds CHANGELOG entry so the auto-release workflow tags v3.11.1 on merge. README version refs are feature-gate markers for v3.11.0 (models.json SPOT / auto-sync) and remain historically accurate — no README change needed. Fixes #37. Co-authored-by: dtzp555 --- CHANGELOG.md | 5 +++++ package.json | 2 +- server.mjs | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) 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`);