fix(decisions): serialize triage updates

Lock each company/source triage identity before reading current state so concurrent partial updates preserve both fields and emit monotonic history versions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-01 17:13:08 +00:00
1 parent 2b0b3b0634
commit 8a40f5ba61
2 files changed
+34

No files matched your search

@@ -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 = {
+2
View File
@@ -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),