mastra-ai/mastra · error · MastraError

AGENT_MEMORY_THREAD_RESOURCE_MISMATCH

AGENT_MEMORY_THREAD_RESOURCE_MISMATCH

Error message

Thread "${thread.id}" belongs to resource "${thread.resourceId}" but resource "${resourceId}" was provided. A thread can only be used by the resource that owns it.

What it means

assertThreadOwnedByResource enforces memory thread ownership: a memory thread is permanently bound to the resourceId that created it, and if a caller passes a thread whose thread.resourceId differs from the provided resourceId (and the thread has a resourceId at all), this USER error is thrown. This prevents cross-resource memory leakage — one user/tenant reading or writing another's conversation thread. It is raised from agent memory entry points (__primitive, prepareForDurableExecution, createPrepareMemoryStep).

Source

Thrown at packages/core/src/agent/memory-thread-ownership.ts:24

 *
 * Threads are scoped to a single resource. Without this check the agent would happily run the
 * model (and tools) for a thread/resource pair that can never read or write that thread's history,
 * so callers could not rely on `Agent.stream()` to reject an invalid pair before execution.
 *
 * Threads stored without a `resourceId` are treated as unowned so pre-existing rows keep working.
 */
export function assertThreadOwnedByResource({
  thread,
  resourceId,
  agentName,
}: {
  thread: StorageThreadType;
  resourceId: string;
  agentName?: string;
}): void {
  if (!thread.resourceId || thread.resourceId === resourceId) return;

  throw new MastraError({
    id: 'AGENT_MEMORY_THREAD_RESOURCE_MISMATCH',
    domain: ErrorDomain.AGENT,
    category: ErrorCategory.USER,
    details: {
      agentName: agentName ?? '',
      threadId: thread.id,
      expectedResourceId: thread.resourceId,
      actualResourceId: resourceId,
    },
    text: `Thread "${thread.id}" belongs to resource "${thread.resourceId}" but resource "${resourceId}" was provided. A thread can only be used by the resource that owns it.`,
  });
}

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Pass the resourceId that owns the thread — fetch the thread (memory.getThreadById) and use thread.resourceId, or stop passing a mismatched resourceId.
  2. Create a new thread for the current resourceId instead of reusing the foreign thread (e.g. omit threadId so a fresh thread is created/scoped correctly).
  3. If resourceIds changed scheme, migrate the stored threads (update thread.resourceId in storage) so they match the new ids.
  4. Audit the client for stale cached thread ids after user switch/logout and clear them.

Example fix

// before
await agent.stream(prompt, { resourceId: currentUserId, threadId: savedThreadId }); // thread owned by someone else

// after
const thread = await memory.getThreadById({ threadId: savedThreadId });
await agent.stream(prompt, { resourceId: thread.resourceId, threadId: thread.id });
Defensive patterns

Strategy: try-catch

Validate before calling

const thread = await memory.getThreadById({ threadId: requestedThreadId });
if (thread && thread.resourceId !== currentResourceId) {
  // do not attempt to use it; create or fetch a thread owned by currentResourceId
  throw new Error('Thread does not belong to current resource');
}

Type guard

function threadOwnedBy(thread: { resourceId?: string }, resourceId: string): boolean {
  return !thread.resourceId || thread.resourceId === resourceId;
}

Try / catch

try {
  await agent.stream(prompt, { resourceId, threadId });
} catch (e) {
  if (e instanceof MastraError && e.id === 'AGENT_MEMORY_THREAD_RESOURCE_MISMATCH') {
    // stale/foreign thread: clear cached threadId and start a new thread for this resource
    await agent.memory.createThread({ resourceId, title: 'New conversation' });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling agent.generate/stream/memory.remember with `threadId` (or thread object) belonging to userA while passing `resourceId: userB`; hard-coding a thread id in tests or code while the resource id is derived from the current session user; swapping auth backends so stored threads carry old resourceIds that no longer match newly computed ones.

Common situations: Multi-tenant apps where the logged-in user changed but the client kept the old thread id; copying thread ids between environments (staging thread used with production resource id); using a shared demo thread id in code while resource ids differ per request; renaming the resourceId scheme (e.g. 'user:1' to '1') after threads already existed.

Related errors


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