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
- Inspect the ACPX provider stream for duplicated tool-call frames within one turn and report the upstream protocol bug.
- Ensure call ids are unique per turn; do not reuse a callId after a partial frame.
- 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)