fix(ccproxy): raise startup timeouts (auth check 10s→30s, serve health 30s→180s) (#328)

* fix(ccproxy): raise auth status check timeout to 30s

ccproxy's CLI initializes its full plugin system on every invocation;
a cold 'ccproxy auth status' takes ~10s wall time on Apple Silicon,
so the 10s subprocess timeout made OAuth startup fail intermittently
with 'Auth check timed out' even when credentials were valid.

* fix(ccproxy): raise serve health deadline to 120s

ccproxy boot includes plugin init plus Codex CLI detection; measured
~76s to first healthy response on an Apple Silicon Mac (ccproxy-api
0.2.9). The 30s deadline in start_ccproxy() killed the process before
it could come up, failing OAuth startup with 'ccproxy did not become
healthy within 30 seconds'.

* fix(ccproxy): widen serve health deadline to 180s

Full startup measured at ~111s on a second cold run (Apple Silicon,
ccproxy-api 0.2.9); 120s left too little headroom for boot variance.

* fix(ccproxy): centralize startup timeouts

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
This commit is contained in:
Mani Saint-Victor
2026-07-13 06:36:05 -04:00
committed by GitHub
parent f3e65a446f
commit 19888c2db6
2 changed files with 36 additions and 7 deletions
+20 -5
View File
@@ -20,6 +20,9 @@ from EvoScientist.config import EvoScientistConfig
logger = logging.getLogger(__name__)
_CCPROXY_AUTH_TIMEOUT_SECONDS = 30
_CCPROXY_HEALTH_TIMEOUT_SECONDS = 180
# =============================================================================
# Availability & auth checks
@@ -127,7 +130,11 @@ def check_ccproxy_auth(provider: str = "claude_api") -> tuple[bool, str]:
[exe, "auth", "status", provider],
capture_output=True,
text=True,
timeout=10,
# ccproxy's CLI initializes its full plugin system on every
# invocation — a cold start takes ~10s on Apple Silicon, so a
# 10s timeout made OAuth startup fail intermittently with
# "Auth check timed out".
timeout=_CCPROXY_AUTH_TIMEOUT_SECONDS,
)
import re as _re
@@ -186,18 +193,23 @@ def start_ccproxy(port: int) -> subprocess.Popen:
The Popen handle for the ccproxy process.
Raises:
RuntimeError: If ccproxy fails to become healthy within 30 seconds.
RuntimeError: If ccproxy fails to become healthy within
``_CCPROXY_HEALTH_TIMEOUT_SECONDS``.
FileNotFoundError: If ccproxy binary is not found.
"""
exe = _ccproxy_exe() or "ccproxy"
logger.warning(
"Starting ccproxy on port %d; first startup may take up to %d seconds",
port,
_CCPROXY_HEALTH_TIMEOUT_SECONDS,
)
proc = subprocess.Popen(
[exe, "serve", "--port", str(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Wait for health (ccproxy can take up to ~11s on first start)
deadline = time.monotonic() + 30
deadline = time.monotonic() + _CCPROXY_HEALTH_TIMEOUT_SECONDS
while time.monotonic() < deadline:
if proc.poll() is not None:
raise RuntimeError(
@@ -213,7 +225,10 @@ def start_ccproxy(port: int) -> subprocess.Popen:
proc.wait(timeout=3)
except subprocess.TimeoutExpired:
proc.kill()
raise RuntimeError("ccproxy did not become healthy within 30 seconds")
raise RuntimeError(
"ccproxy did not become healthy within "
f"{_CCPROXY_HEALTH_TIMEOUT_SECONDS} seconds"
)
def stop_ccproxy(proc: subprocess.Popen | None) -> None:
+16 -2
View File
@@ -6,6 +6,8 @@ from unittest.mock import MagicMock, patch
import pytest
from EvoScientist.ccproxy_manager import (
_CCPROXY_AUTH_TIMEOUT_SECONDS,
_CCPROXY_HEALTH_TIMEOUT_SECONDS,
check_ccproxy_auth,
ensure_ccproxy,
is_ccproxy_available,
@@ -52,6 +54,8 @@ class TestCheckCcproxyAuth:
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
assert cmd[1:] == ["auth", "status", "claude_api"]
# ccproxy CLI cold start takes ~10s; timeout must leave headroom
assert mock_run.call_args[1]["timeout"] == _CCPROXY_AUTH_TIMEOUT_SECONDS
@patch("subprocess.run")
def test_valid_auth_codex(self, mock_run):
@@ -123,9 +127,10 @@ class TestIsCcproxyRunning:
class TestStartCcproxy:
@patch("EvoScientist.ccproxy_manager.logger.warning")
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running")
@patch("subprocess.Popen")
def test_success(self, mock_popen, mock_running):
def test_success(self, mock_popen, mock_running, mock_warning):
proc = MagicMock()
proc.poll.return_value = None
mock_popen.return_value = proc
@@ -134,6 +139,11 @@ class TestStartCcproxy:
result = start_ccproxy(8000)
assert result is proc
mock_warning.assert_called_once_with(
"Starting ccproxy on port %d; first startup may take up to %d seconds",
8000,
_CCPROXY_HEALTH_TIMEOUT_SECONDS,
)
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running", return_value=False)
@patch("EvoScientist.ccproxy_manager.time")
@@ -143,7 +153,11 @@ class TestStartCcproxy:
proc.poll.return_value = None
mock_popen.return_value = proc
# Simulate time passing beyond deadline
mock_time.monotonic.side_effect = [0, 0, 31]
mock_time.monotonic.side_effect = [
0,
0,
_CCPROXY_HEALTH_TIMEOUT_SECONDS + 1,
]
mock_time.sleep = MagicMock()
with pytest.raises(RuntimeError, match="did not become healthy"):