mastra-ai/mastra · error

Multi-thread observer produced degenerate output after retry

Error message

Multi-thread observer produced degenerate output after retry. ${describeDegenerateOutput(result.text)}

What it means

`ObserverRunner.callMultiThread` runs the multi-thread Observer variant that summarizes several threads at once. Its output is parsed by `parseMultiThreadObserverOutput`, which detects degenerate repetition; if the automatic single retry also yields degenerate output, this error is thrown (observer-runner.ts:543). It protects cross-thread memory from being overwritten by a model that fell into a repetition loop.

Source

Thrown at packages/memory/src/processors/observational-memory/observer-runner.ts:543

      );
    };

    let result = await doGenerate();
    let parsed = parseMultiThreadObserverOutput(result.text, activeExtractors);
    let retriedDueToDegenerate = false;

    if (parsed.degenerate) {
      omDebug(
        `[OM:callMultiThreadObserver] degenerate repetition detected, retrying once. ${describeDegenerateOutput(result.text, 2000)}`,
      );
      result = await doGenerate();
      parsed = parseMultiThreadObserverOutput(result.text, activeExtractors);
      retriedDueToDegenerate = true;
      if (parsed.degenerate) {
        omDebug(
          `[OM:callMultiThreadObserver] degenerate repetition on retry, failing. ${describeDegenerateOutput(result.text, 2000)}`,
        );
        throw new Error(
          `Multi-thread observer produced degenerate output after retry. ${describeDegenerateOutput(result.text)}`,
        );
      }
    }

    const structuredExtractionByThread = new Map<string, Awaited<ReturnType<typeof extractStructuredValues>>>();

    const aggregatedExtractedValues = mergeExtractedValues(
      ...Array.from(parsed.threads, ([threadId, threadResult]) =>
        mergeExtractedValues(threadResult.extractedValues, structuredExtractionByThread.get(threadId)?.values),
      ),
    );
    const aggregatedExtractionFailures = mergeExtractionFailures(
      ...Array.from(parsed.threads, ([threadId, threadResult]) =>
        mergeExtractionFailures(threadResult.extractionFailures, structuredExtractionByThread.get(threadId)?.failures),
      ),
    );
    const aggregatedBuiltIns = getBuiltInExtractedValues(aggregatedExtractedValues);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Switch to a stronger observation model via `observationModel`, or adjust `routingThresholds`/`routingStrategy` so large multi-thread batches route to a capable model.
  2. Reduce batch size: observe threads more frequently or lower per-cycle thread/token limits so the multi-thread prompt fits comfortably in context.
  3. Add repetition-curbing `modelSettings` (frequencyPenalty/repetitionPenalty, moderate temperature) to the observation config.
  4. Inspect the `describeDegenerateOutput` snippet in the message to identify the looping content, and check whether a specific thread's content triggers it (move that thread to single-thread observation or exclude it).

Example fix

// before
new ObservationalMemory({
  observationModel: 'ollama/qwen2.5:0.5b',
  perResource: { enabled: true }, // multi-thread batches overflow a small model
});

// after
new ObservationalMemory({
  observationModel: 'openai/gpt-4o-mini',
  perResource: { enabled: true },
  modelSettings: { frequencyPenalty: 0.5 },
});
Defensive patterns

Strategy: fallback

Validate before calling

// before enabling multi-thread observation, ensure the model can handle the combined prompt
const totalTokens = threads.reduce((n, t) => n + countTokens(t.messages), 0);
if (totalTokens > modelMaxInputTokens * 0.6) {
  throw new Error('multi-thread batch too large for observation model; reduce batch or use stronger model');
}

Type guard

function looksDegenerate(text: string): boolean {
  const chunks = text.split(/\n{2,}/).map(c => c.trim()).filter(Boolean);
  if (chunks.length < 3) return false;
  return new Set(chunks).size / chunks.length < 0.5;
}

Try / catch

try {
  await memory.process(messages, { abortSignal: signal });
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Multi-thread observer produced degenerate output')) {
    logger.warn('multi-thread observer looping; falling back to per-thread observation');
    await processPerThread(messages, { abortSignal: signal });
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Multi-thread observation is enabled (per-resource observation across multiple threads) and the observation model returns repetition-looped text for the combined multi-thread prompt; the built-in one-shot retry in `callMultiThread` also parses as degenerate, so the batch fails. More likely with small/quantized models given the larger, more complex multi-thread prompt.

Common situations: Running observation across many long threads with a local 1-3B model that loops on the large prompt; context-window overflow when batching multiple threads (truncated output triggers repetition); misconfigured routing thresholds selecting a weak model for large inputs; provider degradation returning repetitive filler.

Related errors


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