{"record":{"id":"b962b4a6ba8d0b34","repo":"paperclipai/paperclip","slug":"acpx-tool-call-is-duplicated","errorCode":null,"errorMessage":"ACPX tool call is duplicated","messagePattern":"ACPX tool call is duplicated","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts","lineNumber":590,"sourceCode":"        retryable: false,\n      },\n    };\n  } finally {\n    rejectTurnWaiters(currentTurnId, \"ACPX turn became terminal\");\n    if (turnId === currentTurnId) turnId = null;\n  }\n  // A terminal frame is also runnerd's permission to recycle this provider.\n  // Publish it only after no callback can inherit this turn's mutable binding.\n  emit(\"runtime.turn_terminal\", terminal, currentTurnId);\n}\n\nasync function waitForTool(call: RunnerToolCall): Promise<unknown> {\n  const activeTurnId = turnId;\n  if (!activeTurnId || call.signal.aborted) {\n    throw new Error(\"ACPX tool call is not bound to an active turn\");\n  }\n  const callId = boundedIdentity(call.callId, \"callId\");\n  if (tools.has(callId)) throw new Error(\"ACPX tool call is duplicated\");\n  const operationId = boundedIdentity(call.tool, \"operationId\");\n  if (tools.size >= MAX_PENDING_TOOLS) {\n    throw new Error(\"ACPX pending tool limit reached\");\n  }\n  if (\n    operationId === PRP_COMPLETION_TOOL_NAME ||\n    operationId === PRP_BLOCK_TOOL_NAME\n  ) {\n    const validation = validatePrpStructuredRunResult(call.arguments);\n    if (!validation.ok) {\n      throw new Error(\"ACPX semantic result failed PRP schema validation\");\n    }\n    const blocked = validation.result.reportedWorkDisposition === \"blocked\";\n    if (\n      (operationId === PRP_BLOCK_TOOL_NAME && !blocked) ||\n      (operationId === PRP_COMPLETION_TOOL_NAME && blocked)\n    ) {\n      throw new Error(","sourceCodeStart":572,"sourceCodeEnd":608,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts#L572-L608","documentation":"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.","triggerScenarios":"Thrown at packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts:590 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}