From 9aacc26eb6bea6fda5d5d48a59976adf2e200182 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 3 Dec 2025 20:53:01 +0700 Subject: [PATCH] feat: add 60-second timeout to Agent Control MCP wait operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement hard-coded 60-second timeout for agent wait operations in three tools: - create_agent (when background=false and initialPrompt provided) - send_agent_prompt (when background=false) - wait_for_agent (always) When timeout occurs, returns gracefully with message "Awaiting the agent timed out, await again" instead of throwing an error. Agent continues running after timeout. Added AGENT_WAIT_TIMEOUT_MS constant and waitForAgentWithTimeout helper function to handle timeout logic with proper cleanup and signal forwarding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../server/src/server/agent/mcp-server.ts | 107 +++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 8e01b0c41..a6012fa57 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -46,6 +46,8 @@ const AgentStatusEnum = z.enum([ "closed", ]); +const AGENT_WAIT_TIMEOUT_MS = 60000; // 60 seconds + function expandPath(path: string): string { if (path.startsWith("~/") || path === "~") { return resolve(homedir(), path.slice(2)); @@ -53,6 +55,71 @@ function expandPath(path: string): string { return resolve(path); } +async function waitForAgentWithTimeout( + agentManager: AgentManager, + agentId: string, + existingSignal?: AbortSignal +): Promise { + const timeoutSignal = AbortSignal.timeout(AGENT_WAIT_TIMEOUT_MS); + const abortController = new AbortController(); + + const forwardExistingAbort = () => { + if (!abortController.signal.aborted) { + const reason = existingSignal?.reason ?? new Error("External abort"); + abortController.abort(reason); + } + }; + + const forwardTimeout = () => { + if (!abortController.signal.aborted) { + abortController.abort(new Error("wait timeout")); + } + }; + + if (existingSignal) { + if (existingSignal.aborted) { + forwardExistingAbort(); + } else { + existingSignal.addEventListener("abort", forwardExistingAbort, { + once: true, + }); + } + } + + if (timeoutSignal.aborted) { + forwardTimeout(); + } else { + timeoutSignal.addEventListener("abort", forwardTimeout, { once: true }); + } + + try { + const result = await agentManager.waitForAgentEvent(agentId, { + signal: abortController.signal, + }); + 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, await again", + }; + } + throw error; + } finally { + if (existingSignal && !existingSignal.aborted) { + existingSignal.removeEventListener("abort", forwardExistingAbort); + } + if (!timeoutSignal.aborted) { + timeoutSignal.removeEventListener("abort", forwardTimeout); + } + } +} + function startAgentRun( agentManager: AgentManager, agentId: string, @@ -216,7 +283,7 @@ export async function createAgentMcpServer( // If not running in background, wait for completion if (!background) { - const result = await agentManager.waitForAgentEvent(snapshot.id); + const result = await waitForAgentWithTimeout(agentManager, snapshot.id); const responseData = { agentId: snapshot.id, @@ -314,6 +381,22 @@ export async function createAgentMcpServer( } } + const timeoutSignal = AbortSignal.timeout(AGENT_WAIT_TIMEOUT_MS); + const forwardTimeout = () => { + if (!abortController.signal.aborted) { + abortController.abort(new Error("wait timeout")); + } + }; + + if (timeoutSignal.aborted) { + forwardTimeout(); + } else { + timeoutSignal.addEventListener("abort", forwardTimeout, { once: true }); + cleanupFns.push(() => + timeoutSignal.removeEventListener("abort", forwardTimeout) + ); + } + const unregister = waitTracker.register(agentId, (reason) => { if (!abortController.signal.aborted) { abortController.abort(new Error(reason ?? "wait_for_agent cancelled")); @@ -339,6 +422,26 @@ export async function createAgentMcpServer( structuredContent: validJson, }; return response; + } catch (error) { + if ( + error instanceof Error && + error.message === "wait timeout" + ) { + const snapshot = agentManager.getAgent(agentId); + const validJson = ensureValidJson({ + agentId, + status: snapshot?.lifecycle ?? "idle", + permission: null, + lastMessage: "Awaiting the agent timed out, await again", + }); + + const response = { + content: [], + structuredContent: validJson, + }; + return response; + } + throw error; } finally { cleanup(); } @@ -392,7 +495,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); const responseData = { success: true,