{"record":{"id":"7460a106f5f510d9","repo":"mastra-ai/mastra","slug":"turn-already-ended","errorCode":null,"errorMessage":"Turn already ended","messagePattern":"Turn already ended","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/memory/src/processors/observational-memory/observation-turn/turn.ts","lineNumber":197,"sourceCode":"  setRecord(record: ObservationalMemoryRecord): void {\n    this._record = record;\n    if (this._context) {\n      this._context.record = record;\n    }\n  }\n\n  /** Patch the cached turn record with merged fields. */\n  patchRecord(patch: Partial<ObservationalMemoryRecord>): void {\n    this.setRecord({ ...this.record, ...patch });\n  }\n\n  /**\n   * Create a step handle. If a previous step exists, it is finalized\n   * (its output messages will be saved at the start of the new step's prepare()).\n   */\n  step(stepNumber: number): ObservationStep {\n    if (!this._started) throw new Error('Turn not started — call start() first');\n    if (this._ended) throw new Error('Turn already ended');\n\n    this._currentStep = new ObservationStep(this, stepNumber);\n    return this._currentStep;\n  }\n\n  /**\n   * Finalize the turn: save any remaining messages and return the current cached record.\n   *\n   * When async observation buffering is enabled and there are unobserved messages,\n   * a background buffer operation is kicked off so that observations are computed\n   * proactively while the agent is idle, rather than waiting for the next turn.\n   * The returned record does not wait for that background buffering pass to finish.\n   */\n  async end(): Promise<TurnResult> {\n    if (this._ended) throw new Error('Turn already ended');\n    this._ended = true;\n\n    // Save any unsaved messages from the last step","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/memory/src/processors/observational-memory/observation-turn/turn.ts#L179-L215","documentation":"ObservationTurn enforces a linear lifecycle: start() -> step() -> end(). This error is thrown by step() when the turn has already been finalized via end(), so no further steps can be created. It guards against reusing a consumed turn object.","triggerScenarios":"Calling turn.step(n) after turn.end() has been called, or after a prior step sequence completed; typically from reusing a cached/stale ObservationTurn reference.","commonSituations":"Agent loops that keep a reference to the previous turn across iterations; double-processing the same messageList (e.g. retry logic re-invokes step on an already-ended turn); storing the turn in memory and calling step again on a later request.","solutions":["Create a fresh ObservationTurn (call start()) for each new turn instead of reusing the old object","Restructure the loop so step()/end() are called exactly once per turn lifecycle","Guard with a check of the turn's ended state (or track it locally) before calling step()"],"exampleFix":"// before\nif (!turn) turn = new ObservationTurn(...);\nturn.step(stepNumber);\n// after\nif (!turn || turnEnded) {\n  turn = new ObservationTurn(...);\n  turn.start();\n  turnEnded = false;\n}\nconst step = turn.step(stepNumber);\n// ... at end of loop:\nawait turn.end();\nturnEnded = true;","handlingStrategy":"validation","validationCode":"if (!turn || turnIsEnded) {\n  turn = createNewTurn();\n  turn.start();\n}\nconst step = turn.step(stepNumber);","typeGuard":"function turnIsActive(turn: ObservationTurn | null | undefined): turn is ObservationTurn {\n  return !!turn && !turn.isEnded();\n}","tryCatchPattern":"try {\n  const step = turn.step(stepNumber);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Turn already ended') {\n    turn = createNewTurn();\n    turn.start();\n  } else throw err;\n}","preventionTips":["Treat ObservationTurn as single-use: create a new turn per request/iteration","Never cache turn objects across agent loop iterations or requests","Prefer the result() helper which manages the full lifecycle","Track finalization with your own boolean if end() is called from multiple code paths"],"tags":["lifecycle","state-management","memory"],"backgroundTag":"invalid-state-transition","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T08:17:16.595Z"}