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

  1. Wait for the current turn to complete (turnId becomes null) before suspending
  2. Cancel or drain pending tools/inputs first, then retry session.suspend
  3. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/aae6f72fb777c7ec. Report an issue: GitHub.