mastra-ai/mastra · error · Error

Step ${this.stepNumber} already prepared

Error message

Step ${this.stepNumber} already prepared

What it means

Each ObservationStep is single-use: prepare() runs the observation pipeline (threshold checks, buffering, system message building) exactly once. A second prepare() call on the same step would double-apply those effects, so it throws.

Source

Thrown at packages/memory/src/processors/observational-memory/observation-turn/step.ts:66

  toJSON() {
    return { stepNumber: this.stepNumber, prepared: this._prepared };
  }

  /** Step context from prepare(). Throws if prepare() hasn't been called. */
  get context(): StepContext {
    if (!this._context) throw new Error('Step not prepared yet — call prepare() first');
    return this._context;
  }

  /**
   * Prepare this step for agent generation.
   *
   * For step 0: activates buffered chunks, checks reflection, builds system message, filters observed.
   * For step > 0: checks thresholds, triggers buffer/observe, saves previous messages,
   * builds system message, filters observed.
   */
  async prepare(): Promise<StepContext> {
    if (this._prepared) throw new Error(`Step ${this.stepNumber} already prepared`);

    const { threadId, resourceId, messageList } = this.turn;
    // Cast to any for internal access to private OM methods (Turn/Step are internal consumers)
    const om = this.turn.om;
    let activated = false;
    let observed = false;
    let buffered = false;
    let reflected = false;
    let didThresholdCleanup = false;
    let observerExchange: StepContext['observerExchange'];

    // ── Step 0: Activate buffered chunks ──────────────────────
    if (this.stepNumber === 0) {
      const step0Messages = getObservableMessages(messageList);
      const activation = await om.activate({
        threadId,
        resourceId,
        checkThreshold: true,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Create a new step via `turn.step(n)` (with an incremented step number) and prepare that instead
  2. Restructure retry logic so retries happen inside/after a single prepare, not by re-calling prepare
  3. Track preparation with step.toJSON().prepared before calling prepare()

Example fix

// before
await step.prepare();
await step.prepare(); // throws
// after
await step.prepare();
const next = turn.step(step.stepNumber + 1);
await next.prepare();
Defensive patterns

Strategy: type-guard

Validate before calling

if (!step.toJSON().prepared) {
  await step.prepare();
}

Type guard

function canPrepare(step: ObservationStep): boolean {
  return !step.toJSON().prepared;
}

Try / catch

try {
  await step.prepare();
} catch (e) {
  if (e instanceof Error && /already prepared/.test(e.message)) {
    // reuse existing context instead of re-preparing
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `await step.prepare()` twice on the same ObservationStep instance — e.g. a retry wrapper that re-invokes prepare after a partial failure, or code that re-enters generation for the same step number.

Common situations: Retry logic around agent generation that re-calls prepare instead of creating a new step; double invocation from both a hook and the main loop; accidental loop reuse of a step variable.

Related errors


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