vercel/ai · error

Harness session ${this.sessionId} has no unfinished turn to

Error message

Harness session ${this.sessionId} has no unfinished turn to suspend.

What it means

Thrown by `HarnessAgentSession.suspendTurn()` when the session is active but its turn state is 'idle', meaning there is no in-flight, awaiting, or suspended turn to freeze into a continuation payload. Suspend only makes sense while a turn is unfinished; with no turn running there is nothing to capture, so the library refuses instead of returning a meaningless state. Call `suspendTurn()` only after a prompt/continue turn has started and not yet finished.

Source

Thrown at packages/harness/src/agent/harness-agent-session.ts:552

   * Gracefully freeze the active turn at the slice boundary and return the
   * continuation payload, **leaving the sandbox/runtime running** so the next
   * process can continue. Resolves once the in-flight `stream()` /
   * `continueStream()` has cleanly wound down at a precise cursor (see
   * `doSuspendTurn`).
   *
   * After this call the session is detached. This in-process handle no
   * longer drives turns; a future slice creates a fresh session from the
   * returned state. The sandbox is **not** stopped because bridge-backed
   * adapters may still have a live bridge.
   */
  async suspendTurn(): Promise<HarnessAgentContinueTurnState> {
    if (this.sessionState !== 'active' || this.underlyingSession == null) {
      throw new Error(
        `Harness session ${this.sessionId} is not active and cannot be suspended.`,
      );
    }
    if (this.turnState === 'idle') {
      throw new Error(
        `Harness session ${this.sessionId} has no unfinished turn to suspend.`,
      );
    }
    const session = this.underlyingSession;
    try {
      return await this.suspendCurrentTurn({ session });
    } finally {
      this.endLocalHandle({ sessionState: 'detached' });
    }
  }

  private getPendingToolApprovals(): readonly HarnessAgentPendingToolApproval[] {
    return Array.from(this.pendingToolApprovals.values());
  }

  private getPendingToolResults(): readonly HarnessAgentPendingToolResult[] {
    return Array.from(this.pendingToolResults.values());
  }

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Check `session.hasUnfinishedTurn()` before calling `suspendTurn()` and skip suspension when it returns false.
  2. Track your own turn lifecycle: only call suspendTurn between starting a prompt/continue turn and observing its `ready`/completion resolution.
  3. If the goal is just to end the session with no turn running, call `stop()` or `destroy()` instead of `suspendTurn()`.
  4. Guard against double-suspension: after a successful suspendTurn the handle is detached; do not reuse the same session object.

Example fix

// before
const state = await session.suspendTurn();

// after
if (session.hasUnfinishedTurn()) {
  const state = await session.suspendTurn();
} else {
  await session.stop(); // nothing to suspend; just end the session
}
Defensive patterns

Strategy: validation

Validate before calling

if (!session.hasUnfinishedTurn()) {
  throw new SkipSuspensionError(); // nothing to suspend; use stop()/destroy() instead
}

Try / catch

try {
  const state = await session.suspendTurn();
} catch (error) {
  if (error instanceof Error && /has no unfinished turn to suspend/.test(error.message)) {
    await session.stop(); // treat as already-settled
  } else {
    throw error;
  }
}

Prevention

When it happens

Trigger: Calling `session.suspendTurn()` (a) before ever calling `promptTurn()`, (b) after the previous turn already completed normally (onTurnFinished reset turnState to 'idle'), (c) after a turn failed (onTurnFailed also resets to idle), or (d) calling `suspendTurn()` twice — the first call sets sessionState to 'detached', and although the second would hit the not-active check first, a race where turnState was reset to idle while still 'active' also yields this error.

Common situations: Orchestrators that suspend a session at a slice boundary unconditionally, without checking `hasUnfinishedTurn()`; retry logic that re-invokes suspendTurn after the turn already ended; race conditions where the model finished the turn between the caller's check and the suspend call; confusing suspend with stop/destroy on an idle session.

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


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/47eb73459213795c. Report an issue: GitHub.