paperclipai/paperclip · warning

paperclip_runner_chat_attachment_read_busy

paperclip_runner_chat_attachment_read_busy

Error message

paperclip_runner_chat_attachment_read_busy: chat authorization is temporarily busy; retry this read shortly

What it means

Authorization of the external-chat wait/reuse happens inside a transaction with a 50ms lock_timeout; when authorization is contended by another writer (e.g. run-event persistence locking heartbeat_runs), the scope retries with short backoff for up to ~1 second. If the deadline expires while contention persists, it throws this busy error telling the caller to retry shortly.

Solutions

  1. Retry the read after a short delay — the error message explicitly says it is temporary.
  2. Reduce long-running transactions that lock heartbeat_runs or chat-lineage rows.
  3. Investigate lock contention (pg_locks / slow query logs) if this recurs.
  4. Lower the frequency of concurrent attachment reads per run.

Example fix

// before
const file = await scope.read(input); // may throw read_busy
// after
try {
  const file = await scope.read(input);
} catch (e) {
  if (String(e.message).startsWith("paperclip_runner_chat_attachment_read_busy"))
    await new Promise(r => setTimeout(r, 250));
  return scope.read(input);
  throw e;
}
Defensive patterns

Strategy: retry

Type guard

function isBusyError(e: unknown): boolean {
  return e instanceof Error && e.message.startsWith("paperclip_runner_chat_attachment_read_busy");
}

Try / catch

try {
  return await scope.read(input);
} catch (e) {
  if (isBusyError(e)) {
    await new Promise(r => setTimeout(r, 500));
    return scope.read(input); // single retry; error is explicitly transient
  }
  throw e;
}

Prevention

When it happens

Trigger: Concurrent transactions holding locks the authorization needs for more than ~1s total; many simultaneous reads/writes against the same run rows during a busy window; lock convoy where run-event persistence repeatedly blocks authorizeOnce.

Common situations: High-concurrency boards where several runs and event writers contend on the same company/endpoint rows; slow analytics queries holding locks; batch event flushes coinciding with attachment reads.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


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

Appendix: source

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

  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();
      try {
        return await this.#authorizeOnce(input);
      } catch (error) {
        this.#assertOpen();
        if (!isExternalChatWaitAuthorizationContention(error)) throw error;
        const remaining = deadline - Date.now();
        if (remaining <= 0) {
          throw new Error(
            "paperclip_runner_chat_attachment_read_busy: chat authorization is temporarily busy; retry this read shortly",
          );
        }
        await delay(Math.min(50, remaining), undefined, {
          signal: this.#abort.signal,
        }).catch(() => this.#assertOpen());
      }
    }
  }

  async #authorizeOnce(input: {
    sourceCommentId: string;
    attachmentId: string;
  }): Promise<ChatAttachmentReuseSource> {
    return this.options.db.transaction(async (transaction) => {
      const tx = transaction as unknown as Db;
      // Source rows are also locked by the existing lineage reader. Bound
      // their waits so an inverse source-writer lock order cannot deadlock.

View on GitHub (pinned to 3f1d897a7c)