diff --git a/server/src/__tests__/decision-queues-routes.test.ts b/server/src/__tests__/decision-queues-routes.test.ts index 3dce271bcd3..1c056b771c3 100644 --- a/server/src/__tests__/decision-queues-routes.test.ts +++ b/server/src/__tests__/decision-queues-routes.test.ts @@ -396,6 +396,38 @@ describeEmbeddedPostgres("decision queue routes", () => { expect(current[0]?.setByUserId).toBe("override-user"); }); + it("serializes concurrent partial triage updates without losing state or reusing a version", async () => { + const { companyId, issueId } = await seed(); + const board = boardActor(companyId); + const [decideByResult, snoozeResult] = await Promise.all([ + request(app(board)) + .put(`/api/companies/${companyId}/decision-triage/review/${issueId}`) + .send({ decideBy: "today" }), + request(app(board)) + .put(`/api/companies/${companyId}/decision-triage/review/${issueId}`) + .send({ snoozedUntil: "2026-08-03T12:00:00.000Z" }), + ]); + + expect(decideByResult.status).toBe(200); + expect(snoozeResult.status).toBe(200); + expect([decideByResult.body.version, snoozeResult.body.version].sort()).toEqual([1, 2]); + + const current = await request(app(board)) + .get(`/api/companies/${companyId}/decision-triage/review/${issueId}`) + .expect(200); + expect(current.body).toMatchObject({ + decideBy: "today", + snoozedUntil: "2026-08-03T12:00:00.000Z", + version: 2, + }); + const events = await db.select().from(decisionTriageEvents).where(and( + eq(decisionTriageEvents.companyId, companyId), + eq(decisionTriageEvents.sourceKind, "review"), + eq(decisionTriageEvents.sourceId, issueId), + )); + expect(events.map((event) => event.details.version).sort()).toEqual([1, 2]); + }); + it("rejects task-bridge JWTs and returns the same 404 for missing and unauthorized sources", async () => { const { companyId, agentId, approvalId } = await seed(); const bridge = { diff --git a/server/src/services/decision-queues.ts b/server/src/services/decision-queues.ts index e4af931716f..47398e12128 100644 --- a/server/src/services/decision-queues.ts +++ b/server/src/services/decision-queues.ts @@ -597,6 +597,8 @@ export function decisionQueueService(db: Db) { await requireSourceRead(db, input.authActor, input.companyId, input.sourceKind, input.sourceId); return db.transaction(async (tx) => { const txDb = tx as unknown as Db; + const lockKey = `decision-triage:${input.companyId}:${input.sourceKind}:${input.sourceId}`; + await txDb.execute(sql`select pg_advisory_xact_lock(hashtextextended(${lockKey}, 0))`); const current = await txDb.select().from(decisionTriage).where(and( eq(decisionTriage.companyId, input.companyId), eq(decisionTriage.sourceKind, input.sourceKind),