mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-14 08:26:07 +00:00
fix(mcp): fix cold-start connection drop by using lexically-intact owner exit stack (#2307)
The previous _TransferredStack approach broke anyio lexical context: websocket_client/ClientSession use anyio task groups whose cancel scope is bound to the frame that entered them. Deferring their aclose via a transferred exit stack left the underlying memory streams closed once initialize() returned, so the very next request (refresh -> list_tools) failed with Connection closed. New design: - Attach on the owner exit stack (same task as the serve loop, lexically intact) - A cold-starting process makes initialize() fail; signal _ColdStartRetry up to the outer retry loop, which reuses the live process without consuming retry budget - _lifecycle_loop_with_retry handles _ColdStartRetry like _TransportReconnect: preserves process, no fatal budget, backs off 2s and retries - Two new unit tests: cold-start raises _ColdStartRetry (not fatal) when process is alive; raises fatal error when process has actually exited Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
@@ -25,7 +25,7 @@ from ....core import app
|
||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
||||
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
||||
from ....entity.persistence import mcp as persistence_mcp
|
||||
from .mcp_stdio import BoxStdioSessionRuntime, MCPServerBoxConfig, MCPSessionErrorPhase # noqa: F401
|
||||
from .mcp_stdio import BoxStdioSessionRuntime, MCPServerBoxConfig, MCPSessionErrorPhase, _ColdStartRetry # noqa: F401
|
||||
|
||||
# Synthesized LLM tools for MCP resources (not from server tools/list).
|
||||
# Dispatched in MCPLoader.invoke_tool; placeholder func on LLMTool is never used.
|
||||
@@ -490,6 +490,24 @@ class RuntimeMCPSession:
|
||||
self.error_phase = None
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
except _ColdStartRetry as e:
|
||||
# The managed process is alive but still cold-starting (e.g.
|
||||
# `npx -y <pkg>` is still installing) and cannot yet answer the
|
||||
# handshake. Reuse the live process and retry the attach WITHOUT
|
||||
# consuming the fatal retry budget or stopping the process, so a
|
||||
# slow cold start is waited out instead of failing. Preserve the
|
||||
# process across the finally-block cleanup.
|
||||
if self._shutdown_event.is_set():
|
||||
return
|
||||
self._preserve_managed_process = True
|
||||
self.ap.logger.debug(
|
||||
f'MCP session {self.server_name}: waiting for cold start ({self._describe_exception(e)})'
|
||||
)
|
||||
self.status = MCPSessionStatus.CONNECTING
|
||||
self.error_message = None
|
||||
self.error_phase = None
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
except Exception as e:
|
||||
self.retry_count = attempt + 1
|
||||
if self._shutdown_event.is_set():
|
||||
|
||||
Reference in New Issue
Block a user