mastra-ai/mastra · error · Error

Turn already started

Error message

Turn already started

What it means

A Turn models one user turn and start() may only run once, since it fetches/caches the record and captures the generation count baseline. A second start() on the same instance would corrupt that baseline, so it throws 'Turn already started'.

Source

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

  /** The current step, if one exists. */
  get currentStep(): ObservationStep | undefined {
    return this._currentStep;
  }

  addHooks(hooks?: ObservationTurnHooks): void {
    if (!hooks) return;
    Object.assign(this.hooks, hooks);
  }

  /**
   * Load context and cache the record. Call once at the start of the turn.
   *
   * If a MemoryContextProvider is passed, loads historical messages and adds
   * them to the MessageList. Without a provider, only fetches/caches the record.
   */
  async start(memory?: MemoryContextProvider, runState?: MemoryRunState): Promise<TurnContext> {
    if (this._started) throw new Error('Turn already started');
    this._started = true;

    this._record = await this.om.getOrCreateRecord(this.threadId, this.resourceId);
    runState?.set(`observational-memory:record:${this.threadId}:${this.resourceId ?? ''}`, this._record);
    this._generationCountAtStart = this._record.generationCount;
    this.memory = memory;

    if (memory) {
      const ctx = await loadMemoryContextMessages({
        memory,
        messageList: this.messageList,
        threadId: this.threadId,
        resourceId: this.resourceId,
        runState,
      });

      this._context = {
        messages: ctx.messages,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Create a fresh ObservationTurn for a new start cycle rather than reusing the instance
  2. Ensure only one code path calls start() for a given turn instance
  3. Wrap start() so failures happen before side effects, and on retry construct a new turn

Example fix

// before
await turn.start();
await turn.start(memory); // throws
// after
await turn.start(memory);
// for a retry:
const retryTurn = new ObservationTurn(om, threadId, resourceId, messageList);
await retryTurn.start(memory);
Defensive patterns

Strategy: try-catch

Validate before calling

// call start() exactly once per turn instance; track it
if (!turnStartPromise) turnStartPromise = turn.start(memory, runState);
await turnStartPromise;

Type guard

// Memoize the start promise so double invocation is impossible
let started: Promise<TurnContext> | null = null;
function ensureStarted(t: ObservationTurn): Promise<TurnContext> {
  return started ??= t.start();
}

Try / catch

try {
  await turn.start(memory, runState);
} catch (e) {
  if (e instanceof Error && e.message === 'Turn already started') {
    // ignore: another path already started this turn
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `await turn.start()` a second time on the same ObservationTurn — e.g. duplicate start calls from middleware and the main loop, or a retry wrapper that re-invokes start after a failure.

Common situations: Retry logic that re-calls start() instead of creating a new turn; double-wiring of memory hooks both calling start; framework version change where the caller now also starts the turn.

Related errors


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