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:
@@ -46,6 +46,8 @@ const AgentStatusEnum = z.enum([
|
|||||||
"closed",
|
"closed",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const AGENT_WAIT_TIMEOUT_MS = 60000; // 60 seconds
|
||||||
|
|
||||||
function expandPath(path: string): string {
|
function expandPath(path: string): string {
|
||||||
if (path.startsWith("~/") || path === "~") {
|
if (path.startsWith("~/") || path === "~") {
|
||||||
return resolve(homedir(), path.slice(2));
|
return resolve(homedir(), path.slice(2));
|
||||||
@@ -53,6 +55,71 @@ function expandPath(path: string): string {
|
|||||||
return resolve(path);
|
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(
|
function startAgentRun(
|
||||||
agentManager: AgentManager,
|
agentManager: AgentManager,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
@@ -216,7 +283,7 @@ export async function createAgentMcpServer(
|
|||||||
|
|
||||||
// If not running in background, wait for completion
|
// If not running in background, wait for completion
|
||||||
if (!background) {
|
if (!background) {
|
||||||
const result = await agentManager.waitForAgentEvent(snapshot.id);
|
const result = await waitForAgentWithTimeout(agentManager, snapshot.id);
|
||||||
|
|
||||||
const responseData = {
|
const responseData = {
|
||||||
agentId: snapshot.id,
|
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) => {
|
const unregister = waitTracker.register(agentId, (reason) => {
|
||||||
if (!abortController.signal.aborted) {
|
if (!abortController.signal.aborted) {
|
||||||
abortController.abort(new Error(reason ?? "wait_for_agent cancelled"));
|
abortController.abort(new Error(reason ?? "wait_for_agent cancelled"));
|
||||||
@@ -339,6 +422,26 @@ export async function createAgentMcpServer(
|
|||||||
structuredContent: validJson,
|
structuredContent: validJson,
|
||||||
};
|
};
|
||||||
return response;
|
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 {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
@@ -392,7 +495,7 @@ export async function createAgentMcpServer(
|
|||||||
|
|
||||||
// If not running in background, wait for completion
|
// If not running in background, wait for completion
|
||||||
if (!background) {
|
if (!background) {
|
||||||
const result = await agentManager.waitForAgentEvent(agentId);
|
const result = await waitForAgentWithTimeout(agentManager, agentId);
|
||||||
|
|
||||||
const responseData = {
|
const responseData = {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user