From 33a3031f74b938aa6d0d19290bcada0f317eafe8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 5 Jan 2026 23:47:58 +0700 Subject: [PATCH] fix(agent): restore MCP wait timeout and fix direct URL navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore 50-second self-imposed timeout for agent wait operations in MCP server. Returns friendly message instead of letting SDK timeout trigger generic "tool failed" error that confuses orchestrator agents. - Fix direct URL navigation to agent screens by waiting for WebSocket connection before initializing. Previously, init would fire before WS connected, fail silently, and never retry due to guard ref. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/app/agent/[serverId]/[agentId].tsx | 7 +- .../server/src/server/agent/mcp-server.ts | 90 +++++++++++++++++-- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx index 7c8ad2532..12fd89229 100644 --- a/packages/app/src/app/agent/[serverId]/[agentId].tsx +++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx @@ -438,6 +438,11 @@ function AgentScreenContent({ return; } + // Skip if not connected - will re-run when connection is established + if (!ws?.isConnected) { + return; + } + // Skip if already initializing if (isInitializingFromMap === true) { return; @@ -459,7 +464,7 @@ function AgentScreenContent({ }); initializeAgent({ agentId: resolvedAgentId }); - }, [resolvedAgentId, initializeAgent, isInitializingFromMap]); + }, [resolvedAgentId, initializeAgent, isInitializingFromMap, ws?.isConnected]); useEffect(() => { if (Platform.OS !== "web") { diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 9e4d61a1f..5266cf554 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -92,6 +92,9 @@ const AgentStatusEnum = z.enum([ "closed", ]); +// 50 seconds - surface friendly message before SDK tool timeout (~60s) +const AGENT_WAIT_TIMEOUT_MS = 50000; + function expandPath(path: string): string { if (path.startsWith("~/") || path === "~") { return resolve(homedir(), path.slice(2)); @@ -99,6 +102,74 @@ function expandPath(path: string): string { return resolve(path); } +/** + * Wraps agentManager.waitForAgentEvent with a self-imposed timeout. + * Returns a friendly message when timeout occurs, rather than letting + * the SDK tool timeout trigger a generic "tool failed" error. + */ +async function waitForAgentWithTimeout( + agentManager: AgentManager, + agentId: string, + options?: { + signal?: AbortSignal; + waitForActive?: boolean; + } +): Promise { + const timeoutController = new AbortController(); + const combinedController = new AbortController(); + + const timeoutId = setTimeout(() => { + timeoutController.abort(new Error("wait timeout")); + }, AGENT_WAIT_TIMEOUT_MS); + + const forwardAbort = (reason: unknown) => { + if (!combinedController.signal.aborted) { + combinedController.abort(reason); + } + }; + + // Forward external signal abort + if (options?.signal) { + if (options.signal.aborted) { + forwardAbort(options.signal.reason); + } else { + options.signal.addEventListener( + "abort", + () => forwardAbort(options.signal!.reason), + { once: true } + ); + } + } + + // Forward timeout abort + timeoutController.signal.addEventListener( + "abort", + () => forwardAbort(timeoutController.signal.reason), + { once: true } + ); + + try { + const result = await agentManager.waitForAgentEvent(agentId, { + signal: combinedController.signal, + waitForActive: options?.waitForActive, + }); + return result; + } catch (error) { + if (error instanceof Error && error.message === "wait timeout") { + const snapshot = agentManager.getAgent(agentId); + return { + status: snapshot?.lifecycle ?? "idle", + permission: null, + lastMessage: + "Awaiting the agent timed out. This does not mean the agent failed - call wait_for_agent again to continue waiting.", + }; + } + throw error; + } finally { + clearTimeout(timeoutId); + } +} + function startAgentRun( agentManager: AgentManager, agentId: string, @@ -373,9 +444,11 @@ export async function createAgentMcpServer( // If not running in background, wait for completion if (!background) { - const result = await agentManager.waitForAgentEvent(snapshot.id, { - waitForActive: true, - }); + const result = await waitForAgentWithTimeout( + agentManager, + snapshot.id, + { waitForActive: true } + ); const responseData = { agentId: snapshot.id, @@ -481,10 +554,11 @@ export async function createAgentMcpServer( cleanupFns.push(unregister); try { - const result: WaitForAgentResult = - await agentManager.waitForAgentEvent(agentId, { - signal: abortController.signal, - }); + const result: WaitForAgentResult = await waitForAgentWithTimeout( + agentManager, + agentId, + { signal: abortController.signal } + ); const validJson = ensureValidJson({ agentId, @@ -596,7 +670,7 @@ export async function createAgentMcpServer( // If not running in background, wait for completion if (!background) { - const result = await agentManager.waitForAgentEvent(agentId, { + const result = await waitForAgentWithTimeout(agentManager, agentId, { waitForActive: true, });