{"record":{"id":"d744040003fa93c6","repo":"paperclipai/paperclip","slug":"acpx-runtime-host-already-has-an-active-turn","errorCode":null,"errorMessage":"ACPX runtime host already has an active turn","messagePattern":"ACPX runtime host already has an active turn","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/runtime-host.ts","lineNumber":607,"sourceCode":"  }\n\n  async controlGoal(\n    action: \"set\" | \"pause\" | \"resume\" | \"clear\",\n    objective?: string,\n  ): Promise<AcpxRuntimeGoalSnapshot | null> {\n    if (!this.#runtime.controlGoal) {\n      throw new Error(\"ACPX runtime does not expose session goal controls\");\n    }\n    return await this.#runtime.controlGoal(action, objective);\n  }\n\n  startTurn(input: AcpxRuntimeTurnInput): AcpxRuntimeTurn {\n    if (this.#closed || this.#closingStarted) {\n      throw new Error(\"ACPX runtime host is closing\");\n    }\n    if (this.#activeTurn) {\n      throw new Error(\"ACPX runtime host already has an active turn\");\n    }\n    const requestId = boundedRequestId(input.requestId);\n    const text = boundedTurnText(input.text);\n    const turn = this.#runtime.startTurn({\n      text,\n      requestId,\n      ...(input.signal ? { signal: input.signal } : {}),\n      ...(input.onElicitation ? { onElicitation: input.onElicitation } : {}),\n    });\n    this.#activeTurn = turn;\n    void turn.result\n      .finally(() => {\n        // Once shutdown owns this turn, retain its cancellation handle until\n        // runtime cleanup succeeds. The result may settle while cleanup is\n        // failing, and a later close must still be able to retry cancellation.\n        if (this.#activeTurn === turn && !this.#closingStarted) {\n          this.#activeTurn = null;\n        }\n      })","sourceCodeStart":589,"sourceCodeEnd":625,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/runtime-host.ts#L589-L625","documentation":"The ACPX runtime host enforces one active turn at a time. startTurn() throws this error when a turn is already running (#activeTurn is set), preventing interleaved or concurrent turns on the same host.","triggerScenarios":"Calling startTurn() while a previous turn has not completed; concurrent callers issuing turns in parallel; a stuck/never-completing active turn blocking all subsequent turns.","commonSituations":"UI double-submits a prompt; an event handler fires while a prior turn is in flight; a crashed turn never cleared #activeTurn so all later turns fail.","solutions":["Serialize turns: await the previous turn's completion before calling startTurn","Maintain a turn queue that submits the next turn only when the host is idle","If a turn is stuck, abort it (via its signal) or close and reopen the host to clear #activeTurn","Guard concurrent callers with a mutex/promise chain around startTurn"],"exampleFix":"// before\nhost.startTurn({ text: \"a\" });\nhost.startTurn({ text: \"b\" }); // throws\n// after\nawait chain = chain.then(() => runTurn(host, \"b\"));\nasync function runTurn(h, text) {\n  const turn = h.startTurn({ text });\n  await turn.done;\n}","handlingStrategy":"validation","validationCode":"if (host.hasActiveTurn?.()) throw new Error(\"Wait for current turn\");","typeGuard":"function isIdle(host) {\n  return !host.hasActiveTurn?.();\n}","tryCatchPattern":"let chain = Promise.resolve();\ntry {\n  chain = chain.then(() => host.startTurn({ text }));\n} catch (err) {\n  if (err.message.includes(\"already has an active turn\")) {\n    await currentTurnDone;\n    return host.startTurn({ text });\n  }\n  throw err;\n}","preventionTips":["Serialize all startTurn calls behind a promise chain or queue","Await the returned turn's completion before the next turn","Abort stuck turns via their AbortSignal to free the active slot","Debounce/duplicate-suppress UI submissions"],"tags":["acpx","runtime","concurrency","active-turn"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}