{"record":{"id":"1f01593cd22f273c","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-read-limit","errorCode":"paperclip_runner_chat_attachment_read_limit","errorMessage":"paperclip_runner_chat_attachment_read_limit","messagePattern":"paperclip_runner_chat_attachment_read_limit","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/chat-attachment-read.ts","lineNumber":87,"sourceCode":"    this.#assertOpen();\n    if (\n      !UUID.test(input.sourceCommentId) ||\n      !UUID.test(input.attachmentId) ||\n      Object.keys(input).some(\n        (key) => key !== \"sourceCommentId\" && key !== \"attachmentId\",\n      )\n    ) {\n      throw new Error(\n        \"paperclip_runner_chat_attachment_read_arguments_invalid\",\n      );\n    }\n    if (this.options.executionTargetKind !== \"local\") {\n      throw new Error(\n        \"paperclip_runner_chat_attachment_remote_staging_unsupported\",\n      );\n    }\n    if (++this.#readCount > 20)\n      throw new Error(\"paperclip_runner_chat_attachment_read_limit\");\n    const pending = this.#read(input);\n    this.#pending.add(pending);\n    void pending\n      .finally(() => this.#pending.delete(pending))\n      .catch(() => undefined);\n    return pending;\n  }\n\n  async #authorized(input: {\n    sourceCommentId: string;\n    attachmentId: string;\n  }): Promise<ChatAttachmentReuseSource> {\n    // Run-event persistence also briefly locks heartbeat_runs. A NOWAIT miss\n    // is not evidence of policy revocation: retry the whole authorization in\n    // a fresh transaction, never hold partial locks while backing off.\n    const deadline = Date.now() + 1_000;\n    for (;;) {\n      this.#assertOpen();","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-read.ts#L69-L105","documentation":"Each scope allows at most 20 attachment reads; a per-scope counter increments on every read() call and this error is thrown once the 21st is attempted. The cap bounds storage traffic and staged-file cleanup work per run, preventing an agent loop from hammering attachments indefinitely.","triggerScenarios":"An agent repeatedly calling read_chat_attachment more than 20 times within one run/scope, e.g. a retry loop or a model re-reading the same attachments every turn of a long conversation.","commonSituations":"Loopy agent behavior re-reading files; broad batch scripts reading every attachment in a chat; long multi-turn sessions sharing one scope.","solutions":["Have the agent read only the attachments it actually needs per turn; metadata from list_chat_attachments usually suffices for filtering.","Open a fresh scope (new run/binding) if more than 20 genuine reads are required.","Detect this error and stop retrying; it is deterministic, not transient.","Cache previously read attachment results instead of re-reading the same ids."],"exampleFix":"// before\nfor (const att of allAttachments) await scope.read(att); // >20 throws\n// after\nconst subset = allAttachments.slice(0, 20); // or per-turn selection\nfor (const att of subset) await scope.read(att);","handlingStrategy":"try-catch","validationCode":"if (scope.readCountEstimate >= 20) throw new Error(\"attachment read budget exhausted for this run\");","typeGuard":null,"tryCatchPattern":"try {\n  const file = await scope.read(input);\n} catch (e) {\n  if (e.message === \"paperclip_runner_chat_attachment_read_limit\") {\n    return { status: \"read_budget_exhausted\" }; // do not retry\n  }\n  throw e;\n}","preventionTips":["Budget reads per run; prefer metadata for triage and read only chosen files.","Cache read results by (sourceCommentId, attachmentId).","Break read loops when the same ids repeat.","Start a new run/scope if a workflow legitimately needs >20 reads."],"tags":["rate-limit","quota","read-limit"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}