{"record":{"id":"cca903371f5e7c75","repo":"amruthpillai/reactive-resume","slug":"conflict","errorCode":"CONFLICT","errorMessage":"One or more attachments were already linked to another message.","messagePattern":"One or more attachments were already linked to another message\\.","errorType":"http","errorClass":"ORPCError","httpStatus":409,"severity":"error","filePath":"packages/api/src/features/agent/service.ts","lineNumber":402,"sourceCode":"}) {\n\tif (input.attachments.length === 0) return;\n\n\tconst ids = input.attachments.map((attachment) => attachment.id);\n\tconst linked = await db\n\t\t.update(schema.agentAttachment)\n\t\t.set({ messageId: input.messageId })\n\t\t.where(\n\t\t\tand(\n\t\t\t\teq(schema.agentAttachment.threadId, input.threadId),\n\t\t\t\teq(schema.agentAttachment.userId, input.userId),\n\t\t\t\tinArray(schema.agentAttachment.id, ids),\n\t\t\t\tisNull(schema.agentAttachment.messageId),\n\t\t\t),\n\t\t)\n\t\t.returning({ id: schema.agentAttachment.id });\n\n\tif (linked.length !== ids.length) {\n\t\tthrow new ORPCError(\"CONFLICT\", { message: \"One or more attachments were already linked to another message.\" });\n\t}\n}\n\nfunction readAttachmentModelInputs(attachments: AgentAttachmentRecord[]): Promise<AttachmentModelInput[]> {\n\tconst storage = getStorageService();\n\treturn Promise.all(\n\t\tattachments.map(async (attachment) => {\n\t\t\tconst stored = await storage.read(attachment.storageKey);\n\t\t\tif (!stored) {\n\t\t\t\tthrow new ORPCError(\"BAD_REQUEST\", { message: `Attachment ${attachment.filename} could not be read.` });\n\t\t\t}\n\n\t\t\treturn { attachment, data: stored.data };\n\t\t}),\n\t);\n}\n\nfunction attachModelPartsToLatestUserMessage(","sourceCodeStart":384,"sourceCodeEnd":420,"githubUrl":"https://github.com/amruthpillai/reactive-resume/blob/3a5b12e2a40374a9571988701fcb75c5a1831c42/packages/api/src/features/agent/service.ts#L384-L420","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nbutton.onclick = () => send(...);\n\n// after\nlet sending = false;\nbutton.onclick = async () => {\n  if (sending) return;\n  sending = true;\n  try {\n    await send(...);\n  } catch (err) {\n    if (err.code === 'CONFLICT' && /already linked/.test(err.message)) return; // already sent\n    throw err;\n  } finally {\n    sending = false;\n  }\n};","handlingStrategy":"try-catch","validationCode":"let sending = false;\nasync function sendOnce(input) {\n  if (sending) return;\n  sending = true;\n  try { await orpc.agent.messages.send(input); }\n  finally { sending = false; }\n}","typeGuard":null,"tryCatchPattern":"try { await orpc.agent.messages.send(input); }\ncatch (err) {\n  if (err instanceof ORPCError && err.code === 'CONFLICT' && /already linked/i.test(err.message)) {\n    // A concurrent send already linked these attachments; treat as success.\n    return;\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["agent","attachments","race-condition","concurrency","conflict","orpc"],"backgroundTag":null,"analyzedSha":"3a5b12e2a40374a9571988701fcb75c5a1831c42","analyzedAt":"2026-08-12T22:31:22.666Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}