From cc2f46344398225df1e6149585e16ebb131ae422 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 3 Aug 2026 16:44:18 -0700 Subject: [PATCH] [eric] router: adopt a healthy 9Router instead of evicting the one other backends are using --- backend/apps/nine_router/process.py | 14 +++++++++++- backend/tests/test_router_watchdog.py | 31 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/backend/apps/nine_router/process.py b/backend/apps/nine_router/process.py index 5bbdfcad..2dc78420 100644 --- a/backend/apps/nine_router/process.py +++ b/backend/apps/nine_router/process.py @@ -497,7 +497,19 @@ async def p_ensure_running_impl(): p_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" if is_running(): - # In dev mode, kill stale standalone servers (from previous builds) so we can start `next dev` which always uses latest source code + # ADOPT a healthy router instead of evicting it. is_running() has just confirmed HTTP 200 on + # /v1/models, so whatever is on the port is serving; the only thing killing it establishes is + # who owns the process. That eviction is a thrash engine on a shared machine: every dev + # backend that boots kills the router the others are using, their watchdogs revive it, and + # the next boot kills it again. Measured with 8 backends on one box: a coverage sweep scored + # 1/9 with zero code change, wall times went 30s -> 202s, and one run logged 0 kills of its + # own against 4 revives. The original reason to kill ("stale build") does not apply to a + # component we pin by npm version rather than build from source. OPENSWARM_ROUTER_TAKEOVER=1 + # restores the old behaviour for the rare case someone must force a fresh spawn. + if not p_is_packaged and os.environ.get("OPENSWARM_ROUTER_TAKEOVER") != "1": + logger.info("9Router already healthy on port %d; adopting it (set " + "OPENSWARM_ROUTER_TAKEOVER=1 to force a fresh spawn)", NINE_ROUTER_PORT) + return if not p_is_packaged: # But never kill the instance WE already started: a second ensure call (another sub-app's lifespan races settings') would pkill our fresh next-server, leaving a dead window the boot key-sync fails into, so the cp-openai node never registers and gpt-5.* own-key dies. if p_process is not None and p_process.poll() is None: diff --git a/backend/tests/test_router_watchdog.py b/backend/tests/test_router_watchdog.py index 4bbb4964..e1d1fd7a 100644 --- a/backend/tests/test_router_watchdog.py +++ b/backend/tests/test_router_watchdog.py @@ -204,3 +204,34 @@ def test_detection_revival_gated_on_evidence(): assert ensures, "sub-only users must get a revival attempt" asyncio.run(run()) + + +def test_a_healthy_router_is_adopted_not_evicted(): + """The dev-machine thrash engine, removed. + + Every dev backend used to kill any 9Router it had not personally spawned, on a "stale build" + rationale that does not apply to a component we pin by npm version instead of building from + source. On a shared box that is a loop: each boot evicts the router the others are using, their + watchdogs revive it, the next boot evicts it again. Measured with 8 backends on one machine, a + coverage sweep scored 1/9 with zero code change, wall times went 30s -> 202s, and one backend + logged 0 kills of its own against 4 revives. + + is_running() has already confirmed HTTP 200 on /v1/models before this point, so the router is + serving by definition; killing it settles ownership and nothing else. + """ + killed = [] + with patch.object(proc, "is_running", return_value=True), \ + patch.object(proc, "p_process", None), \ + patch("subprocess.run", side_effect=lambda *a, **k: killed.append(a)), \ + patch.dict("os.environ", {"OPENSWARM_PACKAGED": "0"}, clear=False): + asyncio.run(proc.p_ensure_running_impl()) + assert killed == [], "a healthy router another process owns must be adopted, never pkill'd" + + +def test_takeover_escape_hatch_still_exists(): + """Adoption must not become a trap: someone genuinely needing a fresh spawn keeps a way to get + one, and it is explicit rather than the silent default.""" + import inspect + src = inspect.getsource(proc.p_ensure_running_impl) + assert "OPENSWARM_ROUTER_TAKEOVER" in src + assert 'os.environ.get("OPENSWARM_ROUTER_TAKEOVER") != "1"' in src