mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-29 21:14:49 +02:00
[eric] auto-heal corrupt npx cache in MCP stdio discovery: on ERR_MODULE_NOT_FOUND pointing into ~/.npm/_npx/<hash>/, wipe
that dir and retry once — fixes Discord/YouTube failing with "MCP stdio process exited unexpectedly" after partial npx installs, per-browser auto-close: spawned browser cards now disappear on their own sub-agent's completion instead of lingering until the parent agent finishes, WebSocketManager agent:status matches by session.browser_id (spawned_by is the parent, so it never matched the finishing sub-agent), user-created/pre-selected browsers untouched
This commit is contained in:
@@ -20,12 +20,21 @@ export function claudeToOpenAIRequest(model, body, stream) {
|
||||
result.temperature = body.temperature;
|
||||
}
|
||||
|
||||
// System message
|
||||
// System message — strip the claude-agent-sdk's "You are Claude Code..."
|
||||
// identity sentence before forwarding to non-Anthropic providers. Without
|
||||
// this, Gemini/Codex/etc receive the Claude Code preset's first line as
|
||||
// gospel and dutifully introduce themselves as Claude. The rest of the
|
||||
// preset (tool conventions, scaffolding) stays intact.
|
||||
if (body.system) {
|
||||
const systemContent = Array.isArray(body.system)
|
||||
let systemContent = Array.isArray(body.system)
|
||||
? body.system.map(s => s.text || "").join("\n")
|
||||
: body.system;
|
||||
|
||||
|
||||
systemContent = systemContent.replace(
|
||||
/You are Claude Code, Anthropic's official CLI for Claude\.\s*/,
|
||||
""
|
||||
);
|
||||
|
||||
if (systemContent) {
|
||||
result.messages.push({
|
||||
role: "system",
|
||||
|
||||
@@ -716,8 +716,38 @@ async def _discover_mcp_tools_sse(url: str, headers: dict | None = None) -> list
|
||||
raise HTTPException(status_code=502, detail=f"SSE discovery failed: {first}") from first
|
||||
|
||||
|
||||
async def _discover_mcp_tools_stdio(command: str, args: list[str] | None = None, env: dict | None = None) -> list[dict]:
|
||||
"""Spawn a stdio MCP server process and call tools/list via JSON-RPC over stdin/stdout."""
|
||||
_NPX_CACHE_RE = re.compile(r"_npx[/\\]([0-9a-f]{8,})[/\\]")
|
||||
|
||||
|
||||
def _try_heal_npx_cache(stderr: str) -> str | None:
|
||||
"""On `ERR_MODULE_NOT_FOUND` pointing into `~/.npm/_npx/<hash>/`, wipe that one dir.
|
||||
|
||||
Why: interrupted npx installs leave a `package-lock.json` in the cache dir so
|
||||
subsequent spawns reuse a partially-extracted node_modules tree, which dies at
|
||||
import time. Scoped strictly to the extracted hash subdir — never touches
|
||||
anything outside `~/.npm/_npx/`.
|
||||
"""
|
||||
if "ERR_MODULE_NOT_FOUND" not in stderr:
|
||||
return None
|
||||
m = _NPX_CACHE_RE.search(stderr)
|
||||
if not m:
|
||||
return None
|
||||
hash_ = m.group(1)
|
||||
cache_dir = os.path.join(os.path.expanduser("~"), ".npm", "_npx", hash_)
|
||||
if not os.path.isdir(cache_dir):
|
||||
return None
|
||||
logger.warning("Corrupted npx cache detected at %s; wiping and letting caller retry", cache_dir)
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
return hash_
|
||||
|
||||
|
||||
async def _discover_mcp_tools_stdio(command: str, args: list[str] | None = None, env: dict | None = None, _attempt: int = 0) -> list[dict]:
|
||||
"""Spawn a stdio MCP server process and call tools/list via JSON-RPC over stdin/stdout.
|
||||
|
||||
On the first attempt, a failure that looks like corrupted npx cache
|
||||
(`ERR_MODULE_NOT_FOUND` pointing into `~/.npm/_npx/<hash>/`) triggers one
|
||||
auto-heal + retry. No heal on `_attempt >= 1`.
|
||||
"""
|
||||
cmd_path = _resolve_command(command)
|
||||
if not cmd_path:
|
||||
raise HTTPException(status_code=400, detail=f"Command '{command}' not found on PATH or common install locations")
|
||||
@@ -782,7 +812,9 @@ async def _discover_mcp_tools_stdio(command: str, args: list[str] | None = None,
|
||||
tools_list = data.get("result", {}).get("tools", [])
|
||||
return [{"name": t.get("name", ""), "description": t.get("description", ""), "inputSchema": t.get("inputSchema")} for t in tools_list]
|
||||
|
||||
except HTTPException:
|
||||
except HTTPException as e:
|
||||
if _attempt == 0 and _try_heal_npx_cache(str(e.detail) if e.detail is not None else ""):
|
||||
return await _discover_mcp_tools_stdio(command, args, env, _attempt=1)
|
||||
raise
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPException(status_code=504, detail="MCP stdio server timed out during discovery")
|
||||
|
||||
@@ -129,17 +129,16 @@ class WebSocketManager {
|
||||
if (data.status === 'running' && session_id) {
|
||||
store.dispatch(trackAgentNotification(session_id));
|
||||
}
|
||||
// Auto-remove browsers spawned by this agent when it reaches a
|
||||
// terminal state on its own. agent:closed only fires when the user
|
||||
// clicks X to close the session, so without this hook the browser
|
||||
// cards would linger after natural completion or error. 'stopped'
|
||||
// is intentionally skipped to preserve the "inspect after manual
|
||||
// stop" affordance — matching the agent:closed branch below.
|
||||
if (session_id && (data.status === 'completed' || data.status === 'error')) {
|
||||
const browserCards = store.getState().dashboardLayout.browserCards;
|
||||
for (const card of Object.values(browserCards)) {
|
||||
if (card.spawned_by === session_id) {
|
||||
store.dispatch(removeBrowserCard(card.browser_id));
|
||||
// Per-sub-agent close via browser_id; skip user-created cards (no spawned_by).
|
||||
if (
|
||||
(data.status === 'completed' || data.status === 'error') &&
|
||||
data.session?.mode === 'browser-agent'
|
||||
) {
|
||||
const browserId = data.session.browser_id;
|
||||
if (browserId) {
|
||||
const card = store.getState().dashboardLayout.browserCards[browserId];
|
||||
if (card && card.spawned_by) {
|
||||
store.dispatch(removeBrowserCard(browserId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user