{"record":{"id":"3b920d730cd22ffd","repo":"paperclipai/paperclip","slug":"paperclip-runner-chat-attachment-read-busy","errorCode":"paperclip_runner_chat_attachment_read_busy","errorMessage":"paperclip_runner_chat_attachment_read_busy: chat authorization is temporarily busy; retry this read shortly","messagePattern":"paperclip_runner_chat_attachment_read_busy: chat authorization is temporarily busy; retry this read shortly","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"server/src/services/native-runtime/chat-attachment-read.ts","lineNumber":113,"sourceCode":"\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();\n      try {\n        return await this.#authorizeOnce(input);\n      } catch (error) {\n        this.#assertOpen();\n        if (!isExternalChatWaitAuthorizationContention(error)) throw error;\n        const remaining = deadline - Date.now();\n        if (remaining <= 0) {\n          throw new Error(\n            \"paperclip_runner_chat_attachment_read_busy: chat authorization is temporarily busy; retry this read shortly\",\n          );\n        }\n        await delay(Math.min(50, remaining), undefined, {\n          signal: this.#abort.signal,\n        }).catch(() => this.#assertOpen());\n      }\n    }\n  }\n\n  async #authorizeOnce(input: {\n    sourceCommentId: string;\n    attachmentId: string;\n  }): Promise<ChatAttachmentReuseSource> {\n    return this.options.db.transaction(async (transaction) => {\n      const tx = transaction as unknown as Db;\n      // Source rows are also locked by the existing lineage reader. Bound\n      // their waits so an inverse source-writer lock order cannot deadlock.","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-read.ts#L95-L131","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the read after a short delay — the error message explicitly says it is temporary.","Reduce long-running transactions that lock heartbeat_runs or chat-lineage rows.","Investigate lock contention (pg_locks / slow query logs) if this recurs.","Lower the frequency of concurrent attachment reads per run."],"exampleFix":"// before\nconst file = await scope.read(input); // may throw read_busy\n// after\ntry {\n  const file = await scope.read(input);\n} catch (e) {\n  if (String(e.message).startsWith(\"paperclip_runner_chat_attachment_read_busy\"))\n    await new Promise(r => setTimeout(r, 250));\n  return scope.read(input);\n  throw e;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isBusyError(e: unknown): boolean {\n  return e instanceof Error && e.message.startsWith(\"paperclip_runner_chat_attachment_read_busy\");\n}","tryCatchPattern":"try {\n  return await scope.read(input);\n} catch (e) {\n  if (isBusyError(e)) {\n    await new Promise(r => setTimeout(r, 500));\n    return scope.read(input); // single retry; error is explicitly transient\n  }\n  throw e;\n}","preventionTips":["Keep transactions touching heartbeat_runs short to avoid lock contention.","Schedule heavy event flushes away from interactive tool calls.","Monitor pg_locks for recurring contention on run rows.","Bound retries; the busy error already waits ~1s internally before surfacing."],"tags":["contention","lock-timeout","transient","retry"],"backgroundTag":"request-timeout","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"}