amruthpillai/reactive-resume · error · ORPCError
CONFLICT
CONFLICT
Error message
One or more attachments were already linked to another message.
What it means
CONFLICT raised in linkAttachmentsToMessage after an UPDATE ... WHERE id IN ids AND messageId IS NULL returned fewer linked rows than ids.length. Unlike the read-time check (error 7/8), this is the write-time guard: between the SELECT and the UPDATE, another message linked one of the attachments (or it was deleted), so the atomic conditional update silently skipped it.
Source
Thrown at packages/api/src/features/agent/service.ts:402
}) {
if (input.attachments.length === 0) return;
const ids = input.attachments.map((attachment) => attachment.id);
const linked = await db
.update(schema.agentAttachment)
.set({ messageId: input.messageId })
.where(
and(
eq(schema.agentAttachment.threadId, input.threadId),
eq(schema.agentAttachment.userId, input.userId),
inArray(schema.agentAttachment.id, ids),
isNull(schema.agentAttachment.messageId),
),
)
.returning({ id: schema.agentAttachment.id });
if (linked.length !== ids.length) {
throw new ORPCError("CONFLICT", { message: "One or more attachments were already linked to another message." });
}
}
function readAttachmentModelInputs(attachments: AgentAttachmentRecord[]): Promise<AttachmentModelInput[]> {
const storage = getStorageService();
return Promise.all(
attachments.map(async (attachment) => {
const stored = await storage.read(attachment.storageKey);
if (!stored) {
throw new ORPCError("BAD_REQUEST", { message: `Attachment ${attachment.filename} could not be read.` });
}
return { attachment, data: stored.data };
}),
);
}
function attachModelPartsToLatestUserMessage(View on GitHub (pinned to 3a5b12e2a4)
Solutions
- Make the Send button idempotent: disable it while a request is in flight and ignore duplicate responses.
- On CONFLICT, re-fetch the thread state and decide whether the message was already sent; if so, treat as success.
- Server-side, hold a per-thread advisory lock during send to serialize link attempts.
Example fix
// before
button.onclick = () => send(...);
// after
let sending = false;
button.onclick = async () => {
if (sending) return;
sending = true;
try {
await send(...);
} catch (err) {
if (err.code === 'CONFLICT' && /already linked/.test(err.message)) return; // already sent
throw err;
} finally {
sending = false;
}
}; Defensive patterns
Strategy: try-catch
Validate before calling
let sending = false;
async function sendOnce(input) {
if (sending) return;
sending = true;
try { await orpc.agent.messages.send(input); }
finally { sending = false; }
} Try / catch
try { await orpc.agent.messages.send(input); }
catch (err) {
if (err instanceof ORPCError && err.code === 'CONFLICT' && /already linked/i.test(err.message)) {
// A concurrent send already linked these attachments; treat as success.
return;
}
throw err;
} Prevention
- Single-flight the Send button to prevent duplicate concurrent requests.
- After a timeout-induced retry, re-fetch the thread to see if the message actually went through.
- Use idempotency keys if you add them to the API later.
When it happens
Trigger: Two concurrent send-message calls on the same thread each tried to link the same unlinked attachment; a retry of a send that already partially succeeded; an attachment was linked by a different message between read and write.
Common situations: Double-click on Send that fires two requests; client retry after a timeout where the first request actually succeeded; multiple browser tabs on the same thread.
Related errors
AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12).
Data as JSON: /api/errors/cca903371f5e7c75.
Report an issue: GitHub.