mastra-ai/mastra · error · Error

Extractor slug " " is reserved by Observational Memory.

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.

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.

Appendix: 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)