{"record":{"id":"a8db3126356fb1fa","repo":"mastra-ai/mastra","slug":"agent-memory-thread-resource-mismatch","errorCode":"AGENT_MEMORY_THREAD_RESOURCE_MISMATCH","errorMessage":"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.","messagePattern":"Thread \"(.+?)\" belongs to resource \"(.+?)\" but resource \"(.+?)\" was provided\\. A thread can only be used by the resource that owns it\\.","errorType":"error_code","errorClass":"MastraError","httpStatus":null,"severity":"error","filePath":"packages/core/src/agent/memory-thread-ownership.ts","lineNumber":24,"sourceCode":" *\n * Threads are scoped to a single resource. Without this check the agent would happily run the\n * model (and tools) for a thread/resource pair that can never read or write that thread's history,\n * so callers could not rely on `Agent.stream()` to reject an invalid pair before execution.\n *\n * Threads stored without a `resourceId` are treated as unowned so pre-existing rows keep working.\n */\nexport function assertThreadOwnedByResource({\n  thread,\n  resourceId,\n  agentName,\n}: {\n  thread: StorageThreadType;\n  resourceId: string;\n  agentName?: string;\n}): void {\n  if (!thread.resourceId || thread.resourceId === resourceId) return;\n\n  throw new MastraError({\n    id: 'AGENT_MEMORY_THREAD_RESOURCE_MISMATCH',\n    domain: ErrorDomain.AGENT,\n    category: ErrorCategory.USER,\n    details: {\n      agentName: agentName ?? '',\n      threadId: thread.id,\n      expectedResourceId: thread.resourceId,\n      actualResourceId: resourceId,\n    },\n    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.`,\n  });\n}\n","sourceCodeStart":6,"sourceCodeEnd":37,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/agent/memory-thread-ownership.ts#L6-L37","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Pass the resourceId that owns the thread — fetch the thread (memory.getThreadById) and use thread.resourceId, or stop passing a mismatched resourceId.","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).","If resourceIds changed scheme, migrate the stored threads (update thread.resourceId in storage) so they match the new ids.","Audit the client for stale cached thread ids after user switch/logout and clear them."],"exampleFix":"// before\nawait agent.stream(prompt, { resourceId: currentUserId, threadId: savedThreadId }); // thread owned by someone else\n\n// after\nconst thread = await memory.getThreadById({ threadId: savedThreadId });\nawait agent.stream(prompt, { resourceId: thread.resourceId, threadId: thread.id });","handlingStrategy":"try-catch","validationCode":"const thread = await memory.getThreadById({ threadId: requestedThreadId });\nif (thread && thread.resourceId !== currentResourceId) {\n  // do not attempt to use it; create or fetch a thread owned by currentResourceId\n  throw new Error('Thread does not belong to current resource');\n}","typeGuard":"function threadOwnedBy(thread: { resourceId?: string }, resourceId: string): boolean {\n  return !thread.resourceId || thread.resourceId === resourceId;\n}","tryCatchPattern":"try {\n  await agent.stream(prompt, { resourceId, threadId });\n} catch (e) {\n  if (e instanceof MastraError && e.id === 'AGENT_MEMORY_THREAD_RESOURCE_MISMATCH') {\n    // stale/foreign thread: clear cached threadId and start a new thread for this resource\n    await agent.memory.createThread({ resourceId, title: 'New conversation' });\n  } else throw e;\n}","preventionTips":["Never hard-code thread ids; resolve them from the current authenticated resourceId","Clear cached thread ids on user logout/account switch in the client","When changing resourceId schemes, run a storage migration for existing threads","Prefer letting Mastra create/retrieve the thread from resourceId + threadId it owns, and validate ownership before reuse"],"tags":["memory","threads","multi-tenancy","authorization"],"backgroundTag":"thread-resource-ownership-mismatch","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}