{"record":{"id":"6fb86707a529cb1d","repo":"vercel/ai","slug":"claude-code-session-sessionid-is-already-stoppe-6fb867","errorCode":null,"errorMessage":"claude-code session ${sessionId} is already stopped; cannot stop.","messagePattern":"claude-code session (.+?) is already stopped; cannot stop\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/harness-claude-code/src/claude-code-harness.ts","lineNumber":1957,"sourceCode":"              new Promise<void>(resolve => {\n                stopTimer = setTimeout(resolve, 5000);\n                stopTimer.unref?.();\n              }),\n            ]);\n          }\n        } finally {\n          if (stopTimer) clearTimeout(stopTimer);\n          try {\n            await proc?.kill();\n          } catch {}\n          channel.close();\n        }\n      })();\n      return stopPromise;\n    },\n    doStop: async () => {\n      if (stopped) {\n        throw new Error(\n          `claude-code session ${sessionId} is already stopped; cannot stop.`,\n        );\n      }\n      stopped = true;\n      /*\n       * If the bridge's channel already closed (e.g. mid-turn WS drop)\n       * there is no one to acknowledge a `stop` message. Synthesize an empty\n       * payload — for Claude Code the resume state structurally is `{}`\n       * (the conversation lives in the workdir, captured by the sandbox\n       * snapshot during the subsequent `sandboxSession.stop()`), so we\n       * lose nothing by skipping the round-trip.\n       */\n      // Tell the channel we are tearing down so the bridge's post-stop\n      // socket close finalises instead of triggering a reconnect.\n      channel.beginClose();\n      const data: unknown = channel.isClosed()\n        ? {}\n        : await new Promise<unknown>((resolve, reject) => {","sourceCodeStart":1939,"sourceCodeEnd":1975,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness-claude-code/src/claude-code-harness.ts#L1939-L1975","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nasync function stop(session) {\n  await session.doStop();\n}\n// called from both abort handler and finally -> second call throws\n\n// after\nasync function stop(session) {\n  if (session.stopped) return; // or track a local stopPromise\n  await session.doStop();\n}","handlingStrategy":"try-catch","validationCode":"let stopRequested = false;\nfunction requestStop(session) {\n  if (stopRequested) return Promise.resolve();\n  stopRequested = true;\n  return session.doStop();\n}","typeGuard":"function isAlreadyStoppedError(err: unknown): boolean {\n  return err instanceof Error && /already stopped; cannot stop\\.$/.test(err.message);\n}","tryCatchPattern":"try {\n  await session.doStop();\n} catch (err) {\n  if (!isAlreadyStoppedError(err)) throw err;\n  // already stopped: treat as success\n}","preventionTips":["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"],"tags":["session-lifecycle","claude-code","harness","double-stop"],"backgroundTag":"session-already-stopped","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}