{"record":{"id":"aae6f72fb777c7ec","repo":"paperclipai/paperclip","slug":"acpx-session-is-not-at-a-safe-suspension-point","errorCode":null,"errorMessage":"ACPX session is not at a safe suspension point","messagePattern":"ACPX session is not at a safe suspension point","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts","lineNumber":475,"sourceCode":"    const action = objective\n      ? \"set\"\n      : status === \"paused\"\n        ? \"pause\"\n        : status === \"active\"\n          ? \"resume\"\n          : null;\n    if (!action) throw new Error(\"session.goal.set requires an objective or active/paused status\");\n    const goal = await activeHost.controlGoal(action, objective || undefined);\n    return observedGoalProjection(activeHost.goalCapability(), goal, turnId !== null);\n  }\n  if (request.command === \"session.goal.clear\") {\n    const activeHost = requireHost();\n    await activeHost.controlGoal(\"clear\");\n    return observedGoalProjection(activeHost.goalCapability(), null, turnId !== null);\n  }\n  if (request.command === \"session.suspend\") {\n    if (turnId || tools.size > 0 || inputs.size > 0) {\n      throw new Error(\"ACPX session is not at a safe suspension point\");\n    }\n    // Cleanup retries must be able to reach the retained host. The command is\n    // still serialized, and retainActiveHostCleanup keeps admission closed\n    // until one sequential close proves ownership was released.\n    const activeHost = requireHost({ allowCleanupRetry: true });\n    const identity = acpxProviderSessionIdentity(\n      activeHost.identity(),\n      activeHost.binding(),\n    );\n    await closeSidecarHostForCommand(\n      activeHost,\n      boundedOptionalText(request.params.reason, \"Paperclip suspension\", 4_000),\n      undefined,\n      (cleanup) => retainActiveHostCleanup(activeHost, cleanup),\n    );\n    host = null;\n    openParams = null;\n    runId = null;","sourceCodeStart":457,"sourceCodeEnd":493,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts#L457-L493","documentation":"session.suspend can only run when the session is quiescent: no turn in flight (turnId null) and no outstanding tools or inputs. The sidecar refuses suspension at any other point to avoid tearing down a host mid-execution.","triggerScenarios":"Dispatching session.suspend while a turn is active (turnId set), while tools are registered, or while inputs are pending (tools.size > 0 or inputs.size > 0).","commonSituations":"Cleanup/suspend triggered by a timeout while an agent turn is still streaming; a supervisor suspending on shutdown without first cancelling the turn; double-suspend racing an in-flight request.","solutions":["Wait for the current turn to complete (turnId becomes null) before suspending","Cancel or drain pending tools/inputs first, then retry session.suspend","Use the cleanup-retry path: requireHost({ allowCleanupRetry: true }) still serializes and retains the host until close completes"],"exampleFix":"// before\nawait host.dispatch({ command: 'session.suspend' }); // mid-turn\n// after\nawait host.waitUntilIdle();\nawait host.dispatch({ command: 'session.suspend' });","handlingStrategy":"validation","validationCode":"function isSafeSuspensionPoint(state) { return state.turnId === null && state.tools.size === 0 && state.inputs.size === 0; }\nif (isSafeSuspensionPoint(hostState)) await host.dispatch({ command: 'session.suspend' });","typeGuard":"function canSuspend(s: { turnId: string | null; tools: Set<unknown>; inputs: Set<unknown> }): boolean {\n  return s.turnId === null && s.tools.size === 0 && s.inputs.size === 0;\n}","tryCatchPattern":"try { await host.dispatch({ command: 'session.suspend' }); }\ncatch (e) { if (String(e.message).includes('safe suspension point')) { await host.cancelTurn(); await waitForIdle(host); await host.dispatch({ command: 'session.suspend' }); } else throw e; }","preventionTips":["Subscribe to turn/idle events and suspend only from an idle handler","Cancel in-flight turns before shutdown paths that suspend","Avoid racing suspend with turn submission; serialize through the command channel"],"tags":["acp","state","suspension"],"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-14T00:17:10.932Z"}