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

  1. Rebuild the store (or fix the mergedInto pointers) so the chain is acyclic — remove the loop edge.
  2. Serialize merge operations (single-threaded queue/lock) so concurrent merges cannot interleave.
  3. Before merging, resolve both terminals under the same lock and re-validate target != source.
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/c2baffc07b10d8a1. Report an issue: GitHub.