vercel/ai · error · Error

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

Error message

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

What it means

doSuspendTurn operates on the live session's channel; once the session is stopped (via stop/destroy/terminateBridge or a prior suspend/detach), the channel is closing or closed and suspension is impossible. The harness throws to prevent suspending an already-terminated ACP session identified by sessionId.

Source

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

            turnStartConfig,
            recoveryMode: {
              type: 'lossy-rerun',
              acpSessionId: latestACPSessionId!,
              reason: recoveryStatus?.reason ?? 'bridge process loss',
            },
          });
        },
      });
    },
    doCompact: async () => {
      throw unsupported({
        harnessId,
        message: 'ACP v1 does not define manual session compaction.',
      });
    },
    doSuspendTurn: async () => {
      if (stopped) {
        throw new Error(
          `${harnessId} ACP session ${sessionId} is stopped; cannot suspend.`,
        );
      }
      if (!turnInFlight) {
        throw new Error(
          `${harnessId} ACP session ${sessionId} has no in-flight turn to suspend.`,
        );
      }
      stopped = true;
      const lastSeenEventId = await channel.suspend();
      return {
        type: 'continue-turn',
        harnessId,
        specificationVersion: 'harness-v1',
        data: createLifecycleData({
          bridge: {
            port: bridgePort,
            token: bridgeToken,

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Track session lifecycle and only call suspendTurn while the session is active (not stopped).
  2. If the turn must survive shutdown, suspend it before calling stop/destroy on the session.
  3. If already suspended/detached, use the returned lifecycle data to resume (continue-turn/resume-session) instead of suspending again.
  4. Guard the suspend call with a local 'stopped' flag set when you stop the session.

Example fix

// before
await session.stop();
await session.suspendTurn(); // throws: session stopped
// after
await session.suspendTurn(); // suspend first
await session.stop();
Defensive patterns

Strategy: validation

Validate before calling

let stopped = false;
async function safeSuspend(session) {
  if (stopped) throw new Error('session already stopped');
  return session.suspendTurn();
}

Try / catch

try {
  await session.suspendTurn();
} catch (e) {
  if (e instanceof Error && e.message.includes('is stopped; cannot suspend')) {
    // use previously returned lifecycle data to resume instead
  } else throw e;
}

Prevention

When it happens

Trigger: Calling suspendTurn on a session that was already stopped — after stop/destroy was issued, after a prior suspendTurn or detach set stopped=true, or on a session restored as stopped.

Common situations: Double-suspending after a race; calling suspend after the app already terminated the bridge during shutdown; invoking suspend on a stale session handle kept after detach; shutdown handlers running twice.

Related errors


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