mastra-ai/mastra · error · Error
Turn already ended
Error message
Turn already ended
What it means
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.
Source
Thrown at packages/memory/src/processors/observational-memory/observation-turn/turn.ts:197
setRecord(record: ObservationalMemoryRecord): void {
this._record = record;
if (this._context) {
this._context.record = record;
}
}
/** Patch the cached turn record with merged fields. */
patchRecord(patch: Partial<ObservationalMemoryRecord>): void {
this.setRecord({ ...this.record, ...patch });
}
/**
* Create a step handle. If a previous step exists, it is finalized
* (its output messages will be saved at the start of the new step's prepare()).
*/
step(stepNumber: number): ObservationStep {
if (!this._started) throw new Error('Turn not started — call start() first');
if (this._ended) throw new Error('Turn already ended');
this._currentStep = new ObservationStep(this, stepNumber);
return this._currentStep;
}
/**
* Finalize the turn: save any remaining messages and return the current cached record.
*
* When async observation buffering is enabled and there are unobserved messages,
* a background buffer operation is kicked off so that observations are computed
* proactively while the agent is idle, rather than waiting for the next turn.
* The returned record does not wait for that background buffering pass to finish.
*/
async end(): Promise<TurnResult> {
if (this._ended) throw new Error('Turn already ended');
this._ended = true;
// Save any unsaved messages from the last stepView on GitHub (pinned to 75dd419e61)
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()
Example fix
// before
if (!turn) turn = new ObservationTurn(...);
turn.step(stepNumber);
// after
if (!turn || turnEnded) {
turn = new ObservationTurn(...);
turn.start();
turnEnded = false;
}
const step = turn.step(stepNumber);
// ... at end of loop:
await turn.end();
turnEnded = true; Defensive patterns
Strategy: validation
Validate before calling
if (!turn || turnIsEnded) {
turn = createNewTurn();
turn.start();
}
const step = turn.step(stepNumber); Type guard
function turnIsActive(turn: ObservationTurn | null | undefined): turn is ObservationTurn {
return !!turn && !turn.isEnded();
} Try / catch
try {
const step = turn.step(stepNumber);
} catch (err) {
if (err instanceof Error && err.message === 'Turn already ended') {
turn = createNewTurn();
turn.start();
} else throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- MastraAuthBetterAuth is not initialized — init() must run fi
- Shared browser not launched. Call createSharedSession() firs
- Browser not launched
- App deletion failed: ${data.error}
- SlackProvider not attached to Mastra. Call __attach() first.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/7460a106f5f510d9.
Report an issue: GitHub.