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
- Check the session's stopped state before calling doDetach and skip the call if it is already stopped
- Wrap doDetach in try-catch and treat this error as a no-op idempotent outcome rather than a failure
- Restructure code so teardown happens in exactly one place (single owner / AbortSignal-driven cleanup) instead of multiple call sites
- 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
- Keep a single teardown function per session and call it from exactly one place
- Track stopped/detached state in a Set or WeakSet keyed by session id
- Make detach idempotent by swallowing the already-stopped error
- Avoid calling detach from both abort handlers and finally blocks on the same session
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
- 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/87936606e8aa20d9.
Report an issue: GitHub.