From 455b8fa4d359019994e6ba4fb3f01194805f69f8 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 19 Aug 2026 16:43:38 -0700 Subject: [PATCH] [eric] agents: MCP responses over the CLI's silent output ceiling get shrunk instead of dropped; a 340KB result line was written and never resolved, the lost-delegation class (ENG-327 root) --- .../apps/agents/browser_agent_mcp_server.py | 12 ++++- .../apps/agents/combined_meta_mcp_server.py | 24 +++++++++ .../tests/test_mcp_response_transport_cap.py | 53 +++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 backend/tests/test_mcp_response_transport_cap.py diff --git a/backend/apps/agents/browser_agent_mcp_server.py b/backend/apps/agents/browser_agent_mcp_server.py index 1913d75b..eca770e4 100644 --- a/backend/apps/agents/browser_agent_mcp_server.py +++ b/backend/apps/agents/browser_agent_mcp_server.py @@ -198,7 +198,10 @@ def call_backend(tasks: list[dict]) -> dict: return {"error": str(e)} -MAX_IMAGE_B64_BYTES = 400_000 +# 400KB was over the CLI's silent MCP-result ceiling: a 340,096-byte response line was WRITTEN and +# never resolved (diag-proven 2026-08-19), which is the whole "delegation result died on the stdio +# hop" class. 90KB keeps the line safely under the CLI's ~25K-token output cap with headroom. +MAX_IMAGE_B64_BYTES = 90_000 MAX_SUMMARY_CHARS = 16_000 MAX_ACTION_LOG_ENTRIES = 40 REPORT_DIR = os.environ.get( @@ -263,7 +266,12 @@ def compress_screenshot(b64_png: str) -> tuple[str, str] | None: img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS) buf = BytesIO() img.convert("RGB").save(buf, format="JPEG", quality=45) - return base64.b64encode(buf.getvalue()).decode(), "image/jpeg" + out = base64.b64encode(buf.getvalue()).decode() + if len(out) > MAX_IMAGE_B64_BYTES: + buf2 = BytesIO() + img.resize((640, int(img.height * 640 / img.width)), Image.LANCZOS).convert("RGB").save(buf2, format="JPEG", quality=35) + out = base64.b64encode(buf2.getvalue()).decode() + return out, "image/jpeg" except Exception: return None diff --git a/backend/apps/agents/combined_meta_mcp_server.py b/backend/apps/agents/combined_meta_mcp_server.py index 3dcd8a01..b77a8902 100644 --- a/backend/apps/agents/combined_meta_mcp_server.py +++ b/backend/apps/agents/combined_meta_mcp_server.py @@ -74,11 +74,35 @@ def p_call(mod, tool_name: str, arguments: dict) -> dict: P_STDOUT_LOCK = threading.Lock() +P_MAX_RESPONSE_BYTES = 200_000 + +def p_shrink_oversize(result): + """The CLI silently DROPS an MCP response line past its output ceiling; the call then hangs + until a watchdog shoots this healthy process (diag-proven 2026-08-19: 340KB written, never + resolved, 150s kill). Better an answer without its picture than no answer: strip image blocks + first, then hard-elide the text.""" + content = result.get("content") if isinstance(result, dict) else None + if not isinstance(content, list): + return result + kept = [c for c in content if not (isinstance(c, dict) and c.get("type") == "image")] + if len(kept) < len(content): + kept.append({"type": "text", "text": "[screenshot omitted: full response exceeded the transport limit]"}) + out = dict(result) + out["content"] = kept + if len(json.dumps({"result": out})) > P_MAX_RESPONSE_BYTES: + for c in out["content"]: + if isinstance(c, dict) and isinstance(c.get("text"), str) and len(c["text"]) > 100_000: + c["text"] = c["text"][:80_000] + "\n\n[... elided: response exceeded the transport limit ...]" + return out + + def send_response(id_, result=None, error=None): msg = {"jsonrpc": "2.0", "id": id_} if error is not None: msg["error"] = error else: + if result is not None and len(json.dumps({"jsonrpc": "2.0", "id": id_, "result": result})) > P_MAX_RESPONSE_BYTES: + result = p_shrink_oversize(result) msg["result"] = result with P_STDOUT_LOCK: sys.stdout.write(json.dumps(msg) + "\n") diff --git a/backend/tests/test_mcp_response_transport_cap.py b/backend/tests/test_mcp_response_transport_cap.py new file mode 100644 index 00000000..7ebbb246 --- /dev/null +++ b/backend/tests/test_mcp_response_transport_cap.py @@ -0,0 +1,53 @@ +"""Pins the transport ceiling contract (found 2026-08-19): a tools/call response line past the +CLI's silent drop threshold must be shrunk (image stripped, text elided) rather than written, +because an oversized line is WRITTEN successfully and then never resolves, hanging the call +until a watchdog shoots the healthy sidecar.""" +import importlib.util +import json +import os + +spec = importlib.util.spec_from_file_location( + "combined_meta_mcp_server", + os.path.join(os.path.dirname(__file__), "..", "apps", "agents", "combined_meta_mcp_server.py"), +) + + +def p_load(): + os.environ.setdefault("OSW_MCP_MODULES", "") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def test_small_response_untouched(): + mod = p_load() + r = {"content": [{"type": "text", "text": "hello"}]} + assert mod.p_shrink_oversize(r) == r + + +def test_oversize_image_block_stripped_with_note(): + mod = p_load() + r = {"content": [ + {"type": "text", "text": "summary here"}, + {"type": "image", "data": "A" * 340_000, "mimeType": "image/png"}, + ]} + out = mod.p_shrink_oversize(r) + kinds = [c.get("type") for c in out["content"]] + assert "image" not in kinds + assert any("screenshot omitted" in str(c.get("text", "")) for c in out["content"]) + assert len(json.dumps({"result": out})) < mod.P_MAX_RESPONSE_BYTES + + +def test_oversize_text_elided(): + mod = p_load() + r = {"content": [{"type": "text", "text": "B" * 400_000}]} + out = mod.p_shrink_oversize(r) + assert len(json.dumps({"result": out})) < mod.P_MAX_RESPONSE_BYTES + assert "elided" in out["content"][0]["text"] + + +def test_error_results_keep_flag(): + mod = p_load() + r = {"content": [{"type": "image", "data": "C" * 300_000, "mimeType": "image/png"}], "isError": True} + out = mod.p_shrink_oversize(r) + assert out.get("isError") is True