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

  1. Make the Send button idempotent: disable it while a request is in flight and ignore duplicate responses.
  2. On CONFLICT, re-fetch the thread state and decide whether the message was already sent; if so, treat as success.
  3. 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

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.