vercel/ai · error · Error

${harnessId} ACP session ${sessionId} is stopped; cannot det

Error message

${harnessId} ACP session ${sessionId} is stopped; cannot detach.

What it means

doDetach ends the session's attachment to the bridge and returns a resume-session handle; it is only valid while the session is still live. Once the session is stopped (stop/destroy issued or a prior suspend/detach), the bridge is being torn down and detach throws.

Source

Thrown at packages/harness-acp/src/v1/acp-v1-harness.ts:1616

      const lastSeenEventId = await channel.suspend();
      return {
        type: 'continue-turn',
        harnessId,
        specificationVersion: 'harness-v1',
        data: createLifecycleData({
          bridge: {
            port: bridgePort,
            token: bridgeToken,
            lastSeenEventId,
            ...(sandboxId == null ? {} : { sandboxId }),
            stateDir: bridgeStateDir,
          },
        }),
      };
    },
    doDetach: async () => {
      if (stopped) {
        throw new Error(
          `${harnessId} ACP session ${sessionId} is stopped; cannot detach.`,
        );
      }
      if (turnInFlight) {
        throw new Error(
          `${harnessId} ACP session ${sessionId} has an in-flight turn; suspend it instead.`,
        );
      }
      stopped = true;
      const lastSeenEventId = await channel.suspend();
      return {
        type: 'resume-session',
        harnessId,
        specificationVersion: 'harness-v1',
        data: createLifecycleData({
          bridge: {
            port: bridgePort,
            token: bridgeToken,

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Call detach exactly once, before any stop/destroy, and keep the returned resume-session handle for later resumption.
  2. Guard cleanup code with an idempotency flag so stop/detach are not both invoked.
  3. If the session is already stopped, discard the handle — there is nothing detachable left; use the earlier suspend/detach data to resume.
  4. Only resume from persisted lifecycle data instead of trying to detach a dead session.

Example fix

// before
await session.suspendTurn();
await session.detach(); // throws: session stopped
// after
const handle = await session.detach(); // detach directly when no turn is in flight
Defensive patterns

Strategy: validation

Validate before calling

let detachedOrStopped = false;
async function safeDetach(session) {
  if (detachedOrStopped) throw new Error('session no longer detachable');
  detachedOrStopped = true;
  return session.detach();
}

Try / catch

try {
  await session.detach();
} catch (e) {
  if (e instanceof Error && e.message.includes('is stopped; cannot detach')) {
    // reuse handle from the earlier suspend/detach call
  } else throw e;
}

Prevention

When it happens

Trigger: Calling detach on a session already stopped by stop/destroy, or after a previous suspendTurn/detach call set stopped=true.

Common situations: Shutdown handlers calling both suspend and then detach; calling detach on a stale session reference after the process was terminated; re-running cleanup logic twice (e.g. in both a signal handler and a finally block).

Related errors


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