paperclipai/paperclip · error · Error

ACPX runtime host is closing

Error message

ACPX runtime host is closing

What it means

startTurn() on the ACPX runtime host is refused once the host has been closed or a close sequence has begun (#closed or #closingStarted). The host is single-lifetime: after close() no new turns may be started.

Source

Thrown at packages/paperclip-runner/src/drivers/acpx/runtime-host.ts:603

  }

  goalSnapshot(): AcpxRuntimeGoalSnapshot | null {
    return this.#runtime.goalSnapshot?.() ?? null;
  }

  async controlGoal(
    action: "set" | "pause" | "resume" | "clear",
    objective?: string,
  ): Promise<AcpxRuntimeGoalSnapshot | null> {
    if (!this.#runtime.controlGoal) {
      throw new Error("ACPX runtime does not expose session goal controls");
    }
    return await this.#runtime.controlGoal(action, objective);
  }

  startTurn(input: AcpxRuntimeTurnInput): AcpxRuntimeTurn {
    if (this.#closed || this.#closingStarted) {
      throw new Error("ACPX runtime host is closing");
    }
    if (this.#activeTurn) {
      throw new Error("ACPX runtime host already has an active turn");
    }
    const requestId = boundedRequestId(input.requestId);
    const text = boundedTurnText(input.text);
    const turn = this.#runtime.startTurn({
      text,
      requestId,
      ...(input.signal ? { signal: input.signal } : {}),
      ...(input.onElicitation ? { onElicitation: input.onElicitation } : {}),
    });
    this.#activeTurn = turn;
    void turn.result
      .finally(() => {
        // Once shutdown owns this turn, retain its cancellation handle until
        // runtime cleanup succeeds. The result may settle while cleanup is
        // failing, and a later close must still be able to retry cancellation.

View on GitHub (pinned to 01ad858492)

Solutions

  1. Check the host's closed/closing state (or track it via close events) before calling startTurn
  2. Reopen a new runtime host via open() and submit the turn there
  3. Fix lifecycle management so no turn submissions are queued after close() is initiated
  4. Guard retries: on shutdown, drop pending turns instead of re-submitting

Example fix

// before
host.close();
host.startTurn({ text: "hi" }); // throws
// after
if (!host.isClosed && !host.isClosing) {
  host.startTurn({ text: "hi" });
} else {
  const fresh = await open({ /* same options */ });
  fresh.startTurn({ text: "hi" });
}
Defensive patterns

Strategy: try-catch

Validate before calling

function canStartTurn(host) {
  return !host.isClosed && !host.isClosing;
}

Type guard

function isOpenForTurns(host) {
  return "isClosed" in host && host.isClosed === false && host.isClosingStarted === false;
}

Try / catch

try {
  host.startTurn({ text });
} catch (err) {
  if (err.message === "ACPX runtime host is closing") {
    const fresh = await open(options);
    return fresh.startTurn({ text });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling startTurn() after close() was called, after an async close began (closingStarted set even if teardown is still in flight), or concurrently racing a close that started between an earlier availability check and the startTurn call.

Common situations: A request arrives while the session is shutting down; retry logic re-issues startTurn after the host already closed due to an earlier failure; lifecycle race where a scheduler keeps submitting turns to a closing host.

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/e5319b3c98a24e4b. Report an issue: GitHub.