paperclipai/paperclip · error

ACPX tool call is duplicated

Error message

ACPX tool call is duplicated

What it means

Duplicate-detection guard in the ACPX runtime sidecar's waitForTool: when a tool call arrives whose call id is already registered among the turn's pending tool calls, the sidecar rejects it as duplicated instead of double-binding a waiter to the same call. Fires when the ACPX provider re-emits a tool call frame (retry/echo) within the same active turn — treating it as a provider protocol violation keeps tool results one-to-one with calls.

Solutions

  1. Inspect the ACPX provider stream for duplicated tool-call frames within one turn and report the upstream protocol bug.
  2. Ensure call ids are unique per turn; do not reuse a callId after a partial frame.
  3. If a legitimate retry is needed, bind it as a new call with a fresh id rather than reusing the pending one.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts:590 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/b962b4a6ba8d0b34. Report an issue: GitHub.

Appendix: source

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

        retryable: false,
      },
    };
  } 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(

View on GitHub (pinned to 3f1d897a7c)