From 25aed51c040cc21b2dabea8baf0a35987b8c3551 Mon Sep 17 00:00:00 2001 From: Renn F Date: Mon, 15 Jun 2026 22:29:33 +0200 Subject: [PATCH] fix(agent): launch agent uv-run subprocesses with --no-sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents with a write workspace (developer/product_owner/head_marketing/documenter) run with cwd = their git workspace clone. Claude Code launches each MCP server (flow/do/git-readonly/optimal/docs/search) and the SDK server as `uv run python -m ...` from that cwd. When the clone's uv.lock drifts from the baked image, `uv run` re-resolves and re-syncs /app/.venv against the clone's lock — a multi-minute stall on a cold wheel cache — so the servers never reach "connected": they sit at status="pending" and the agent gets ZERO gateway verbs. It then can't claim/commit/idle (all MCP verbs), its Stop is rejected, and it respawns in a loop redoing work it can't submit. UV_PROJECT_ENVIRONMENT pins the venv location but does NOT stop the cwd-relative resolve/sync (confirmed empirically on uv 0.11.1); `--no-sync` does, so the servers reuse the baked /app/.venv as-is and start instantly. The /app-cwd roles (qa/cell_pm/main_pm/auditor) were unaffected because their env already matches. - orchestrator.py: --no-sync on all 6 generated MCP servers - docker/scripts/sdk-startup-hook.sh: --no-sync on the agent_sdk.server launch - test_spawn_strict_mcp.py: assert every server's args start with run,--no-sync --- docker/scripts/sdk-startup-hook.sh | 11 +++++++---- roboco/runtime/orchestrator.py | 17 ++++++++++++----- tests/unit/runtime/test_spawn_strict_mcp.py | 10 ++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/docker/scripts/sdk-startup-hook.sh b/docker/scripts/sdk-startup-hook.sh index 58b4c12f..3b807d08 100644 --- a/docker/scripts/sdk-startup-hook.sh +++ b/docker/scripts/sdk-startup-hook.sh @@ -18,15 +18,18 @@ PRECOMPACT_FILE="/tmp/roboco-precompact-${AGENT_ID}.md" # set (torch/lancedb/pyarrow/scipy, ~350MB) into a fresh venv. On a cold # uv wheel cache (first spawn after an image rebuild) that download takes # minutes and the SDK/MCP layer never comes up before the agent reaps. -# Pin uv to the pre-baked image venv so it starts instantly regardless -# of cwd. (The orchestrator sets the same var in every MCP server's env -# in the generated mcp-config.json — keep both in sync.) +# Pin uv to the pre-baked image venv AND pass `--no-sync` on the run below. +# Pinning the env location alone is NOT sufficient: `uv run` still discovers +# the cwd project and re-syncs the pinned venv against the clone's (drifted) +# lock — that resync is the actual multi-minute stall. `--no-sync` skips it. +# (The orchestrator passes the same var + --no-sync to every MCP server in +# the generated mcp-config.json — keep both in sync.) export UV_PROJECT_ENVIRONMENT=/app/.venv # --- SDK bring-up --------------------------------------------------------- if ! curl -sf "http://localhost:${SDK_PORT}/health" >/dev/null 2>&1; then echo "[SDK] Starting for agent ${AGENT_ID} on port ${SDK_PORT}..." - nohup uv run python -m roboco.agent_sdk.server > "$LOG_FILE" 2>&1 & + nohup uv run --no-sync python -m roboco.agent_sdk.server > "$LOG_FILE" 2>&1 & SDK_PID=$! sleep 2 if curl -sf "http://localhost:${SDK_PORT}/health" >/dev/null 2>&1; then diff --git a/roboco/runtime/orchestrator.py b/roboco/runtime/orchestrator.py index 0eb3fe9a..359502cd 100644 --- a/roboco/runtime/orchestrator.py +++ b/roboco/runtime/orchestrator.py @@ -2015,8 +2015,12 @@ class AgentOrchestrator: # but on a cold cache (first spawn after an image rebuild) the # download takes minutes and the MCP servers never come up # before the agent burns its budget. Pinning the project env - # to the pre-baked venv makes `uv run` reuse it instantly, - # regardless of cwd. + # to the pre-baked venv is necessary but NOT sufficient: `uv run` + # still resolves the project from the workspace cwd and re-syncs + # when the clone's uv.lock drifts from the image — leaving the MCP + # servers stuck at status="pending" so the agent gets zero gateway + # verbs. Each server is therefore launched with `uv run --no-sync` + # (below) to use /app/.venv as-is and start instantly. "UV_PROJECT_ENVIRONMENT": "/app/.venv", } @@ -2031,19 +2035,19 @@ class AgentOrchestrator: # Intent verbs — every role-scoped lifecycle transition. "roboco-flow": { "command": "uv", - "args": ["run", "python", "-m", "roboco.mcp.flow_server"], + "args": ["run", "--no-sync", "python", "-m", "roboco.mcp.flow_server"], "env": mcp_env, }, # Content tools — commit, push, PR, journal, notify, message. "roboco-do": { "command": "uv", - "args": ["run", "python", "-m", "roboco.mcp.do_server"], + "args": ["run", "--no-sync", "python", "-m", "roboco.mcp.do_server"], "env": mcp_env, }, # Read-only git views — status, log, diff, branches. "roboco-git-readonly": { "command": "uv", - "args": ["run", "python", "-m", "roboco.mcp.git_readonly"], + "args": ["run", "--no-sync", "python", "-m", "roboco.mcp.git_readonly"], "env": mcp_env, }, # Knowledge base — RAG / semantic search / ask_mentor. @@ -2051,6 +2055,7 @@ class AgentOrchestrator: "command": "uv", "args": [ "run", + "--no-sync", "python", "-m", "roboco.mcp.optimal_server", @@ -2075,6 +2080,7 @@ class AgentOrchestrator: "command": "uv", "args": [ "run", + "--no-sync", "python", "-m", "roboco.mcp.docs_server", @@ -2097,6 +2103,7 @@ class AgentOrchestrator: "command": "uv", "args": [ "run", + "--no-sync", "python", "-m", "roboco.mcp.search_server", diff --git a/tests/unit/runtime/test_spawn_strict_mcp.py b/tests/unit/runtime/test_spawn_strict_mcp.py index aee8cb3a..75f76b77 100644 --- a/tests/unit/runtime/test_spawn_strict_mcp.py +++ b/tests/unit/runtime/test_spawn_strict_mcp.py @@ -92,3 +92,13 @@ class TestMcpConfigPinsBakedVenv: f"/app/.venv — without it `uv run` re-downloads deps into a " f"cwd-relative venv on every spawn (#179). env={spec['env']}" ) + # Pinning the env location is necessary but NOT sufficient: from a + # workspace-clone cwd `uv run` still discovers the clone project and + # re-syncs the pinned venv against its drifted lock, which stalls and + # leaves the server stuck at status="pending" (zero gateway verbs). + # `--no-sync` skips that resync — it must come right after `run`. + assert spec["args"][:2] == ["run", "--no-sync"], ( + f"MCP server {name!r} must launch with `uv run --no-sync ...` so " + f"a drifted workspace-clone lock can't trigger a resync stall; " + f"got args={spec['args']}" + )