vercel/ai · error
claude-code session ${sessionId} is already stopped; cannot
Error message
claude-code session ${sessionId} is already stopped; cannot stop. What it means
Each claude-code harness session keeps a `stopped` flag that is set once by doStop (and by doDetach/doSuspendTurn). doStop throws this error when called on a session that is already stopped, since stopping twice has no meaningful effect and the underlying bridge channel is already being torn down. It protects the stop machinery from double-execution.
Source
Thrown at packages/harness-claude-code/src/claude-code-harness.ts:1957
new Promise<void>(resolve => {
stopTimer = setTimeout(resolve, 5000);
stopTimer.unref?.();
}),
]);
}
} finally {
if (stopTimer) clearTimeout(stopTimer);
try {
await proc?.kill();
} catch {}
channel.close();
}
})();
return stopPromise;
},
doStop: async () => {
if (stopped) {
throw new Error(
`claude-code session ${sessionId} is already stopped; cannot stop.`,
);
}
stopped = true;
/*
* If the bridge's channel already closed (e.g. mid-turn WS drop)
* there is no one to acknowledge a `stop` message. Synthesize an empty
* payload — for Claude Code the resume state structurally is `{}`
* (the conversation lives in the workdir, captured by the sandbox
* snapshot during the subsequent `sandboxSession.stop()`), so we
* lose nothing by skipping the round-trip.
*/
// Tell the channel we are tearing down so the bridge's post-stop
// socket close finalises instead of triggering a reconnect.
channel.beginClose();
const data: unknown = channel.isClosed()
? {}
: await new Promise<unknown>((resolve, reject) => {View on GitHub (pinned to 69428b1f8b)
Solutions
- Guard doStop with a local boolean or promise so it runs only once per session
- Catch this error and ignore it — an already-stopped session satisfies the goal of stopping
- Check that the session handle you hold is still the active one; obtain a fresh session from createClaudeCode if the old one was stopped
- Deduplicate concurrent stop paths (abort handler vs explicit stop) behind a single teardown function
Example fix
// before
async function stop(session) {
await session.doStop();
}
// called from both abort handler and finally -> second call throws
// after
async function stop(session) {
if (session.stopped) return; // or track a local stopPromise
await session.doStop();
} Defensive patterns
Strategy: try-catch
Validate before calling
let stopRequested = false;
function requestStop(session) {
if (stopRequested) return Promise.resolve();
stopRequested = true;
return session.doStop();
} Type guard
function isAlreadyStoppedError(err: unknown): boolean {
return err instanceof Error && /already stopped; cannot stop\.$/.test(err.message);
} Try / catch
try {
await session.doStop();
} catch (err) {
if (!isAlreadyStoppedError(err)) throw err;
// already stopped: treat as success
} Prevention
- Memoize the stop call: store the doStop promise and reuse it for all stop requests
- Unify abort-signal and explicit-stop paths through one stopOnce helper
- Never reuse a session handle after it has been stopped; create a new one via createClaudeCode
- Log-and-ignore the already-stopped error instead of surfacing it to users
When it happens
Trigger: Calling session.doStop() twice on the same session, calling doStop after doDetach or doSuspendTurn already set stopped = true, or concurrent doStop calls racing where the second arrives after the first set the flag.
Common situations: Cleanup code invoked both by an abort signal and a finally block; a UI stop button pressed twice while the first stop is in flight; reusing a pooled or cached session that a previous turn already stopped.
Related errors
- claude-code session ${sessionId} is already stopped; cannot
- claude-code session ${sessionId} is stopped; cannot suspend.
- ${harnessId} ACP session ${sessionId} is stopped; cannot sus
- ${harnessId} ACP session ${sessionId} has no in-flight turn
- ${harnessId} ACP session ${sessionId} is stopped; cannot det
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/6fb86707a529cb1d.
Report an issue: GitHub.