paperclipai/paperclip · error
ACPX session is not at a safe suspension point
Error message
ACPX session is not at a safe suspension point
What it means
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.
Source
Thrown at packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts:475
const action = objective
? "set"
: status === "paused"
? "pause"
: status === "active"
? "resume"
: null;
if (!action) throw new Error("session.goal.set requires an objective or active/paused status");
const goal = await activeHost.controlGoal(action, objective || undefined);
return observedGoalProjection(activeHost.goalCapability(), goal, turnId !== null);
}
if (request.command === "session.goal.clear") {
const activeHost = requireHost();
await activeHost.controlGoal("clear");
return observedGoalProjection(activeHost.goalCapability(), null, turnId !== null);
}
if (request.command === "session.suspend") {
if (turnId || tools.size > 0 || inputs.size > 0) {
throw new Error("ACPX session is not at a safe suspension point");
}
// Cleanup retries must be able to reach the retained host. The command is
// still serialized, and retainActiveHostCleanup keeps admission closed
// until one sequential close proves ownership was released.
const activeHost = requireHost({ allowCleanupRetry: true });
const identity = acpxProviderSessionIdentity(
activeHost.identity(),
activeHost.binding(),
);
await closeSidecarHostForCommand(
activeHost,
boundedOptionalText(request.params.reason, "Paperclip suspension", 4_000),
undefined,
(cleanup) => retainActiveHostCleanup(activeHost, cleanup),
);
host = null;
openParams = null;
runId = null;View on GitHub (pinned to 01ad858492)
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
Example fix
// before
await host.dispatch({ command: 'session.suspend' }); // mid-turn
// after
await host.waitUntilIdle();
await host.dispatch({ command: 'session.suspend' }); Defensive patterns
Strategy: validation
Validate before calling
function isSafeSuspensionPoint(state) { return state.turnId === null && state.tools.size === 0 && state.inputs.size === 0; }
if (isSafeSuspensionPoint(hostState)) await host.dispatch({ command: 'session.suspend' }); Type guard
function canSuspend(s: { turnId: string | null; tools: Set<unknown>; inputs: Set<unknown> }): boolean {
return s.turnId === null && s.tools.size === 0 && s.inputs.size === 0;
} Try / catch
try { await host.dispatch({ command: 'session.suspend' }); }
catch (e) { if (String(e.message).includes('safe suspension point')) { await host.cancelTurn(); await waitForIdle(host); await host.dispatch({ command: 'session.suspend' }); } else throw e; } Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- OpenCode target executable is not a regular file
- The negotiated ACP goal extension does not support token bud
- session.goal.set requires an objective or active/paused stat
- OpenCode thread is not open
- Warm run transition binding is invalid.
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/aae6f72fb777c7ec.
Report an issue: GitHub.