{"record":{"id":"c2baffc07b10d8a1","repo":"mastra-ai/mastra","slug":"knowledge-merge-cycle-detected-at-node-id","errorCode":null,"errorMessage":"Knowledge merge cycle detected at ${node.id}","messagePattern":"Knowledge merge cycle detected at (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/core/src/storage/domains/knowledge/inmemory.ts","lineNumber":653,"sourceCode":"      snapshot.nodeKeys.forEach((id, key) => this.#db.knowledgeNodeKeys.set(key, id));\n      this.#db.knowledgeRecords.clear();\n      snapshot.records.forEach((record, id) => this.#db.knowledgeRecords.set(id, record));\n      this.#db.knowledgeMentions.clear();\n      snapshot.mentions.forEach((mentions, key) => this.#db.knowledgeMentions.set(key, mentions));\n      this.#db.knowledgeActivity.splice(0, this.#db.knowledgeActivity.length, ...snapshot.activity);\n      this.#db.knowledgeSemanticOutbox.clear();\n      snapshot.outbox.forEach((entry, id) => this.#db.knowledgeSemanticOutbox.set(id, entry));\n      this.#db.knowledgeSemanticIdempotency.clear();\n      snapshot.idempotency.forEach((id, key) => this.#db.knowledgeSemanticIdempotency.set(key, id));\n      throw error;\n    }\n  }\n\n  #resolveTerminalNode(id: string): KnowledgeNode | null {\n    let node = this.#db.knowledgeNodes.get(id);\n    const seen = new Set<string>();\n    while (node?.mergedInto) {\n      if (seen.has(node.id)) throw new Error(`Knowledge merge cycle detected at ${node.id}`);\n      seen.add(node.id);\n      node = this.#db.knowledgeNodes.get(node.mergedInto);\n    }\n    return node ?? null;\n  }\n\n  #replaceMentions(\n    sourceType: KnowledgeMention['sourceType'],\n    sourceId: string,\n    text: string,\n    resolutionScope: KnowledgeScope,\n    defaultScope: KnowledgeScope,\n  ): void {\n    const mentions = new Set<string>();\n    for (const name of parseKnowledgeWikilinks(text)) {\n      let node = this.#resolveNode({ name, scope: resolutionScope });\n      node ??= this.#createNode({ name, kind: 'node', scope: defaultScope });\n      mentions.add(node.id);","sourceCodeStart":635,"sourceCodeEnd":671,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/storage/domains/knowledge/inmemory.ts#L635-L671","documentation":"#resolveTerminalNode follows a node's mergedInto chain to find the final surviving node, tracking visited IDs. If it revisits a node (the chain loops), it throws 'Knowledge merge cycle detected at <id>' to prevent infinite loops. This is a data-integrity guard: mergeNodes should prevent cycles (see 'Cannot create a knowledge merge cycle'), so encountering this means stored merge pointers are corrupt.","triggerScenarios":"Reading any knowledge API that resolves terminal nodes while the store contains a mergedInto loop (e.g., A.mergedInto=B and B.mergedInto=A), typically introduced by concurrent merges in different orders or by manually mutating the in-memory maps.","commonSituations":"Race conditions where two async mergeNodes calls interleave (both resolve pre-merge terminals, then both set mergedInto); tests that hand-craft node graphs in #db; deserialized/restored stores with inconsistent merge pointers.","solutions":["Rebuild the store (or fix the mergedInto pointers) so the chain is acyclic — remove the loop edge.","Serialize merge operations (single-threaded queue/lock) so concurrent merges cannot interleave.","Before merging, resolve both terminals under the same lock and re-validate target != source.","If hand-seeding test data, ensure mergedInto always points to an eventually-terminal node."],"exampleFix":"// before (concurrent)\nawait Promise.all([merge(aIntoB), merge(bIntoA)]); // creates cycle\n// after\nfor (const op of [merge(aIntoB), merge(bIntoA)]) await op; // sequential; second sees updated chain and is skipped/redirected","handlingStrategy":"retry","validationCode":"// before scheduling merges, verify no node in the batch already points (transitively) at another batch node\nfunction chainReaches(nodes: Map<string, KnowledgeNode>, from: string, to: string): boolean {\n  let n = nodes.get(from);\n  while (n?.mergedInto) { if (n.mergedInto === to) return true; n = nodes.get(n.mergedInto); }\n  return false;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await storage.mergeNodes(input);\n} catch (e) {\n  if (e.message.startsWith('Knowledge merge cycle detected')) {\n    // store graph is corrupt: rebuild mergedInto pointers or recreate the store\n    throw new StoreCorruptionError(e.message);\n  }\n  throw e;\n}","preventionTips":["Serialize all mergeNodes calls — never run concurrent merges against one in-memory store.","Re-resolve terminal nodes under the same lock right before writing mergedInto.","Never mutate knowledgeNodes/mergedInto maps directly outside the API.","Add a startup integrity check that walks mergedInto chains for loops."],"tags":["knowledge","merge","cycle","corruption","concurrency"],"backgroundTag":"merge-cycle-detected","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}