{"record":{"id":"b09d05aecc440ee3","repo":"mastra-ai/mastra","slug":"thread-not-found-threadid","errorCode":null,"errorMessage":"Thread not found: ${threadId}","messagePattern":"Thread not found: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/agent-controller/agent-controller.ts","lineNumber":545,"sourceCode":"          const sessionDeletion = this.#sessionDeletionPromises.get(session);\n          if (sessionDeletion) await sessionDeletion;\n          // Evict the dead session's registry entry so the retry finds a\n          // fresh slot instead of the same dead session forever.\n          this.#sessionsByResource.delete(registryKey);\n          continue;\n        }\n        // An exact thread binding is part of the createSession contract\n        // (\"existing threads are resumed; missing threads are created with this\n        // id\"), so honor it on cached sessions too. Without this, whichever\n        // request creates the session first wins: a thread-agnostic caller (SSE\n        // subscribe, message listing) racing ahead of an exact-thread create\n        // would leave the session bound to a different thread and the requested\n        // thread never created.\n        if (threadId && session.thread.getId() !== threadId) {\n          const existingThread = await session.thread.getById({ threadId });\n          if (existingThread) {\n            if (existingThread.resourceId !== effectiveResourceId) {\n              throw new Error(`Thread not found: ${threadId}`);\n            }\n            await session.thread.switch({ threadId });\n          } else {\n            await session.thread.create({ id: threadId });\n          }\n        }\n        // A deletion may have started during the thread-rebinding awaits.\n        pendingDeletion = this.#deletionsInProgress.get(registryKey);\n        if (pendingDeletion) {\n          await pendingDeletion;\n          continue;\n        }\n        if (this.#sessionsBeingDeleted.has(session)) {\n          const sessionDeletion = this.#sessionDeletionPromises.get(session);\n          if (sessionDeletion) await sessionDeletion;\n          // Evict the dead session's registry entry so the retry finds a\n          // fresh slot instead of the same dead session forever.\n          this.#sessionsByResource.delete(registryKey);","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/agent-controller/agent-controller.ts#L527-L563","documentation":"When createSession is given an existing threadId, the controller loads the thread and verifies it belongs to the same resourceId (user) as the session. If the thread exists but is owned by a different resource, Mastra throws 'Thread not found' — deliberately masking cross-resource threads so one user cannot attach to another user's thread.","triggerScenarios":"Calling createSession (or the ACP server run) with a threadId that exists in storage but whose stored resourceId differs from effectiveResourceId — e.g. passing another user's conversation id, or reusing a hardcoded threadId across different resourceIds.","commonSituations":"Multi-tenant apps where clients cache thread ids per workspace/user and send the wrong one; sharing a thread id between dev accounts; resourceId derived from auth that changed between calls (anonymous vs logged-in); tests reusing fixtures across users.","solutions":["Ensure the resourceId used to create the session matches the resourceId that owns the threadId","Generate a fresh threadId per user/conversation instead of sharing ids across resources","Fix resourceId derivation (auth context) so it is stable across requests for the same user","Omit threadId to let createSession create a new thread bound to the current resource","In tests, create distinct threads per resource fixture rather than sharing ids"],"exampleFix":"// before\ncontroller.createSession({ resourceId: userId, threadId: sharedThreadId })\n// after\ncontroller.createSession({ resourceId: userId, threadId: threadOwnedByUserId ?? undefined })","handlingStrategy":"validation","validationCode":"const thread = await storage.getThreadById({ threadId });\nif (thread && thread.resourceId !== currentResourceId) {\n  throw new Error('Thread belongs to a different resource; create a new session');\n}","typeGuard":"function threadBelongsToResource(thread: { resourceId: string } | null, resourceId: string): thread is { resourceId: string } {\n  return thread !== null && thread.resourceId === resourceId;\n}","tryCatchPattern":"try {\n  await controller.createSession({ resourceId, threadId });\n} catch (e) {\n  if ((e as Error).message.startsWith('Thread not found')) {\n    // thread exists but is owned by another resource — create a fresh thread instead\n    await controller.createSession({ resourceId });\n  }\n}","preventionTips":["Scope thread ids per resourceId in clients; never share across users","Keep resourceId derivation from auth stable across requests","Let createSession create threads instead of passing pre-existing foreign ids","In tests, fixture a distinct thread per resource"],"tags":["session","thread","ownership","multi-tenancy"],"backgroundTag":"resource-ownership-mismatch","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}