paperclipai/paperclip · error · ToolGatewayHttpError
settledInvocation?.errorCode ?? tool_execution_failed
settledInvocation?.errorCode ?? tool_execution_failed
Error message
settledInvocation?.errorMessage ?? "Approved tool action failed"
What it means
This 502 ToolGatewayHttpError is thrown by replayMatchingAgentAction when an agent retries a governed tool call whose action request is in 'executing' status. The gateway waits for the in-flight execution to settle (waitForActionRequestExecution); if the invocation did not end in 'executed' (failed, errored, or unsettled), the stored invocation error message/errorCode is surfaced as a 502 'Approved tool action failed' (or the invocation's own message), so the retrying agent learns the execution failed rather than hanging.
Solutions
- Inspect the error's code (settledInvocation.errorCode) and message to identify why the underlying execution failed; fix that root cause (credentials, parameters, remote connectivity) before retrying.
- Change the tool arguments (or add a distinguishing parameter) so the retry does not replay the same failed action request and instead creates a fresh request.
- If the remote tool failed transiently, wait for the action request to leave 'executing' and re-issue the call once settled.
- Check the remote MCP connection health/auth for the tool; upstream failures are the most common cause of the stored errorMessage.
Example fix
// before: blind retry replays the failed executing request
await toolCall("send_email", sameArgs); // 502 Approved tool action failed
// after: inspect failure, fix, and issue a fresh request
if (err.code === "tool_execution_failed") {
await fixRemoteConnection();
await toolCall("send_email", { ...sameArgs, attempt: 2 }); // new argumentsHash → new request
} Defensive patterns
Strategy: try-catch
Validate before calling
// before retrying the same call, check the previous action request state
const prev = await getMatchingActionRequest({ toolName, argumentsHash });
if (prev?.status === "executed") return prev.result; // replay instead of re-executing
if (prev?.status === "rejected") throw new Error("Action was declined; do not retry the same call"); Type guard
function isReplayableExecution(inv: { status?: string } | null | undefined): inv is { status: "executed" } {
return !!inv && inv.status === "executed";
} Try / catch
try {
return await toolCall(toolName, args);
} catch (err) {
if (err?.status === 502 && err?.code === "tool_execution_failed") {
// underlying execution failed; inspect err.message, fix root cause, then
// vary the arguments to create a fresh action request instead of replaying the failed one
return toolCall(toolName, { ...args, retryNonce: Date.now() });
}
throw err;
} Prevention
- Treat 502 tool_execution_failed as a root-cause signal from the remote tool, not a transient HTTP blip.
- Fix upstream connectivity/credentials before retrying identical arguments.
- Vary arguments (nonce) when a genuinely fresh attempt is required so a new action request is created.
- Cap retries and surface the stored errorCode to the operator instead of looping.
When it happens
Trigger: Retrying a tool call whose arguments hash matches an existing action request already in 'executing' state, where the underlying tool execution failed — e.g. the remote MCP tool returned an error, the execution timed out, or the invocation row recorded an errorCode other than success.
Common situations: An agent retry loop re-issues the same tool call while the first execution fails on the remote provider (network error, auth failure at the upstream tool, invalid parameters rejected by the remote server); a crashed executor leaves the invocation unsettled so the fallback 'tool_execution_failed' code is used.
Related errors
- dropping batch after attempt(s); event(s) lost
- DUPLEX_CHANNEL_OPEN_FAILED
- oauth_refresh_failed
- OpenCode event stream closed before the session became…
- (fallback: runner_prp_command_ : )
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/b78f86ff28d7e618.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/tool-gateway.ts:8088
result: storedInvocationResult(invocation),
invocationId: invocation.id,
};
}
if (actionRequest.status === "executing") {
const settled = await waitForActionRequestExecution(actionRequest.id);
const [settledInvocation] = await db
.select()
.from(toolInvocations)
.where(eq(toolInvocations.id, invocation.id))
.limit(1);
if (settled?.status === "executed" && settledInvocation) {
return {
matched: true as const,
result: storedInvocationResult(settledInvocation),
invocationId: invocation.id,
};
}
throw new ToolGatewayHttpError(
502,
settledInvocation?.errorMessage ?? "Approved tool action failed",
settledInvocation?.errorCode ?? "tool_execution_failed",
);
}
if (actionRequest.status === "approved" && actionRequest.decidedAt) {
const result = await executeApprovedAgentInvocation({
actionRequest,
invocation,
});
return { matched: true as const, result, invocationId: invocation.id };
}
return null;
}
/**
* Project an ask-first test request + its invocation onto the lifecycle the
* Test tab panel renders. Recovers the redacted parameter snapshot (theView on GitHub (pinned to 3f1d897a7c)