{"record":{"id":"4b4339f40f6aa60a","repo":"mastra-ai/mastra","slug":"knowledge-node-cursor-does-not-match-the-active-br","errorCode":null,"errorMessage":"Knowledge node cursor does not match the active browse filters.","messagePattern":"Knowledge node cursor does not match the active browse filters\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/src/storage/domains/knowledge/base.ts","lineNumber":204,"sourceCode":"  try {\n    value = JSON.parse(decodeURIComponent(cursor));\n  } catch {\n    throw new Error('Invalid knowledge node cursor.');\n  }\n  if (!value || typeof value !== 'object') throw new Error('Invalid knowledge node cursor.');\n  const parsed = value as Record<string, unknown>;\n  const updatedAt = typeof parsed.updatedAt === 'string' ? new Date(parsed.updatedAt) : new Date(Number.NaN);\n  if (\n    parsed.version !== 1 ||\n    parsed.type !== 'node' ||\n    typeof parsed.name !== 'string' ||\n    typeof parsed.id !== 'string' ||\n    Number.isNaN(updatedAt.getTime()) ||\n    parsed.namePrefix !== (filters.namePrefix?.toLocaleLowerCase() ?? null) ||\n    parsed.kind !== (filters.kind ?? null) ||\n    parsed.hasContent !== (filters.hasContent ?? null)\n  ) {\n    throw new Error('Knowledge node cursor does not match the active browse filters.');\n  }\n  return { updatedAt, name: parsed.name, id: parsed.id };\n}\n\n/** @experimental Knowledge APIs are experimental and may change without notice. */\nexport interface QueryKnowledgeInput {\n  node: KnowledgeNodeReference;\n\n  scope: KnowledgeScope;\n  after?: string;\n  limit?: number;\n  includeDeleted?: boolean;\n}\n\n/** @experimental Knowledge APIs are experimental and may change without notice. */\nexport interface QueryKnowledgeOutput {\n  records: KnowledgeRecord[];\n  nextCursor?: string;","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/storage/domains/knowledge/base.ts#L186-L222","documentation":"parseKnowledgeNodeCursor embeds the active browse filters (namePrefix lowercased, kind, hasContent) into each cursor token created by createKnowledgeNodeCursor. When a cursor is passed back to listNodes, it is decoded and its embedded filter fingerprint is compared against the filters of the current request; any mismatch (or a malformed cursor missing version:1/type:'node'/valid fields) throws this error. This prevents cursors from silently resuming a differently-filtered listing with wrong pagination semantics.","triggerScenarios":"Calling KnowledgeStorage.listNodes (or equivalent browse API) with a cursor string obtained from a previous listNodes call that used different filters — e.g. page 1 with kind:'doc', page 2 with kind:'note'; or adding/removing namePrefix or hasContent between pages; or passing a cursor from listNodes into a differently-shaped browse call.","commonSituations":"Pagination state stored client-side (URL query param, React state) while the user edits the search/filter UI before fetching the next page; server restarting with a changed default filter; serializing cursors across API versions or tenants; hand-crafting cursors instead of using the returned nextCursor.","solutions":["Always echo back the exact cursor string returned by the previous listNodes call together with the IDENTICAL filters object used in that call.","If filters changed, restart pagination: drop the cursor and issue a fresh first-page request with the new filters.","Regenerate cursors with createKnowledgeNodeCursor(lastNode, filters) only if you intentionally keep the same filters.","Ensure the cursor is not truncated or mangled by URL encoding/decoding (it is encodeURIComponent-wrapped JSON)."],"exampleFix":"// before\nconst page2 = await storage.listNodes({ scope, kind: 'note', cursor: page1Cursor });\n// after\nconst page2 = await storage.listNodes({ scope, kind: 'doc', cursor: page1Cursor }); // same filters as the request that produced page1Cursor","handlingStrategy":"try-catch","validationCode":"// Store {cursor, filters} together and compare before resuming:\nfunction canResumeCursor(saved: { cursor: string; filters: { namePrefix?: string; kind?: string; hasContent?: boolean } }, current: typeof saved.filters) {\n  return (saved.filters.kind ?? null) === (current.kind ?? null)\n    && (saved.filters.namePrefix?.toLocaleLowerCase() ?? null) === (current.namePrefix?.toLocaleLowerCase() ?? null)\n    && (saved.filters.hasContent ?? null) === (current.hasContent ?? null);\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await storage.listNodes({ ...filters, cursor });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('does not match the active browse filters')) {\n    return await storage.listNodes({ ...filters }); // restart pagination with new filters\n  }\n  throw e;\n}","preventionTips":["Persist cursor and its filters as one unit; never mix a cursor with different filters.","Reset pagination state whenever filter UI inputs change.","Only use nextCursor values returned by the API; never construct cursors by hand."],"tags":["pagination","cursor","knowledge","validation"],"backgroundTag":"cursor-filter-mismatch","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}