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
- Call detach exactly once, before any stop/destroy, and keep the returned resume-session handle for later resumption.
- Guard cleanup code with an idempotency flag so stop/detach are not both invoked.
- If the session is already stopped, discard the handle — there is nothing detachable left; use the earlier suspend/detach data to resume.
- 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
- Detach exactly once per session, before any stop/destroy
- Centralize teardown in one idempotent cleanup function
- Keep the returned resume-session handle; never detach a stopped session to 'get' one
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
- ${harnessId} ACP session ${sessionId} is stopped; cannot sus
- ${harnessId} ACP session ${sessionId} has no in-flight turn
- ${harnessId} ACP session ${sessionId} has an in-flight turn;
- The ${model.provider} model "${model.modelId}" does not supp
- Invalid argument for parameter requests: requests must not b
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/191fb4c8c203386b.
Report an issue: GitHub.