feat: add 60-second timeout to Agent Control MCP wait operations

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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-03 20:53:01 +07:00
co-authored by Claude
parent 5f645d6b7a
commit 9aacc26eb6
+105 -2
View File
@@ -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<WaitForAgentResult> {
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,