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
- Check `session.hasUnfinishedTurn()` before calling `suspendTurn()` and skip suspension when it returns false.
- Track your own turn lifecycle: only call suspendTurn between starting a prompt/continue turn and observing its `ready`/completion resolution.
- If the goal is just to end the session with no turn running, call `stop()` or `destroy()` instead of `suspendTurn()`.
- 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
- Always gate suspendTurn behind `session.hasUnfinishedTurn()`.
- Only suspend between turn start and turn completion; never on a fresh or finished session.
- Remember suspendTurn detaches the handle — never call it twice on the same session.
- Distinguish suspend (freeze unfinished turn) from stop/destroy (end an idle session).
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
- `Harness session ${this.sessionId} has no unfinished turn to
- `Harness session ${this.sessionId} already has a turn in pro
- `Harness session ${this.sessionId} has an unfinished turn an
- Invalid argument for parameter requests: requests must not b
- Invalid argument for parameter requests: request IDs must no
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/47eb73459213795c.
Report an issue: GitHub.