paperclipai/paperclip · warning · ExternalChatWaitAuthorizationContentionError
ExternalChatWaitAuthorizationContentionError
Error message
ExternalChatWaitAuthorizationContentionError
What it means
Thrown by principalAuthorized when running in 'nonblocking' lock mode and the pg_try_advisory_xact_lock for the chat-identity key (companyId:principalId) cannot be acquired, meaning another transaction is concurrently authorizing or mutating the same external chat principal. The error (message 'paperclip_external_chat_wait_authorization_contended') signals lock contention rather than authorization failure; use isExternalChatWaitAuthorizationContention() to detect it (it also matches PG 55P03 lock_not_available).
Solutions
- Detect contention with isExternalChatWaitAuthorizationContention(err) and retry the authorization after a short backoff — contention is transient.
- Fall back to lockMode 'blocking' (pg_advisory_xact_lock) when the caller can afford to wait for the concurrent transaction to finish.
- Reduce lock hold time: keep transactions using this lock short and avoid slow I/O inside them.
- Serialize per-principal work (per-principal queue or deduplication) so concurrent requests for the same chat principal are rare.
Example fix
// before
const ok = await authorizeChatConversationForBoundRun(tx, endpoint, principalId, "nonblocking");
// after
let ok: boolean;
try {
ok = await authorizeChatConversationForBoundRun(tx, endpoint, principalId, "nonblocking");
} catch (err) {
if (!isExternalChatWaitAuthorizationContention(err)) throw err;
ok = await authorizeChatConversationForBoundRun(tx, endpoint, principalId, "blocking"); // wait out the contention
} Defensive patterns
Strategy: retry
Type guard
function isLockContention(err: unknown): boolean {
return isExternalChatWaitAuthorizationContention(err);
} Try / catch
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await authorizeChatConversationForBoundRun(tx, endpoint, principalId, "nonblocking");
} catch (err) {
if (!isExternalChatWaitAuthorizationContention(err) || attempt === 2) throw err;
await new Promise(r => setTimeout(r, 50 * 2 ** attempt));
}
} Prevention
- Keep transactions that take the chat-identity advisory lock short — no external I/O inside them.
- Deduplicate webhooks per principal before processing to avoid self-contention.
- Use blocking lock mode wherever waiting is acceptable; reserve nonblocking for latency-sensitive paths with retry.
- Alert on repeated contention for the same principal — it usually indicates a stuck long transaction or retry storm.
When it happens
Trigger: authorizeChatConversationForBoundRun invoked with lockMode 'nonblocking' while a concurrent transaction already holds the advisory xact lock or row locks (FOR UPDATE NOWAIT) for the same endpoint company/principal — e.g. simultaneous webhook deliveries or a wait-authorization check racing a delivery-processing transaction.
Common situations: Duplicate/rapid-fire chat platform webhooks for the same user; a long-running delivery-processing transaction holding the lock; retry storms after a slow provider response; multiple worker instances processing the same principal concurrently.
Related errors
- Cannot seed target embedded PostgreSQL at
- run-dispatch: run changed issue context repeatedly while…
- ACPX provider ownership admission is closed
- ACPX runtime host already has an active turn
- Bridge envelope changed while reading.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/bd4560fa51f32202.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/native-runtime/chat-attachment-reuse.ts:338
});
}
async function principalAuthorized(
tx: Db,
endpoint: typeof chatEndpoints.$inferSelect,
principalId: string,
lockMode: AuthorizationLockMode = "blocking",
): Promise<boolean> {
if (lockMode === "blocking") {
await tx.execute(
sql`select pg_advisory_xact_lock(hashtextextended(${`chat-identity:${endpoint.companyId}:${principalId}`}, 0))`,
);
} else if (lockMode === "nonblocking") {
const [lock] = (await tx.execute(
sql`select pg_try_advisory_xact_lock(hashtextextended(${`chat-identity:${endpoint.companyId}:${principalId}`}, 0)) as acquired`,
)) as unknown as Array<{ acquired: boolean }>;
if (!lock?.acquired) {
throw new ExternalChatWaitAuthorizationContentionError();
}
}
const principalQuery = tx
.select({ id: chatExternalPrincipals.id })
.from(chatExternalPrincipals)
.where(
and(
eq(chatExternalPrincipals.id, principalId),
eq(chatExternalPrincipals.companyId, endpoint.companyId),
eq(chatExternalPrincipals.provider, endpoint.provider),
eq(
chatExternalPrincipals.providerAccountId,
endpoint.providerAccountId ?? "",
),
),
);
const [principal] = await (lockMode === "read"
? principalQuery.limit(1)View on GitHub (pinned to 3f1d897a7c)