{"record":{"id":"fbd8d33a5a790139","repo":"mastra-ai/mastra","slug":"cannot-merge-a-knowledge-node-into-a-target-that-i","errorCode":null,"errorMessage":"Cannot merge a knowledge node into a target that is narrower than its source scope","messagePattern":"Cannot merge a knowledge node into a target that is narrower than its source scope","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/storage/domains/knowledge/inmemory.ts","lineNumber":258,"sourceCode":"        if (record.node !== input.id) continue;\n        this.#enqueue('record', record.id, 'delete', createKnowledgeUlid(), record.scope);\n        if (!record.deletedAt) this.#enqueue('record', record.id, 'upsert', createKnowledgeUlid(), record.scope);\n      }\n    }\n    this.#enqueue('node', input.id, 'upsert', updated.version, scope);\n    return cloneNode(updated);\n  }\n\n  async mergeNodes(input: { sourceId: string; targetId: string; sourceVersion: number }): Promise<KnowledgeNode> {\n    if (input.sourceId === input.targetId) throw new Error('Cannot merge a knowledge node into itself');\n    const source = this.#db.knowledgeNodes.get(input.sourceId);\n    if (!source) throw new KnowledgeNotFoundError('node', input.sourceId);\n    if (source.version !== input.sourceVersion) throw new KnowledgeConflictError(input.sourceId);\n    const target = this.#resolveTerminalNode(input.targetId);\n    if (!target) throw new KnowledgeNotFoundError('node', input.targetId);\n    if (target.id === source.id) throw new Error('Cannot create a knowledge merge cycle');\n    if (!isKnowledgeScopeVisible(target.scope, source.scope)) {\n      throw new Error('Cannot merge a knowledge node into a target that is narrower than its source scope');\n    }\n\n    for (const [id, record] of this.#db.knowledgeRecords) {\n      if (record.node === source.id) {\n        this.#db.knowledgeRecords.set(id, { ...record, node: target.id });\n        this.#enqueue('record', id, record.deletedAt ? 'delete' : 'upsert', createKnowledgeUlid(), record.scope);\n      }\n    }\n    for (const [key, mentions] of this.#db.knowledgeMentions) {\n      if (mentions.has(source.id)) {\n        const next = new Set(mentions);\n        next.delete(source.id);\n        next.add(target.id);\n        this.#db.knowledgeMentions.set(key, next);\n        const separator = key.indexOf(':');\n        const sourceType = key.slice(0, separator);\n        const sourceId = key.slice(separator + 1);\n        if (sourceType === 'record') {","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/storage/domains/knowledge/inmemory.ts#L240-L276","documentation":"mergeNodes() enforces that a node can only be merged into a target whose scope is visible from (i.e., at least as wide as) the source's scope. If isKnowledgeScopeVisible(target.scope, source.scope) is false, the target scope is narrower than the source scope, and merging would silently narrow the visibility of the source's records. The library refuses before moving any knowledge records.","triggerScenarios":"Calling mergeNodes() where source.scope is e.g. {org, project} and target.scope is a narrower subset like {org, project, agent} (or a different tenant), so records moved to the target would escape their original visibility ceiling.","commonSituations":"Merging nodes created by different agents with different scopes; refactoring scope taxonomies (renamed agents/projects) so old nodes no longer nest correctly; a dedupe job matching nodes by text similarity across scopes.","solutions":["Pick a merge target whose scope is equal to or wider than the source node's scope.","Rescope the source node/records to the target's scope first (if the ceiling allows), then merge.","If the target must be narrower, raise the target's scope instead of merging the wider node into it.","Pre-filter merge candidates by comparing knowledge scope keys before submitting merges in bulk jobs."],"exampleFix":"// before\nawait storage.mergeNodes({ sourceId: wide.id, sourceVersion, targetId: narrow.id }); // target scope narrower\n// after\nconst candidates = allNodes.filter(n => isKnowledgeScopeVisible(n.scope, wide.scope) && n.id !== wide.id);\nawait storage.mergeNodes({ sourceId: wide.id, sourceVersion, targetId: candidates[0].id });","handlingStrategy":"validation","validationCode":"const source = await storage.getKnowledgeNode(sourceId);\nconst target = await storage.getKnowledgeNode(targetId);\nif (source && target && !isKnowledgeScopeVisible(target.scope, source.scope)) {\n  throw new SkipMerge('target scope narrower than source scope');\n}","typeGuard":"function canMergeIntoScope(sourceScope: KnowledgeScope, targetScope: KnowledgeScope): boolean {\n  return isKnowledgeScopeVisible(targetScope, sourceScope);\n}","tryCatchPattern":"try {\n  await storage.mergeNodes(input);\n} catch (e) {\n  if (e.message.includes('narrower than its source scope')) {\n    // choose a wider target or rescope first\n    return null;\n  }\n  throw e;\n}","preventionTips":["Filter merge candidates with isKnowledgeScopeVisible before submitting.","Keep scope taxonomies consistent across agents creating knowledge.","Rescope records first if consolidation requires a narrower target.","Log skipped merges so scope mismatches are visible."],"tags":["knowledge","merge","scope","visibility"],"backgroundTag":"scope-visibility-violation","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T08:17:16.595Z"}