mastra-ai/mastra · error
No active suspension to resume
Error message
No active suspension to resume
What it means
Thrown when attempting to resume a suspended tool execution but the session's suspension registry has no entry for the given toolCallId. Suspensions are tracked in `this.suspensions`; resume requires a matching active suspension so the library knows which tool, run, and resume boundary to restore. If the suspension was already resumed, cleared, or never registered, this error is thrown.
Solutions
- Check for a pending suspension (via the session's suspension/pending-suspension state) before calling resume, and guard against duplicate resume calls.
- Verify the toolCallId matches the one supplied in the suspend event for the current session.
- If the process restarted, reload the suspended run state (e.g., by fetching the run/thread) to re-register suspensions before resuming.
- Ensure the resume call targets the same session instance that received the suspension.
Example fix
// before
await session.resumeSuspendedTool({ toolCallId, resumeData }); // may not exist
// after
const pending = session.getPendingSuspension?.(toolCallId);
if (pending) {
await session.resumeSuspendedTool({ toolCallId, resumeData });
} Defensive patterns
Strategy: validation
Validate before calling
// before resuming
const pending = session.getPendingSuspension?.(toolCallId);
if (!pending) {
console.warn('No pending suspension for', toolCallId);
return;
} Type guard
function hasPendingSuspension(session: { getPendingSuspension?: (id: string) => unknown }, toolCallId: string): boolean {
return Boolean(session.getPendingSuspension?.(toolCallId));
} Try / catch
try {
await session.resumeSuspendedTool({ toolCallId, resumeData });
} catch (err) {
if (err instanceof Error && err.message === 'No active suspension to resume') {
// already resumed or state lost; refresh suspension list from the run
} else throw err;
} Prevention
- Treat resume as one-shot: disable the control after the first call since the suspension is deleted on resume.
- Store the exact toolCallId from the suspend event, not a different identifier.
- After a process restart, reload run state to rebuild suspension tracking before resuming.
When it happens
Trigger: Calling the resume method with a toolCallId that has no active suspension — the suspension was already resumed (it is deleted before resuming), the run finished, the session restarted, or the ID is wrong/typo'd.
Common situations: Double-clicking 'Resume' so the second call finds the suspension already deleted; resuming after the process restarted and in-memory suspension state was lost; passing the wrong toolCallId (e.g., a run ID instead of a tool call ID); resuming a suspension registered in a different session instance.
Related errors
- Cannot resume a suspended tool without a current thread
- AGENT_RESUME_NO_SNAPSHOT_FOUND
- AGENT_RESUME_TOOL_CALL_NOT_SUSPENDED
- AGENT_SEND_STREAM_RESUME_NO_SUSPENDED_THREAD_RUN
- No active run to approve tool call for
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/fed739c2f5af670e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/core/src/agent-controller/session.ts:3857
* Interactive builtins (`ask_user`, `request_access`) are exempted from the
* approval re-check on resume: their resume schema is `z.string()` /
* `z.array(z.string())` which cannot carry the `{ approved }` field the
* approval gate demands, so re-entering the approval branch would always
* reject the answer. The caller already handled approval (setForTool policy,
* yolo mode, or a prior explicit approval gate).
*/
async resumeToolCall({
resumeData,
toolCallId,
requestContext: requestContextInput,
}: {
resumeData: any;
toolCallId: string;
requestContext?: RequestContext;
}): Promise<void> {
const suspension = this.suspensions.get({ toolCallId });
if (!suspension) {
throw new Error('No active suspension to resume');
}
const agent = this.machinery.getAgent();
// Remove before resuming so a re-suspend during the resumed run can
// re-register the same toolCallId without being clobbered by this cleanup.
// Drop the matching display-state entry too so the UI stops rendering the
// resolved prompt while any other parked suspensions stay visible.
this.suspensions.delete({ toolCallId });
this.displayState.deletePendingSuspension(toolCallId);
const requestContext = await this.machinery.buildRequestContext(requestContextInput);
const threadId = this.thread.getId();
if (!threadId) {
throw new Error('Cannot resume a suspended tool without a current thread');
}
await this.thread.ensureSubscription(threadId);View on GitHub (pinned to 75dd419e61)