{"record":{"id":"218c3749701cec1d","repo":"vercel/ai","slug":"claude-code-session-sessionid-is-stopped-canno","errorCode":null,"errorMessage":"claude-code session ${sessionId} is stopped; cannot suspend.","messagePattern":"claude-code session (.+?) is stopped; cannot suspend\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/harness-claude-code/src/claude-code-harness.ts","lineNumber":2046,"sourceCode":"        specificationVersion: 'harness-v1',\n        // The bridge's stop reply carries the session id it observed; the\n        // adapter's own record backfills it when the reply predates the\n        // field or the channel was already closed.\n        data: {\n          ...(lastClaudeSessionId\n            ? { claudeSessionId: lastClaudeSessionId }\n            : {}),\n          ...lifecycleData,\n          ...(sandboxCredentialEnvironment == null\n            ? {}\n            : { sandboxCredentialEnvironment }),\n        } as HarnessV1ResumeSessionState['data'],\n      };\n      return payload;\n    },\n    doSuspendTurn: async () => {\n      if (stopped) {\n        throw new Error(\n          `claude-code session ${sessionId} is stopped; cannot suspend.`,\n        );\n      }\n      stopped = true;\n      /*\n       * Freeze the host at a precise cursor without stopping the active model\n       * turn. `channel.suspend` stops processing inbound frames, drains what\n       * was already dispatched, then closes the host socket with reason\n       * `'suspended'`. The bridge keeps the turn running and accumulates events\n       * past the cursor for the next slice to replay. The sandbox process is\n       * deliberately left alive.\n       */\n      const lastSeenEventId = await channel.suspend();\n      const payload: HarnessV1ContinueTurnState = {\n        type: 'continue-turn',\n        harnessId: 'claude-code',\n        specificationVersion: 'harness-v1',\n        data: {","sourceCodeStart":2028,"sourceCodeEnd":2064,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness-claude-code/src/claude-code-harness.ts#L2028-L2064","documentation":"doSuspendTurn freezes the host at a precise event cursor so the turn can later be resumed. Because suspension also marks the session stopped, calling it on a session whose `stopped` flag is already set (from a prior suspend, stop, or detach) throws this error — there is no live channel cursor left to freeze.","triggerScenarios":"Calling session.doSuspendTurn() after the session was already stopped via a previous doSuspendTurn, doStop, or doDetach; or after the session was suspended and its state handed off for resume.","commonSituations":"An app that suspends on both a shutdown hook and an explicit pause action; retrying a suspend after a failure elsewhere in the shutdown path; attempting to suspend a session that was restored from a previous suspend operation (the old handle remains stopped — use the resumed session).","solutions":["Only call doSuspendTurn once per session lifecycle; track suspension with a local flag or promise","Catch this error and treat it as success if your goal was already achieved by the earlier suspend/stop","If the session was suspended and resumed, operate on the new session handle returned by the resume flow, not the original one","If you intended to end the session rather than pause it, call doStop instead"],"exampleFix":"// before\nawait session.doSuspendTurn();\nawait session.doSuspendTurn(); // throws: stopped; cannot suspend\n\n// after\nlet suspended = false;\nasync function suspendOnce(session) {\n  if (suspended) return;\n  suspended = true;\n  return session.doSuspendTurn();\n}","handlingStrategy":"try-catch","validationCode":"const suspendedSessions = new Set<string>();\nfunction canSuspend(sessionId: string): boolean {\n  return !suspendedSessions.has(sessionId);\n}","typeGuard":"function isStoppedSessionError(err: unknown): boolean {\n  return err instanceof Error && err.message.includes('is stopped; cannot suspend');\n}","tryCatchPattern":"try {\n  await session.doSuspendTurn();\n} catch (err) {\n  if (!isStoppedSessionError(err)) throw err;\n  // already suspended/stopped: nothing to do\n}","preventionTips":["Suspend exactly once per session: guard with a promise or flag","After resuming, use the new session handle; never re-suspend with the pre-resume handle","Choose deliberately between doStop (terminate) and doSuspendTurn (pause) — they both set stopped","Add shutdown hooks that check a suspended-set before suspending again"],"tags":["session-lifecycle","claude-code","harness","suspend"],"backgroundTag":"session-already-stopped","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}