mastra-ai/mastra · error
Thread not found
Error message
Thread not found
What it means
When a resourceId is supplied, recallThreadFromStart() verifies thread ownership: it loads the thread via memory.getThreadById({ threadId }) and requires it to exist AND have thread.resourceId === resourceId. If the thread does not exist, or exists under a different resource (user), it throws the generic 'Thread not found' — deliberately vague to avoid leaking cross-resource data.
Source
Thrown at packages/memory/src/tools/om-tools.ts:1146
limit?: number;
detail?: RecallDetail;
partType?: 'text' | 'tool-call' | 'tool-result' | 'reasoning' | 'image' | 'file';
toolName?: string;
anchor?: 'start' | 'end';
maxTokens?: number;
}): Promise<RecallResult> {
if (!memory) {
throw new Error('Memory instance is required for recall');
}
if (!threadId) {
throw new Error('Thread ID is required for recall');
}
// Verify the thread belongs to the current resource
if (resourceId && memory.getThreadById) {
const thread = await memory.getThreadById({ threadId });
if (!thread || thread.resourceId !== resourceId) {
throw new Error('Thread not found');
}
}
const MAX_PAGE = 50;
const MAX_LIMIT = 20;
const normalizedPage = Math.max(Math.min(page, MAX_PAGE), 1);
const normalizedLimit = Math.min(Math.max(limit, 1), MAX_LIMIT);
const pageIndex = normalizedPage - 1;
const fetchCount = pageIndex * normalizedLimit + normalizedLimit + 1;
const result = await memory.recall({
threadId,
resourceId,
page: 0,
perPage: fetchCount,
orderBy: { field: 'createdAt', direction: anchor === 'end' ? 'DESC' : 'ASC' },
});
View on GitHub (pinned to 75dd419e61)
Solutions
- Confirm the threadId exists: const t = await memory.getThreadById({ threadId }); check t is non-null.
- Ensure the resourceId passed at runtime matches the thread's resourceId at creation time.
- Re-create the thread with memory.createThread({ threadId, resourceId }) if it was deleted.
- In multi-tenant setups, derive resourceId from authenticated context, not client input; omit resourceId if cross-resource browsing is intended (the check only runs when resourceId is provided).
Example fix
// before
await recallThreadFromStart({ memory, threadId: input.threadId, resourceId: userA });
// after
const thread = await memory.getThreadById({ threadId: input.threadId });
if (!thread || thread.resourceId !== userA) {
return 'Thread not found for this user.';
}
await recallThreadFromStart({ memory, threadId: input.threadId, resourceId: userA }); Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check ownership before browsing
const thread = await memory.getThreadById({ threadId });
if (!thread) throw new Error(`Thread ${threadId} does not exist`);
if (resourceId && thread.resourceId !== resourceId) {
throw new Error(`Thread ${threadId} belongs to resource ${thread.resourceId}`);
} Type guard
function isAccessibleThread(
thread: { resourceId: string } | null,
resourceId?: string
): thread is { resourceId: string } {
return !!thread && (!resourceId || thread.resourceId === resourceId);
} Try / catch
try {
return await recallThreadFromStart({ memory, threadId, resourceId });
} catch (err) {
if (err instanceof Error && err.message === 'Thread not found') {
// do not leak whether it was missing vs. forbidden
return { error: 'Thread not found or not accessible for this user.' };
}
throw err;
} Prevention
- Derive resourceId from the authenticated session, never from client input.
- List browsable threads via the resource-scope mode='threads' instead of guessing IDs.
- Handle thread deletion (retention policies) by validating existence before deep links.
When it happens
Trigger: Calling recallThreadFromStart with a threadId that was never created, was deleted, or belongs to a different resourceId; the ownership check at om-tools.ts:1143-1148 then throws.
Common situations: Multi-tenant apps where the wrong user's resourceId is bound to the run; listing thread IDs from an old resource and browsing them under a new one; storage cleared/threads pruned while clients hold stale IDs; typos in thread IDs; passing a resourceId that differs from the one used at thread creation.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- AGENT_MEMORY_THREAD_RESOURCE_MISMATCH
- Thread with id ${threadId} is for resource with id ${thread.
- Thread does not belong to the active resource
- Access denied: cannot save messages for a different resource
- Access denied: thread belongs to a different resource
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/b4e80186d4ff0704.
Report an issue: GitHub.