88 desktopDevices ,
99} from '@sim/db/schema'
1010import { sanitizeValueForJsonb } from '@sim/utils/string'
11- import { and , asc , eq , inArray , isNotNull , isNull , ne , or , sql } from 'drizzle-orm'
11+ import { and , asc , eq , inArray , isNotNull , isNull , ne , notInArray , or , sql } from 'drizzle-orm'
12+ import type { DbTransaction } from '@/lib/db/types'
1213import {
1314 DESKTOP_CALL_LEASE_SECONDS ,
1415 DESKTOP_INBOX_HORIZON_HOURS ,
@@ -485,8 +486,10 @@ export async function getDesktopCallState(toolCallId: string) {
485486 msUntilLeaseEnd : sql <
486487 number | null
487488 > `(extract(epoch from (${ copilotAsyncToolCalls . executionLeaseExpiresAt } - clock_timestamp())) * 1000)::float8` ,
489+ runOpen : sql < boolean > `${ copilotRuns . toolAdmissionClosedAt } IS NULL AND ${ copilotRuns . status } NOT IN ('complete', 'error', 'cancelled')` ,
488490 } )
489491 . from ( copilotAsyncToolCalls )
492+ . innerJoin ( copilotRuns , eq ( copilotRuns . id , copilotAsyncToolCalls . runId ) )
490493 . where ( eq ( copilotAsyncToolCalls . toolCallId , toolCallId ) )
491494 . limit ( 1 )
492495 return row ?? null
@@ -499,35 +502,61 @@ interface DesktopCallFailure {
499502 error : string
500503}
501504
505+ /**
506+ * Runs a settlement while holding the run row against Stop, the same serialization the claim and a
507+ * device result use. Nothing is settled on a stopped or ended run: Stop answers for its calls.
508+ */
509+ async function settleOnOpenRun (
510+ runId : string ,
511+ settle : ( tx : DbTransaction ) => Promise < boolean >
512+ ) : Promise < boolean > {
513+ return db . transaction ( async ( tx ) => {
514+ const [ run ] = await tx
515+ . select ( { id : copilotRuns . id } )
516+ . from ( copilotRuns )
517+ . where (
518+ and (
519+ eq ( copilotRuns . id , runId ) ,
520+ isNull ( copilotRuns . toolAdmissionClosedAt ) ,
521+ notInArray ( copilotRuns . status , TERMINAL_RUN_STATUSES )
522+ )
523+ )
524+ . for ( 'share' )
525+ return run ? settle ( tx ) : false
526+ } )
527+ }
528+
502529/**
503530 * Fails a call nobody claimed: the inverse CAS of the claim, so exactly one of the two wins.
504531 * `deadlinePassed` limits it to a call whose pickup window has closed.
505532 */
506533export async function failUnclaimedDesktopCall (
507534 input : DesktopCallFailure & { deadlinePassed : boolean }
508535) : Promise < boolean > {
509- const [ row ] = await db
510- . update ( copilotAsyncToolCalls )
511- . set ( {
512- status : ASYNC_TOOL_STATUS . failed ,
513- result : sanitizeValueForJsonb ( input . result ) ,
514- error : input . error ,
515- completedAt : sql `now()` ,
516- updatedAt : sql `now()` ,
517- } )
518- . where (
519- and (
520- eq ( copilotAsyncToolCalls . toolCallId , input . toolCallId ) ,
521- eq ( copilotAsyncToolCalls . runId , input . runId ) ,
522- eq ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . pending ) ,
523- isNull ( copilotAsyncToolCalls . executionOwnerToken ) ,
524- input . deadlinePassed
525- ? sql `${ copilotAsyncToolCalls . executionLeaseExpiresAt } <= clock_timestamp()`
526- : undefined
536+ return settleOnOpenRun ( input . runId , async ( tx ) => {
537+ const [ row ] = await tx
538+ . update ( copilotAsyncToolCalls )
539+ . set ( {
540+ status : ASYNC_TOOL_STATUS . failed ,
541+ result : sanitizeValueForJsonb ( input . result ) ,
542+ error : input . error ,
543+ completedAt : sql `now()` ,
544+ updatedAt : sql `now()` ,
545+ } )
546+ . where (
547+ and (
548+ eq ( copilotAsyncToolCalls . toolCallId , input . toolCallId ) ,
549+ eq ( copilotAsyncToolCalls . runId , input . runId ) ,
550+ eq ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . pending ) ,
551+ isNull ( copilotAsyncToolCalls . executionOwnerToken ) ,
552+ input . deadlinePassed
553+ ? sql `${ copilotAsyncToolCalls . executionLeaseExpiresAt } <= clock_timestamp()`
554+ : undefined
555+ )
527556 )
528- )
529- . returning ( { toolCallId : copilotAsyncToolCalls . toolCallId } )
530- return Boolean ( row )
557+ . returning ( { toolCallId : copilotAsyncToolCalls . toolCallId } )
558+ return Boolean ( row )
559+ } )
531560}
532561
533562/**
@@ -537,27 +566,29 @@ export async function failUnclaimedDesktopCall(
537566export async function failLapsedDesktopCall (
538567 input : DesktopCallFailure & { ownerToken : string }
539568) : Promise < boolean > {
540- const [ row ] = await db
541- . update ( copilotAsyncToolCalls )
542- . set ( {
543- status : ASYNC_TOOL_STATUS . failed ,
544- result : sanitizeValueForJsonb ( input . result ) ,
545- error : input . error ,
546- claimedBy : null ,
547- claimedAt : null ,
548- completedAt : sql `now()` ,
549- updatedAt : sql `now()` ,
550- } )
551- . where (
552- and (
553- eq ( copilotAsyncToolCalls . toolCallId , input . toolCallId ) ,
554- eq ( copilotAsyncToolCalls . runId , input . runId ) ,
555- eq ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . running ) ,
556- eq ( copilotAsyncToolCalls . executionOwnerToken , input . ownerToken ) ,
557- isNull ( copilotAsyncToolCalls . executionRevokedAt ) ,
558- sql `${ copilotAsyncToolCalls . executionLeaseExpiresAt } <= clock_timestamp()`
569+ return settleOnOpenRun ( input . runId , async ( tx ) => {
570+ const [ row ] = await tx
571+ . update ( copilotAsyncToolCalls )
572+ . set ( {
573+ status : ASYNC_TOOL_STATUS . failed ,
574+ result : sanitizeValueForJsonb ( input . result ) ,
575+ error : input . error ,
576+ claimedBy : null ,
577+ claimedAt : null ,
578+ completedAt : sql `now()` ,
579+ updatedAt : sql `now()` ,
580+ } )
581+ . where (
582+ and (
583+ eq ( copilotAsyncToolCalls . toolCallId , input . toolCallId ) ,
584+ eq ( copilotAsyncToolCalls . runId , input . runId ) ,
585+ eq ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . running ) ,
586+ eq ( copilotAsyncToolCalls . executionOwnerToken , input . ownerToken ) ,
587+ isNull ( copilotAsyncToolCalls . executionRevokedAt ) ,
588+ sql `${ copilotAsyncToolCalls . executionLeaseExpiresAt } <= clock_timestamp()`
589+ )
559590 )
560- )
561- . returning ( { toolCallId : copilotAsyncToolCalls . toolCallId } )
562- return Boolean ( row )
591+ . returning ( { toolCallId : copilotAsyncToolCalls . toolCallId } )
592+ return Boolean ( row )
593+ } )
563594}
0 commit comments