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 otherwise

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Refetch the run's pending/suspended tool calls before acting and use a currently-suspended toolCallId
  2. Make the approve/decline UI idempotent — disable the action after the first click and reflect the resumed state
  3. 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

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

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/f0094c3cd2e6cf32. Report an issue: GitHub.