vercel/ai · error

claude-code session ${sessionId} is stopped; cannot suspend.

Error message

claude-code session ${sessionId} is stopped; cannot suspend.

What it means

doSuspendTurn freezes the host at a precise event cursor so the turn can later be resumed. Because suspension also marks the session stopped, calling it on a session whose `stopped` flag is already set (from a prior suspend, stop, or detach) throws this error — there is no live channel cursor left to freeze.

Source

Thrown at packages/harness-claude-code/src/claude-code-harness.ts:2046

        specificationVersion: 'harness-v1',
        // The bridge's stop reply carries the session id it observed; the
        // adapter's own record backfills it when the reply predates the
        // field or the channel was already closed.
        data: {
          ...(lastClaudeSessionId
            ? { claudeSessionId: lastClaudeSessionId }
            : {}),
          ...lifecycleData,
          ...(sandboxCredentialEnvironment == null
            ? {}
            : { sandboxCredentialEnvironment }),
        } as HarnessV1ResumeSessionState['data'],
      };
      return payload;
    },
    doSuspendTurn: async () => {
      if (stopped) {
        throw new Error(
          `claude-code session ${sessionId} is stopped; cannot suspend.`,
        );
      }
      stopped = true;
      /*
       * Freeze the host at a precise cursor without stopping the active model
       * turn. `channel.suspend` stops processing inbound frames, drains what
       * was already dispatched, then closes the host socket with reason
       * `'suspended'`. The bridge keeps the turn running and accumulates events
       * past the cursor for the next slice to replay. The sandbox process is
       * deliberately left alive.
       */
      const lastSeenEventId = await channel.suspend();
      const payload: HarnessV1ContinueTurnState = {
        type: 'continue-turn',
        harnessId: 'claude-code',
        specificationVersion: 'harness-v1',
        data: {

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Only call doSuspendTurn once per session lifecycle; track suspension with a local flag or promise
  2. Catch this error and treat it as success if your goal was already achieved by the earlier suspend/stop
  3. If the session was suspended and resumed, operate on the new session handle returned by the resume flow, not the original one
  4. If you intended to end the session rather than pause it, call doStop instead

Example fix

// before
await session.doSuspendTurn();
await session.doSuspendTurn(); // throws: stopped; cannot suspend

// after
let suspended = false;
async function suspendOnce(session) {
  if (suspended) return;
  suspended = true;
  return session.doSuspendTurn();
}
Defensive patterns

Strategy: try-catch

Validate before calling

const suspendedSessions = new Set<string>();
function canSuspend(sessionId: string): boolean {
  return !suspendedSessions.has(sessionId);
}

Type guard

function isStoppedSessionError(err: unknown): boolean {
  return err instanceof Error && err.message.includes('is stopped; cannot suspend');
}

Try / catch

try {
  await session.doSuspendTurn();
} catch (err) {
  if (!isStoppedSessionError(err)) throw err;
  // already suspended/stopped: nothing to do
}

Prevention

When it happens

Trigger: Calling session.doSuspendTurn() after the session was already stopped via a previous doSuspendTurn, doStop, or doDetach; or after the session was suspended and its state handed off for resume.

Common situations: An app that suspends on both a shutdown hook and an explicit pause action; retrying a suspend after a failure elsewhere in the shutdown path; attempting to suspend a session that was restored from a previous suspend operation (the old handle remains stopped — use the resumed session).

Related errors


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