paperclipai/paperclip · error

ACPX pending tool limit reached

Error message

ACPX pending tool limit reached

What it means

Resource-cap guard in the ACPX runtime sidecar's waitForTool: the number of tool calls pending within the current turn exceeded the sidecar's bounded pending-tool limit, so further calls are rejected rather than growing an unbounded waiter map. Fires when a single provider turn fans out more concurrent tool calls than the configured cap — a backstop against runaway or malformed provider turns.

Solutions

  1. Reduce the number of concurrent tool calls the agent issues per turn (serialize or batch them).
  2. If the workload legitimately needs more parallel calls, raise the pending-tool limit in the sidecar configuration.
  3. Check for a provider bug that leaks pending entries (calls that never settle) and drains the budget.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts:593 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/0ad9a5510c12bb8d. Report an issue: GitHub.

Appendix: source

Thrown at packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts:593

  } finally {
    rejectTurnWaiters(currentTurnId, "ACPX turn became terminal");
    if (turnId === currentTurnId) turnId = null;
  }
  // A terminal frame is also runnerd's permission to recycle this provider.
  // Publish it only after no callback can inherit this turn's mutable binding.
  emit("runtime.turn_terminal", terminal, currentTurnId);
}

async function waitForTool(call: RunnerToolCall): Promise<unknown> {
  const activeTurnId = turnId;
  if (!activeTurnId || call.signal.aborted) {
    throw new Error("ACPX tool call is not bound to an active turn");
  }
  const callId = boundedIdentity(call.callId, "callId");
  if (tools.has(callId)) throw new Error("ACPX tool call is duplicated");
  const operationId = boundedIdentity(call.tool, "operationId");
  if (tools.size >= MAX_PENDING_TOOLS) {
    throw new Error("ACPX pending tool limit reached");
  }
  if (
    operationId === PRP_COMPLETION_TOOL_NAME ||
    operationId === PRP_BLOCK_TOOL_NAME
  ) {
    const validation = validatePrpStructuredRunResult(call.arguments);
    if (!validation.ok) {
      throw new Error("ACPX semantic result failed PRP schema validation");
    }
    const blocked = validation.result.reportedWorkDisposition === "blocked";
    if (
      (operationId === PRP_BLOCK_TOOL_NAME && !blocked) ||
      (operationId === PRP_COMPLETION_TOOL_NAME && blocked)
    ) {
      throw new Error(
        "ACPX semantic result disposition does not match its terminal operation",
      );
    }

View on GitHub (pinned to 3f1d897a7c)