{"record":{"id":"9339583cc4e1a1af","repo":"mastra-ai/mastra","slug":"access-denied-thread-not-found","errorCode":null,"errorMessage":"Access denied: thread not found","messagePattern":"Access denied: thread not found","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"packages/server/src/server/handlers/agents.ts","lineNumber":2684,"sourceCode":"      // so clients cannot list suspended runs outside their own scope.\n      const effectiveResourceId = getEffectiveResourceId(requestContext, query.resourceId);\n      const effectiveThreadId = getEffectiveThreadId(requestContext, query.threadId);\n\n      // Validate ownership/FGA before honoring a thread filter — without this a\n      // caller could probe another user's suspended approvals (including\n      // tool-call args) by guessing a threadId. Reject when ownership cannot be\n      // verified (no memory configured, or the thread does not exist) so a\n      // thread-scoped query is never honored unchecked.\n      if (effectiveThreadId) {\n        const memory = await agent.getMemory({ requestContext });\n        if (!memory) {\n          throw new HTTPException(403, {\n            message: 'Access denied: agent has no memory configured to validate thread ownership',\n          });\n        }\n        const thread = await memory.getThreadById({ threadId: effectiveThreadId });\n        if (!thread) {\n          throw new HTTPException(403, { message: 'Access denied: thread not found' });\n        }\n        await enforceThreadAccess({\n          mastra,\n          requestContext,\n          threadId: effectiveThreadId,\n          thread,\n          effectiveResourceId,\n        });\n      }\n\n      return await agent.listSuspendedRuns({\n        threadId: effectiveThreadId,\n        resourceId: effectiveResourceId,\n        fromDate: query.fromDate,\n        toDate: query.toDate,\n        perPage: query.perPage,\n        page: query.page,\n      });","sourceCodeStart":2666,"sourceCodeEnd":2702,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/agents.ts#L2666-L2702","documentation":"When listing suspended runs with a threadId filter, the server looks the thread up via memory.getThreadById and rejects with HTTPException 403 if no thread with that id exists. This fails closed to prevent probing other users' suspended tool calls by guessing thread ids; a nonexistent thread is indistinguishable from one you do not own.","triggerScenarios":"GET /agents/:agentId/suspended-runs?threadId=X where X does not exist in the configured memory storage — e.g. deleted thread, typo'd id, wrong storage backend (pointing at a different database), or thread created in another environment.","commonSituations":"Stale UI caches holding thread ids from deleted threads; switching storage providers (dev vs prod database) so ids no longer resolve; passing a resourceId-less thread id from another tenant; ids truncated or reformatted by client code.","solutions":["Verify the threadId against the configured storage (memory.getThreadById or the threads API) before filtering on it","Check the server is connected to the storage backend where the thread was created (same DB_URL/environment)","Re-fetch the current thread list for the resourceId and use a valid id","Remove the threadId filter and list suspended runs scoped by resourceId only"],"exampleFix":"// before\nconst runs = await client.getAgent('a').listSuspendedRuns({ threadId: cachedThreadId }); // stale id\n// after\nconst thread = await client.getMemoryThread(cachedThreadId).catch(() => null);\nconst runs = thread ? await client.getAgent('a').listSuspendedRuns({ threadId: cachedThreadId }) : await client.getAgent('a').listSuspendedRuns({ resourceId });","handlingStrategy":"fallback","validationCode":"const thread = await memory.getThreadById({ threadId });\nif (!thread) throw new Error(`Thread ${threadId} does not exist in configured storage`);","typeGuard":"function isExistingThread(t: unknown): t is { id: string } {\n  return typeof t === 'object' && t !== null && typeof (t as any).id === 'string' && (t as any).id.length > 0;\n}","tryCatchPattern":"try { return await listSuspendedRuns({ threadId }); } catch (e) { if (e?.status === 403 && /thread not found/.test(e.message)) { return listSuspendedRuns({ resourceId }); } throw e; }","preventionTips":["Refresh thread ids from the server instead of caching them indefinitely","Ensure dev and prod point at the same storage backend the threads were created in","Check for thread deletion before using a previously stored threadId"],"tags":["http-403","access-denied","thread-not-found","security"],"backgroundTag":"access-denied-thread-ownership","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}