fix(agent): launch agent uv-run subprocesses with --no-sync

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
This commit is contained in:
Renn F
2026-06-15 22:43:05 +02:00
parent f443a60d29
commit 25aed51c04
3 changed files with 29 additions and 9 deletions
+7 -4
View File
@@ -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 # 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 # 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. # 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 # Pin uv to the pre-baked image venv AND pass `--no-sync` on the run below.
# of cwd. (The orchestrator sets the same var in every MCP server's env # Pinning the env location alone is NOT sufficient: `uv run` still discovers
# in the generated mcp-config.json — keep both in sync.) # 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 export UV_PROJECT_ENVIRONMENT=/app/.venv
# --- SDK bring-up --------------------------------------------------------- # --- SDK bring-up ---------------------------------------------------------
if ! curl -sf "http://localhost:${SDK_PORT}/health" >/dev/null 2>&1; then if ! curl -sf "http://localhost:${SDK_PORT}/health" >/dev/null 2>&1; then
echo "[SDK] Starting for agent ${AGENT_ID} on port ${SDK_PORT}..." 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=$! SDK_PID=$!
sleep 2 sleep 2
if curl -sf "http://localhost:${SDK_PORT}/health" >/dev/null 2>&1; then if curl -sf "http://localhost:${SDK_PORT}/health" >/dev/null 2>&1; then
+12 -5
View File
@@ -2015,8 +2015,12 @@ class AgentOrchestrator:
# but on a cold cache (first spawn after an image rebuild) the # but on a cold cache (first spawn after an image rebuild) the
# download takes minutes and the MCP servers never come up # download takes minutes and the MCP servers never come up
# before the agent burns its budget. Pinning the project env # before the agent burns its budget. Pinning the project env
# to the pre-baked venv makes `uv run` reuse it instantly, # to the pre-baked venv is necessary but NOT sufficient: `uv run`
# regardless of cwd. # 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", "UV_PROJECT_ENVIRONMENT": "/app/.venv",
} }
@@ -2031,19 +2035,19 @@ class AgentOrchestrator:
# Intent verbs — every role-scoped lifecycle transition. # Intent verbs — every role-scoped lifecycle transition.
"roboco-flow": { "roboco-flow": {
"command": "uv", "command": "uv",
"args": ["run", "python", "-m", "roboco.mcp.flow_server"], "args": ["run", "--no-sync", "python", "-m", "roboco.mcp.flow_server"],
"env": mcp_env, "env": mcp_env,
}, },
# Content tools — commit, push, PR, journal, notify, message. # Content tools — commit, push, PR, journal, notify, message.
"roboco-do": { "roboco-do": {
"command": "uv", "command": "uv",
"args": ["run", "python", "-m", "roboco.mcp.do_server"], "args": ["run", "--no-sync", "python", "-m", "roboco.mcp.do_server"],
"env": mcp_env, "env": mcp_env,
}, },
# Read-only git views — status, log, diff, branches. # Read-only git views — status, log, diff, branches.
"roboco-git-readonly": { "roboco-git-readonly": {
"command": "uv", "command": "uv",
"args": ["run", "python", "-m", "roboco.mcp.git_readonly"], "args": ["run", "--no-sync", "python", "-m", "roboco.mcp.git_readonly"],
"env": mcp_env, "env": mcp_env,
}, },
# Knowledge base — RAG / semantic search / ask_mentor. # Knowledge base — RAG / semantic search / ask_mentor.
@@ -2051,6 +2055,7 @@ class AgentOrchestrator:
"command": "uv", "command": "uv",
"args": [ "args": [
"run", "run",
"--no-sync",
"python", "python",
"-m", "-m",
"roboco.mcp.optimal_server", "roboco.mcp.optimal_server",
@@ -2075,6 +2080,7 @@ class AgentOrchestrator:
"command": "uv", "command": "uv",
"args": [ "args": [
"run", "run",
"--no-sync",
"python", "python",
"-m", "-m",
"roboco.mcp.docs_server", "roboco.mcp.docs_server",
@@ -2097,6 +2103,7 @@ class AgentOrchestrator:
"command": "uv", "command": "uv",
"args": [ "args": [
"run", "run",
"--no-sync",
"python", "python",
"-m", "-m",
"roboco.mcp.search_server", "roboco.mcp.search_server",
@@ -92,3 +92,13 @@ class TestMcpConfigPinsBakedVenv:
f"/app/.venv — without it `uv run` re-downloads deps into a " f"/app/.venv — without it `uv run` re-downloads deps into a "
f"cwd-relative venv on every spawn (#179). env={spec['env']}" 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']}"
)