mastra-ai/mastra · error

Cannot merge a knowledge node into a target that is narrower

Error message

Cannot merge a knowledge node into a target that is narrower than its source scope

What it means

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.

Source

Thrown at packages/core/src/storage/domains/knowledge/inmemory.ts:258

        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)) {
        const next = new Set(mentions);
        next.delete(source.id);
        next.add(target.id);
        this.#db.knowledgeMentions.set(key, next);
        const separator = key.indexOf(':');
        const sourceType = key.slice(0, separator);
        const sourceId = key.slice(separator + 1);
        if (sourceType === 'record') {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Pick a merge target whose scope is equal to or wider than the source node's scope.
  2. Rescope the source node/records to the target's scope first (if the ceiling allows), then merge.
  3. If the target must be narrower, raise the target's scope instead of merging the wider node into it.
  4. Pre-filter merge candidates by comparing knowledge scope keys before submitting merges in bulk jobs.

Example fix

// before
await storage.mergeNodes({ sourceId: wide.id, sourceVersion, targetId: narrow.id }); // target scope narrower
// after
const candidates = allNodes.filter(n => isKnowledgeScopeVisible(n.scope, wide.scope) && n.id !== wide.id);
await storage.mergeNodes({ sourceId: wide.id, sourceVersion, targetId: candidates[0].id });
Defensive patterns

Strategy: validation

Validate before calling

const source = await storage.getKnowledgeNode(sourceId);
const target = await storage.getKnowledgeNode(targetId);
if (source && target && !isKnowledgeScopeVisible(target.scope, source.scope)) {
  throw new SkipMerge('target scope narrower than source scope');
}

Type guard

function canMergeIntoScope(sourceScope: KnowledgeScope, targetScope: KnowledgeScope): boolean {
  return isKnowledgeScopeVisible(targetScope, sourceScope);
}

Try / catch

try {
  await storage.mergeNodes(input);
} catch (e) {
  if (e.message.includes('narrower than its source scope')) {
    // choose a wider target or rescope first
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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