mastra-ai/mastra · error
Cannot merge a knowledge node into itself
Error message
Cannot merge a knowledge node into itself
What it means
mergeNodes was called with sourceId === targetId. Merging a node into itself is meaningless and would corrupt the merge chain, so the store rejects it immediately before any lookup.
Source
Thrown at packages/core/src/storage/domains/knowledge/inmemory.ts:250
if (input.content !== undefined || input.name !== undefined || input.scope !== undefined) {
this.#replaceMentions('node', input.id, updated.content ?? '', input.resolutionScope ?? scope, scope);
}
this.#recordActivity('node-updated', 'node', input.id, scope);
const scopeChanged = knowledgeScopeKey(existing.scope) !== knowledgeScopeKey(scope);
if (scopeChanged) {
this.#enqueue('node', input.id, 'delete', createKnowledgeUlid(), existing.scope);
for (const record of this.#db.knowledgeRecords.values()) {
if (record.node !== input.id) continue;
this.#enqueue('record', record.id, 'delete', createKnowledgeUlid(), record.scope);
if (!record.deletedAt) this.#enqueue('record', record.id, 'upsert', createKnowledgeUlid(), record.scope);
}
}
this.#enqueue('node', input.id, 'upsert', updated.version, scope);
return cloneNode(updated);
}
async mergeNodes(input: { sourceId: string; targetId: string; sourceVersion: number }): Promise<KnowledgeNode> {
if (input.sourceId === input.targetId) throw new Error('Cannot merge a knowledge node into itself');
const source = this.#db.knowledgeNodes.get(input.sourceId);
if (!source) throw new KnowledgeNotFoundError('node', input.sourceId);
if (source.version !== input.sourceVersion) throw new KnowledgeConflictError(input.sourceId);
const target = this.#resolveTerminalNode(input.targetId);
if (!target) throw new KnowledgeNotFoundError('node', input.targetId);
if (target.id === source.id) throw new Error('Cannot create a knowledge merge cycle');
if (!isKnowledgeScopeVisible(target.scope, source.scope)) {
throw new Error('Cannot merge a knowledge node into a target that is narrower than its source scope');
}
for (const [id, record] of this.#db.knowledgeRecords) {
if (record.node === source.id) {
this.#db.knowledgeRecords.set(id, { ...record, node: target.id });
this.#enqueue('record', id, record.deletedAt ? 'delete' : 'upsert', createKnowledgeUlid(), record.scope);
}
}
for (const [key, mentions] of this.#db.knowledgeMentions) {
if (mentions.has(source.id)) {View on GitHub (pinned to 75dd419e61)
Solutions
- Guard the call site: skip merging when sourceId === targetId
- When deduplicating, exclude the source node id from candidate targets
- Fetch candidate targets by name and filter out the source before merging
Example fix
// before
await store.mergeNodes({ sourceId, targetId, sourceVersion });
// after
if (sourceId !== targetId) await store.mergeNodes({ sourceId, targetId, sourceVersion }); Defensive patterns
Strategy: validation
Validate before calling
if (sourceId === targetId) throw new Error('refusing to merge a node into itself'); Type guard
function isMergePairValid(sourceId: string, targetId: string): boolean {
return sourceId !== targetId && sourceId.length > 0 && targetId.length > 0;
} Try / catch
try {
await store.mergeNodes({ sourceId, targetId, sourceVersion });
} catch (e) {
if (e instanceof Error && e.message === 'Cannot merge a knowledge node into itself') return null;
throw e;
} Prevention
- Exclude the source id from candidate target lists in dedup UIs and scripts
- Validate merge pairs before invoking mergeNodes
- When targets come from name lookups, filter out the source node by id
- Add unit tests for self-merge guard rails in dedup logic
When it happens
Trigger: Calling mergeNodes({ sourceId: x, targetId: x, sourceVersion }) — usually from code that resolved target and source from the same variable, or a UI allowing the same node to be selected for both fields.
Common situations: Batch dedup scripts where the duplicate list accidentally includes the node itself; frontend dropdown not excluding the source node.
Related errors
- Record is not a pin: ${recordId}
- Relaunch requires a new executionId.
- Unsupported output format: ${format}
- Invalid signal type: ${input.type}. Use a supported signal t
- Mode "${mode.id}" transitionsTo cannot reference itself
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/9d002df57ecc6ebb.
Report an issue: GitHub.