mastra-ai/mastra · error · HTTPException
Access denied: tool call is not suspended on this durable ru
Error message
Access denied: tool call is not suspended on this durable run
What it means
Thrown by validateDurableToolCallAccess when the durable run exists and belongs to the caller's resource, but its snapshot does not show the requested toolCallId as currently suspended — or the run's status is not 'suspended', or the snapshot's agentId does not match the target agent. Approve/decline can only act on live suspended tool calls.
Source
Thrown at packages/server/src/server/handlers/agents.ts:250
workflowRun.resourceId,
input?.state?.resourceId,
input?.messageListState?.memoryInfo?.resourceId,
input?.requestContextEntries?.[MASTRA_RESOURCE_ID_KEY],
].filter((resourceId): resourceId is string => typeof resourceId === 'string' && resourceId.length > 0),
);
const effectiveResourceId = getEffectiveResourceId(requestContext, undefined);
const [persistedResourceId] = persistedResourceIds;
if (persistedResourceIds.size > 1 || (persistedResourceId && persistedResourceId !== effectiveResourceId)) {
throw new HTTPException(403, { message: 'Access denied: durable run belongs to a different resource' });
}
if (
!snapshot ||
snapshot.status !== 'suspended' ||
input?.agentId !== agent.id ||
!hasSuspendedToolCall(snapshot, toolCallId)
) {
throw new HTTPException(403, { message: 'Access denied: tool call is not suspended on this durable run' });
}
}
/**
* Providers whose apiKeyEnvVar entries are aliases for the same credential (any one
* suffices), rather than distinct required values (all needed — the default assumption).
* Keep this list to cases with a confirmed alias relationship; see the "google" entry in
* PROVIDER_OVERRIDES in packages/core/src/llm/model/gateways/models-dev.ts and #17343.
*/
const ALIASED_API_KEY_ENV_VAR_PROVIDERS = new Set(['google']);
/**
* Checks if a provider has its required API key environment variable(s) configured.
* Handles provider IDs with suffixes (e.g., "openai.chat" -> "openai").
* Also handles custom gateway providers that are stored with gateway prefix (e.g., "acme/acme-openai").
* @param providerId - The provider identifier (may include a suffix like ".chat" or be from a custom gateway)
* @param customProviders - Optional record of custom gateway providers to check
* @returns true if all required environment variables are set (or, for aliased providers, if any one is set), false otherwiseView on GitHub (pinned to 75dd419e61)
Solutions
- Refetch the run's pending/suspended tool calls before acting and use a currently-suspended toolCallId
- Make the approve/decline UI idempotent — disable the action after the first click and reflect the resumed state
- Verify you are passing the correct toolCallId and agentId for this run, not one from another run
Example fix
// before
await client.declineToolCall({ runId, toolCallId }); // second call after approve
// after
const pending = await getPendingToolCalls({ runId });
if (pending.includes(toolCallId)) {
await client.declineToolCall({ runId, toolCallId });
} else {
// tool call already resolved; skip or surface 'already handled' to the user
} Defensive patterns
Strategy: try-catch
Validate before calling
const run = await getDurableRun({ runId });
if (run.status !== 'suspended' || !run.suspendedToolCallIds.includes(toolCallId)) {
throw new SkipActionError('Tool call is no longer pending');
} Type guard
function isSuspendedToolCall(snapshot: any, toolCallId: string): boolean {
return snapshot?.status === 'suspended' &&
JSON.stringify(snapshot).includes(toolCallId);
} Try / catch
try {
await client.approveToolCall({ runId, toolCallId });
} catch (e) {
if (isHttpException(e, 403) && String(e.message).includes('not suspended')) {
// already resolved: refresh UI state, treat as idempotent no-op
}
} Prevention
- Make approve/decline UI idempotent (single click, optimistic disable)
- Refetch pending tool calls immediately before acting
- Discard cached toolCallIds after run resume/completion events
- Verify agentId matches the run's agent when calling the route
When it happens
Trigger: Approving or declining a toolCallId that was already approved/declined (run resumed), a tool call on a completed/failed run, a toolCallId from a different run, or passing an agentId that does not match the run's agent.
Common situations: Double-clicking an approve button and sending a second request; UI with stale pending-tool-call lists after the run resumed; clients caching toolCallIds across page reloads; calling decline after approve already resumed the run.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied: durable run belongs to a different resource
- Agent "${agentId}" is already connected to Slack. Disconnect
- Agent "${agentId}" is already connected to Telegram. Disconn
- No Telegram installation found for agent "${agentId}"
- tool_result must be preceded by a tool_call
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/f0094c3cd2e6cf32.
Report an issue: GitHub.