vercel/ai · error

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

Error message

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

What it means

The claude-code harness session exposes a doDetach operation on each created session. The library tracks a `stopped` flag per session and throws this error when detach is called on a session that has already been stopped (by doStop or doSuspendTurn, or after a prior detach tore things down), because detaching a stopped session has no channel left to suspend and would corrupt state. It is a guard against double teardown of the same session.

Source

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

      return control;
    },
    doCompact: async (customInstructions?: string) => {
      /*
       * Claude Code has no SDK/control method for compaction — the supported
       * trigger is the `/compact` slash command submitted as user input. Ride
       * the existing user-message rail; the bridge feeds it into the streaming
       * query input and Claude's native compaction handles the rest, emitting a
       * `compact_boundary` + `PostCompact` we observe as a `compaction` event.
       */
      const text =
        customInstructions && customInstructions.trim()
          ? `/compact ${customInstructions.trim()}`
          : '/compact';
      channel.send({ type: 'user-message', text });
    },
    doDetach: async () => {
      if (stopped) {
        throw new Error(
          `claude-code session ${sessionId} is already stopped; cannot detach.`,
        );
      }
      stopped = true;
      const lastSeenEventId = await channel.suspend();
      const payload: HarnessV1ResumeSessionState = {
        type: 'resume-session',
        harnessId: 'claude-code',
        specificationVersion: 'harness-v1',
        data: {
          ...(sandboxCredentialEnvironment == null
            ? {}
            : { sandboxCredentialEnvironment }),
          bridge: {
            port: bridgePort,
            token: bridgeToken,
            lastSeenEventId,
            ...(sandboxId == null ? {} : { sandboxId }),

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Check the session's stopped state before calling doDetach and skip the call if it is already stopped
  2. Wrap doDetach in try-catch and treat this error as a no-op idempotent outcome rather than a failure
  3. Restructure code so teardown happens in exactly one place (single owner / AbortSignal-driven cleanup) instead of multiple call sites
  4. If you need to keep working after a stop, create a new session via createClaudeCode instead of reusing the stopped one

Example fix

// before
await session.doDetach();
await session.doDetach(); // throws: already stopped; cannot detach

// after
let detached = false;
async function detachOnce() {
  if (detached) return;
  detached = true;
  await session.doDetach();
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Track lifecycle locally before calling
if (sessionStopped.has(sessionId)) {
  throw new Error(`skip detach: session ${sessionId} already stopped`);
}

Type guard

function canDetach(session: { stopped: boolean } | unknown): session is { stopped: false } {
  return typeof session === 'object' && session !== null &&
    (session as { stopped?: boolean }).stopped !== true;
}

Try / catch

try {
  await session.doDetach();
} catch (err) {
  if (err instanceof Error && err.message.includes('already stopped; cannot detach')) {
    return; // idempotent: already torn down
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling session.doDetach() after the same session was already stopped: a second doDetach, a doDetach after doStop, or a doDetach after doSuspendTurn (which also sets stopped = true).

Common situations: Race conditions where two parts of an app both call detach (e.g. an abort handler plus a cleanup path in a finally block); calling detach on a cached session object that a previous request already stopped; resuming work with an old session handle that was torn down earlier.

Related errors


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