{"record":{"id":"47eb73459213795c","repo":"vercel/ai","slug":"harness-session-this-sessionid-has-no-unfinishe","errorCode":null,"errorMessage":"Harness session ${this.sessionId} has no unfinished turn to suspend.","messagePattern":"Harness session (.+?) has no unfinished turn to suspend\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/harness/src/agent/harness-agent-session.ts","lineNumber":552,"sourceCode":"   * Gracefully freeze the active turn at the slice boundary and return the\n   * continuation payload, **leaving the sandbox/runtime running** so the next\n   * process can continue. Resolves once the in-flight `stream()` /\n   * `continueStream()` has cleanly wound down at a precise cursor (see\n   * `doSuspendTurn`).\n   *\n   * After this call the session is detached. This in-process handle no\n   * longer drives turns; a future slice creates a fresh session from the\n   * returned state. The sandbox is **not** stopped because bridge-backed\n   * adapters may still have a live bridge.\n   */\n  async suspendTurn(): Promise<HarnessAgentContinueTurnState> {\n    if (this.sessionState !== 'active' || this.underlyingSession == null) {\n      throw new Error(\n        `Harness session ${this.sessionId} is not active and cannot be suspended.`,\n      );\n    }\n    if (this.turnState === 'idle') {\n      throw new Error(\n        `Harness session ${this.sessionId} has no unfinished turn to suspend.`,\n      );\n    }\n    const session = this.underlyingSession;\n    try {\n      return await this.suspendCurrentTurn({ session });\n    } finally {\n      this.endLocalHandle({ sessionState: 'detached' });\n    }\n  }\n\n  private getPendingToolApprovals(): readonly HarnessAgentPendingToolApproval[] {\n    return Array.from(this.pendingToolApprovals.values());\n  }\n\n  private getPendingToolResults(): readonly HarnessAgentPendingToolResult[] {\n    return Array.from(this.pendingToolResults.values());\n  }","sourceCodeStart":534,"sourceCodeEnd":570,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness/src/agent/harness-agent-session.ts#L534-L570","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst state = await session.suspendTurn();\n\n// after\nif (session.hasUnfinishedTurn()) {\n  const state = await session.suspendTurn();\n} else {\n  await session.stop(); // nothing to suspend; just end the session\n}","handlingStrategy":"validation","validationCode":"if (!session.hasUnfinishedTurn()) {\n  throw new SkipSuspensionError(); // nothing to suspend; use stop()/destroy() instead\n}","typeGuard":null,"tryCatchPattern":"try {\n  const state = await session.suspendTurn();\n} catch (error) {\n  if (error instanceof Error && /has no unfinished turn to suspend/.test(error.message)) {\n    await session.stop(); // treat as already-settled\n  } else {\n    throw error;\n  }\n}","preventionTips":["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)."],"tags":["lifecycle","state-machine","harness","invalid-operation"],"backgroundTag":"invalid-state-transition","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}