mastra-ai/mastra · error · Error

Extractor slug "${extractor.slug}" is reserved by Observatio

Error message

Extractor slug "${extractor.slug}" is reserved by Observational Memory.

What it means

`validateExtractorList` runs when extractor lists are composed for a processor and re-checks each extractor's slug against the reserved XML tag set (observations, observation, extracted-values, thread, message, messages, conversation, history, system, user, assistant, tool, and built-in slugs current-task/suggested-response/thread-title). Non-internal extractors with a reserved slug are rejected because their output tags would collide with Observational Memory's own parsing protocol. This is a defense-in-depth check at composition time, in addition to the constructor guard.

Source

Thrown at packages/memory/src/processors/observational-memory/extractor.ts:222

      },
      this.internal,
    );
  }
}

export async function resolveExtractors(
  extractors: readonly Extractor<any>[],
  context: ExtractorRuntimeContext,
): Promise<Extractor<any>[]> {
  return Promise.all(extractors.map(extractor => extractor.resolve(context)));
}

export function validateExtractorList(extractors: readonly Extractor<any>[]): Extractor<any>[] {
  const seen = new Map<string, string>();
  for (const extractor of extractors) {
    assertValidSlug(extractor.slug, extractor.name);
    if (!extractor.internal && RESERVED_XML_TAGS.has(extractor.slug)) {
      throw new Error(`Extractor slug "${extractor.slug}" is reserved by Observational Memory.`);
    }
    const previous = seen.get(extractor.slug);
    if (previous) {
      throw new Error(`Duplicate extractor slug "${extractor.slug}" from "${previous}" and "${extractor.name}".`);
    }
    seen.set(extractor.slug, extractor.name);
  }
  return [...extractors];
}

function isJsonLike(value: string): boolean {
  return /^(?:[\[{"-]|\d|true\b|false\b|null\b)/.test(value.trim());
}

function candidateValues(raw: string): unknown[] {
  const trimmed = raw.trim();
  const candidates: unknown[] = [];
  const add = (value: unknown) => {

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Rename the extractor so its slug differs from all reserved tags.
  2. If listing extractors from user input, filter/validate slugs against RESERVED_XML_TAGS before composing the list.
  3. Reuse isBuiltInExtractorSlug / slugifyExtractorName to pre-check names.

Example fix

// before
new Extractor({ name: 'Messages', instructions: 'List key messages' });
// after
new Extractor({ name: 'Key Messages', instructions: 'List key messages' });
Defensive patterns

Strategy: validation

Validate before calling

import { slugifyExtractorName, isBuiltInExtractorSlug } from '@mastra/memory/processors/observational-memory';
for (const e of extractors) {
  if (!e.internal && RESERVED_XML_TAGS.has(e.slug)) throw new Error(`Reserved slug: ${e.slug}`);
}

Type guard

function allSlugsSafe(list) {
  return list.every(e => e.internal || !RESERVED.has(e.slug));
}

Try / catch

try {
  validateExtractorList(extractors);
} catch (e) {
  if (e.message.includes('is reserved by Observational Memory')) {
    logger.error('Extractor list contains reserved slug', { msg: e.message });
  } else { throw e; }
}

Prevention

When it happens

Trigger: Composing a list containing an extractor constructed with `internal = true` bypass semantics won't help here — any user-supplied extractor whose slugified name matches a reserved tag (e.g. 'User', 'History', 'Thread Title'); manually constructed Extractor instances passed via ObservationalMemory config; extractors produced by a helper/factory that assembles names dynamically and happens to hit a reserved word.

Common situations: Dynamically generated extractor names from user config; trying to shadow built-in extractors; a name like 'Messages!' that slugifies to 'messages'.

Related errors


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