mastra-ai/mastra · error
Thread not found: ${threadId}
Error message
Thread not found: ${threadId} What it means
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.
Source
Thrown at packages/core/src/agent-controller/agent-controller.ts:545
const sessionDeletion = this.#sessionDeletionPromises.get(session);
if (sessionDeletion) await sessionDeletion;
// Evict the dead session's registry entry so the retry finds a
// fresh slot instead of the same dead session forever.
this.#sessionsByResource.delete(registryKey);
continue;
}
// An exact thread binding is part of the createSession contract
// ("existing threads are resumed; missing threads are created with this
// id"), so honor it on cached sessions too. Without this, whichever
// request creates the session first wins: a thread-agnostic caller (SSE
// subscribe, message listing) racing ahead of an exact-thread create
// would leave the session bound to a different thread and the requested
// thread never created.
if (threadId && session.thread.getId() !== threadId) {
const existingThread = await session.thread.getById({ threadId });
if (existingThread) {
if (existingThread.resourceId !== effectiveResourceId) {
throw new Error(`Thread not found: ${threadId}`);
}
await session.thread.switch({ threadId });
} else {
await session.thread.create({ id: threadId });
}
}
// A deletion may have started during the thread-rebinding awaits.
pendingDeletion = this.#deletionsInProgress.get(registryKey);
if (pendingDeletion) {
await pendingDeletion;
continue;
}
if (this.#sessionsBeingDeleted.has(session)) {
const sessionDeletion = this.#sessionDeletionPromises.get(session);
if (sessionDeletion) await sessionDeletion;
// Evict the dead session's registry entry so the retry finds a
// fresh slot instead of the same dead session forever.
this.#sessionsByResource.delete(registryKey);View on GitHub (pinned to 75dd419e61)
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
Example fix
// before
controller.createSession({ resourceId: userId, threadId: sharedThreadId })
// after
controller.createSession({ resourceId: userId, threadId: threadOwnedByUserId ?? undefined }) Defensive patterns
Strategy: validation
Validate before calling
const thread = await storage.getThreadById({ threadId });
if (thread && thread.resourceId !== currentResourceId) {
throw new Error('Thread belongs to a different resource; create a new session');
} Type guard
function threadBelongsToResource(thread: { resourceId: string } | null, resourceId: string): thread is { resourceId: string } {
return thread !== null && thread.resourceId === resourceId;
} Try / catch
try {
await controller.createSession({ resourceId, threadId });
} catch (e) {
if ((e as Error).message.startsWith('Thread not found')) {
// thread exists but is owned by another resource — create a fresh thread instead
await controller.createSession({ resourceId });
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Factory session not found
- Thread not found: ${overrides.threadId}
- Cannot build stream options without a current thread
- Thread with id ${threadId} is for resource with id ${thread.
- Thread not found: ${threadId}
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/b09d05aecc440ee3.
Report an issue: GitHub.