Skip to content

Commit 7b205fa

Browse files
committed
fix(assistant-actions): read a run that closes mid-review as nothing to review
1 parent 7625929 commit 7b205fa

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

‎apps/sim/lib/assistant-actions/application/review.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ interface ReviewInput {
3838
runId: string
3939
}
4040

41+
const NOTHING_TO_REVIEW = { runId: null, streamId: null, actions: [], nextCursor: null }
42+
4143
export const listOrganizationAssistantActions = defineAuthorizedOrganizationUseCase({
4244
operation: assistantActionOperations.list,
4345
async execute({
@@ -64,9 +66,16 @@ export const listOrganizationAssistantActions = defineAuthorizedOrganizationUseC
6466
)
6567
.orderBy(desc(copilotRuns.startedAt), desc(copilotRuns.id))
6668
.limit(1)
67-
if (!latest) return { runId: null, streamId: null, actions: [], nextCursor: null }
69+
if (!latest) return NOTHING_TO_REVIEW
6870
const runId = latest.id
69-
const run = await requireAssistantRun(principal, { ...input, runId }, context.userId)
71+
const run = await requireAssistantRun(principal, { ...input, runId }, context.userId).catch(
72+
(error: unknown) => {
73+
// The lookup above takes no lock, so the run can close before this one is granted.
74+
if (error instanceof OrchestrationError && error.code === 'conflict') return null
75+
throw error
76+
}
77+
)
78+
if (!run) return NOTHING_TO_REVIEW
7079
const records = await db
7180
.select()
7281
.from(copilotAsyncToolCalls)

‎apps/sim/lib/assistant-actions/tool-permissions.integration.ts‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ import {
2525
} from '@sim/db/schema'
2626
import { readTestRedisUrl } from '@sim/db/testing/test-infrastructure'
2727
import { sha256Hex } from '@sim/security/hash'
28+
import { createDeferred } from '@sim/testing/helpers/deferred'
2829
import { generateId } from '@sim/utils/id'
2930
import { toArray, toRecord } from '@sim/utils/object'
30-
import { eq, inArray } from 'drizzle-orm'
31+
import { eq, inArray, sql } from 'drizzle-orm'
3132
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
3233
import {
3334
claimOrganizationAssistantAction,
@@ -639,6 +640,56 @@ describe('organization Assistant tool permissions', () => {
639640
}
640641
)
641642

643+
it('reads a run that closes during the review read as nothing to review', async () => {
644+
await change(0, 'ask')
645+
await prepare()
646+
const held = createDeferred<number>()
647+
const release = createDeferred<void>()
648+
const closing = db.transaction(async (tx) => {
649+
const [blocker] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`)
650+
if (!blocker) throw new Error('Missing closing transaction')
651+
await tx.update(copilotChats).set({ conversationId: null }).where(eq(copilotChats.id, chatId))
652+
await tx
653+
.update(copilotRuns)
654+
.set({ status: 'complete', toolAdmissionClosedAt: new Date() })
655+
.where(eq(copilotRuns.id, runId))
656+
held.resolve(blocker.pid)
657+
await release.promise
658+
})
659+
void closing.catch((error: unknown) => held.reject(error))
660+
try {
661+
const blockerPid = await held.promise
662+
const reading = review()
663+
void reading.catch(() => undefined)
664+
await vi.waitFor(
665+
async () => {
666+
const [waiting] = await db.execute<{ pid: number }>(sql`
667+
SELECT pid FROM pg_stat_activity
668+
WHERE datname = current_database()
669+
AND ${blockerPid}::int = ANY(pg_blocking_pids(pid))
670+
`)
671+
expect(waiting).toBeDefined()
672+
},
673+
{ timeout: 10_000, interval: 25 }
674+
)
675+
release.resolve()
676+
await closing
677+
expect(await reading).toEqual({
678+
runId: null,
679+
streamId: null,
680+
actions: [],
681+
nextCursor: null,
682+
})
683+
} finally {
684+
release.resolve()
685+
await Promise.allSettled([closing])
686+
await db
687+
.update(copilotChats)
688+
.set({ conversationId: streamId })
689+
.where(eq(copilotChats.id, chatId))
690+
}
691+
})
692+
642693
it('pages eligible proposals past expired rows and keeps canonical turn metadata', async () => {
643694
await change(0, 'ask')
644695
const expired = []

0 commit comments

Comments
 (0)