{"record":{"id":"5295062f1cf3adf5","repo":"vercel/ai","slug":"harnessid-acp-session-sessionid-is-stopped","errorCode":null,"errorMessage":"${harnessId} ACP session ${sessionId} is stopped; cannot suspend.","messagePattern":"(.+?) ACP session (.+?) is stopped; cannot suspend\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/harness-acp/src/v1/acp-v1-harness.ts","lineNumber":1588,"sourceCode":"            turnStartConfig,\n            recoveryMode: {\n              type: 'lossy-rerun',\n              acpSessionId: latestACPSessionId!,\n              reason: recoveryStatus?.reason ?? 'bridge process loss',\n            },\n          });\n        },\n      });\n    },\n    doCompact: async () => {\n      throw unsupported({\n        harnessId,\n        message: 'ACP v1 does not define manual session compaction.',\n      });\n    },\n    doSuspendTurn: async () => {\n      if (stopped) {\n        throw new Error(\n          `${harnessId} ACP session ${sessionId} is stopped; cannot suspend.`,\n        );\n      }\n      if (!turnInFlight) {\n        throw new Error(\n          `${harnessId} ACP session ${sessionId} has no in-flight turn to suspend.`,\n        );\n      }\n      stopped = true;\n      const lastSeenEventId = await channel.suspend();\n      return {\n        type: 'continue-turn',\n        harnessId,\n        specificationVersion: 'harness-v1',\n        data: createLifecycleData({\n          bridge: {\n            port: bridgePort,\n            token: bridgeToken,","sourceCodeStart":1570,"sourceCodeEnd":1606,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness-acp/src/v1/acp-v1-harness.ts#L1570-L1606","documentation":"doSuspendTurn operates on the live session's channel; once the session is stopped (via stop/destroy/terminateBridge or a prior suspend/detach), the channel is closing or closed and suspension is impossible. The harness throws to prevent suspending an already-terminated ACP session identified by sessionId.","triggerScenarios":"Calling suspendTurn on a session that was already stopped — after stop/destroy was issued, after a prior suspendTurn or detach set stopped=true, or on a session restored as stopped.","commonSituations":"Double-suspending after a race; calling suspend after the app already terminated the bridge during shutdown; invoking suspend on a stale session handle kept after detach; shutdown handlers running twice.","solutions":["Track session lifecycle and only call suspendTurn while the session is active (not stopped).","If the turn must survive shutdown, suspend it before calling stop/destroy on the session.","If already suspended/detached, use the returned lifecycle data to resume (continue-turn/resume-session) instead of suspending again.","Guard the suspend call with a local 'stopped' flag set when you stop the session."],"exampleFix":"// before\nawait session.stop();\nawait session.suspendTurn(); // throws: session stopped\n// after\nawait session.suspendTurn(); // suspend first\nawait session.stop();","handlingStrategy":"validation","validationCode":"let stopped = false;\nasync function safeSuspend(session) {\n  if (stopped) throw new Error('session already stopped');\n  return session.suspendTurn();\n}","typeGuard":null,"tryCatchPattern":"try {\n  await session.suspendTurn();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('is stopped; cannot suspend')) {\n    // use previously returned lifecycle data to resume instead\n  } else throw e;\n}","preventionTips":["Suspend before stop/destroy in shutdown sequences","Make cleanup idempotent so suspend/stop are not called twice","Discard session handles after stop; use persisted lifecycle data for resumption"],"tags":["acp","session-lifecycle","suspend","stopped-session"],"backgroundTag":"session-already-stopped","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}