{"record":{"id":"d9bd8c3a31b59c85","repo":"vercel/ai","slug":"harnessid-acp-session-sessionid-has-an-in-fl","errorCode":null,"errorMessage":"${harnessId} ACP session ${sessionId} has an in-flight turn; suspend it instead.","messagePattern":"(.+?) ACP session (.+?) has an in-flight turn; suspend it instead\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/harness-acp/src/v1/acp-v1-harness.ts","lineNumber":1621,"sourceCode":"        data: createLifecycleData({\n          bridge: {\n            port: bridgePort,\n            token: bridgeToken,\n            lastSeenEventId,\n            ...(sandboxId == null ? {} : { sandboxId }),\n            stateDir: bridgeStateDir,\n          },\n        }),\n      };\n    },\n    doDetach: async () => {\n      if (stopped) {\n        throw new Error(\n          `${harnessId} ACP session ${sessionId} is stopped; cannot detach.`,\n        );\n      }\n      if (turnInFlight) {\n        throw new Error(\n          `${harnessId} ACP session ${sessionId} has an in-flight turn; suspend it instead.`,\n        );\n      }\n      stopped = true;\n      const lastSeenEventId = await channel.suspend();\n      return {\n        type: 'resume-session',\n        harnessId,\n        specificationVersion: 'harness-v1',\n        data: createLifecycleData({\n          bridge: {\n            port: bridgePort,\n            token: bridgeToken,\n            lastSeenEventId,\n            ...(sandboxId == null ? {} : { sandboxId }),\n            stateDir: bridgeStateDir,\n          },\n        }),","sourceCodeStart":1603,"sourceCodeEnd":1639,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness-acp/src/v1/acp-v1-harness.ts#L1603-L1639","documentation":"Detach requires an idle session. If a turn is currently in flight, doDetach refuses to tear down the attachment because the in-flight turn would be lost; the caller must suspend instead, which captures the turn state and returns a continue-turn handle for later resumption.","triggerScenarios":"Calling detach while a promptTurn is actively streaming (turnInFlight=true) — e.g. attempting to detach mid-turn to migrate the session, or a cleanup routine firing during an active turn.","commonSituations":"App shutdown while a long turn is still running; trying to hand the session to another process mid-turn via detach; a timeout handler detaching while the model is still generating.","solutions":["Call suspendTurn instead of detach while a turn is in flight; it returns a continue-turn handle to resume the turn later.","Wait for the current turn to finish (done event) before detaching.","Abort the in-flight turn first, then detach once turnInFlight is false.","If migrating mid-turn is the goal, design around suspend + resume rather than detach."],"exampleFix":"// before\nawait session.detach(); // throws: turn in flight\n// after\nconst handle = await session.suspendTurn(); // continue-turn handle\n// later: resume and continue the turn","handlingStrategy":"validation","validationCode":"if (turnInFlight) {\n  const handle = await session.suspendTurn(); // continue-turn handle\n} else {\n  const handle = await session.detach(); // resume-session handle\n}","typeGuard":null,"tryCatchPattern":"try {\n  await session.detach();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('in-flight turn; suspend it instead')) {\n    await session.suspendTurn();\n  } else throw e;\n}","preventionTips":["Never detach during an active turn; wait for done or abort first","Route mid-turn shutdown through suspendTurn","Defer cleanup callbacks until the turn stream completes"],"tags":["acp","detach","turn-in-flight","suspend"],"backgroundTag":"turn-in-flight-detach-blocked","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}