diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index e0629d270d902..350aa106a805f 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -1071,7 +1071,13 @@ class APIServerAdapter(BasePlatformAdapter): active_api_runs = sum( 1 for status in self._run_statuses.values() - if status.get("status") in {"queued", "running", "waiting_for_approval"} + # "stopping" (set by _handle_stop_run) is not terminal: the run + # stays in this state, doing real executor-thread work, until the + # agent actually notices the interrupt and the task settles to + # "cancelled" — an unbounded window, not the old ~5s hard-timeout + # wait. Excluding it here undercounts active_api_runs for the + # whole duration of a cooperative stop. + if status.get("status") in {"queued", "running", "waiting_for_approval", "stopping"} ) process_depth = 0 active_delegations = 0 diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index 7700cec107d02..adb1ee4371ea2 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -926,6 +926,26 @@ class TestHealthDetailedEndpoint: patch("tools.async_delegation.active_count", return_value=2): assert adapter._readiness_work_counts() == (3, 4, 2) + def test_readiness_work_counts_include_stopping_runs(self, adapter): + """Regression: _handle_stop_run() sets status="stopping" and holds it + there — cooperatively, with no hard timeout — until the agent notices + the interrupt and the task actually exits. A run in that window is + still doing real executor-thread work and must count as active, + the same as "running"; excluding it undercounts active_api_runs for + the whole (now-unbounded) cooperative-stop duration.""" + adapter._run_statuses = { + "queued": {"status": "queued"}, + "running": {"status": "running"}, + "approval": {"status": "waiting_for_approval"}, + "stopping": {"status": "stopping"}, + "done": {"status": "completed"}, + "cancelled": {"status": "cancelled"}, + } + + with patch("tools.process_registry.process_registry.completion_queue.qsize", return_value=0), \ + patch("tools.async_delegation.active_count", return_value=0): + assert adapter._readiness_work_counts() == (4, 0, 0) + # --------------------------------------------------------------------------- # /v1/models endpoint