paperclipai/paperclip · error

paperclip_runner_chat_attachment_read_limit

paperclip_runner_chat_attachment_read_limit

Error message

paperclip_runner_chat_attachment_read_limit

What it means

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.

Solutions

  1. Have the agent read only the attachments it actually needs per turn; metadata from list_chat_attachments usually suffices for filtering.
  2. Open a fresh scope (new run/binding) if more than 20 genuine reads are required.
  3. Detect this error and stop retrying; it is deterministic, not transient.
  4. Cache previously read attachment results instead of re-reading the same ids.

Example fix

// before
for (const att of allAttachments) await scope.read(att); // >20 throws
// after
const subset = allAttachments.slice(0, 20); // or per-turn selection
for (const att of subset) await scope.read(att);
Defensive patterns

Strategy: try-catch

Validate before calling

if (scope.readCountEstimate >= 20) throw new Error("attachment read budget exhausted for this run");

Try / catch

try {
  const file = await scope.read(input);
} catch (e) {
  if (e.message === "paperclip_runner_chat_attachment_read_limit") {
    return { status: "read_budget_exhausted" }; // do not retry
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: Loopy agent behavior re-reading files; broad batch scripts reading every attachment in a chat; long multi-turn sessions sharing one scope.

Related errors


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

Appendix: source

Thrown at server/src/services/native-runtime/chat-attachment-read.ts:87

    this.#assertOpen();
    if (
      !UUID.test(input.sourceCommentId) ||
      !UUID.test(input.attachmentId) ||
      Object.keys(input).some(
        (key) => key !== "sourceCommentId" && key !== "attachmentId",
      )
    ) {
      throw new Error(
        "paperclip_runner_chat_attachment_read_arguments_invalid",
      );
    }
    if (this.options.executionTargetKind !== "local") {
      throw new Error(
        "paperclip_runner_chat_attachment_remote_staging_unsupported",
      );
    }
    if (++this.#readCount > 20)
      throw new Error("paperclip_runner_chat_attachment_read_limit");
    const pending = this.#read(input);
    this.#pending.add(pending);
    void pending
      .finally(() => this.#pending.delete(pending))
      .catch(() => undefined);
    return pending;
  }

  async #authorized(input: {
    sourceCommentId: string;
    attachmentId: string;
  }): Promise<ChatAttachmentReuseSource> {
    // Run-event persistence also briefly locks heartbeat_runs. A NOWAIT miss
    // is not evidence of policy revocation: retry the whole authorization in
    // a fresh transaction, never hold partial locks while backing off.
    const deadline = Date.now() + 1_000;
    for (;;) {
      this.#assertOpen();

View on GitHub (pinned to 3f1d897a7c)