mastra-ai/mastra · critical
Knowledge merge cycle detected at ${node.id}
Error message
Knowledge merge cycle detected at ${node.id} What it means
#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.
Source
Thrown at packages/core/src/storage/domains/knowledge/inmemory.ts:653
snapshot.nodeKeys.forEach((id, key) => this.#db.knowledgeNodeKeys.set(key, id));
this.#db.knowledgeRecords.clear();
snapshot.records.forEach((record, id) => this.#db.knowledgeRecords.set(id, record));
this.#db.knowledgeMentions.clear();
snapshot.mentions.forEach((mentions, key) => this.#db.knowledgeMentions.set(key, mentions));
this.#db.knowledgeActivity.splice(0, this.#db.knowledgeActivity.length, ...snapshot.activity);
this.#db.knowledgeSemanticOutbox.clear();
snapshot.outbox.forEach((entry, id) => this.#db.knowledgeSemanticOutbox.set(id, entry));
this.#db.knowledgeSemanticIdempotency.clear();
snapshot.idempotency.forEach((id, key) => this.#db.knowledgeSemanticIdempotency.set(key, id));
throw error;
}
}
#resolveTerminalNode(id: string): KnowledgeNode | null {
let node = this.#db.knowledgeNodes.get(id);
const seen = new Set<string>();
while (node?.mergedInto) {
if (seen.has(node.id)) throw new Error(`Knowledge merge cycle detected at ${node.id}`);
seen.add(node.id);
node = this.#db.knowledgeNodes.get(node.mergedInto);
}
return node ?? null;
}
#replaceMentions(
sourceType: KnowledgeMention['sourceType'],
sourceId: string,
text: string,
resolutionScope: KnowledgeScope,
defaultScope: KnowledgeScope,
): void {
const mentions = new Set<string>();
for (const name of parseKnowledgeWikilinks(text)) {
let node = this.#resolveNode({ name, scope: resolutionScope });
node ??= this.#createNode({ name, kind: 'node', scope: defaultScope });
mentions.add(node.id);View on GitHub (pinned to 75dd419e61)
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.
Example fix
// before (concurrent) await Promise.all([merge(aIntoB), merge(bIntoA)]); // creates cycle // after for (const op of [merge(aIntoB), merge(bIntoA)]) await op; // sequential; second sees updated chain and is skipped/redirected
Defensive patterns
Strategy: retry
Validate before calling
// before scheduling merges, verify no node in the batch already points (transitively) at another batch node
function chainReaches(nodes: Map<string, KnowledgeNode>, from: string, to: string): boolean {
let n = nodes.get(from);
while (n?.mergedInto) { if (n.mergedInto === to) return true; n = nodes.get(n.mergedInto); }
return false;
} Try / catch
try {
await storage.mergeNodes(input);
} catch (e) {
if (e.message.startsWith('Knowledge merge cycle detected')) {
// store graph is corrupt: rebuild mergedInto pointers or recreate the store
throw new StoreCorruptionError(e.message);
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot create a knowledge merge cycle
- Cannot merge a knowledge node into a target that is narrower
- MastraFactory.prepare() called twice
- Factory kickoff lease was lost before completion.
- This relationship would create a cycle.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/c2baffc07b10d8a1.
Report an issue: GitHub.