@@ -13,6 +13,7 @@ import {
1313 inArray ,
1414 isNotNull ,
1515 isNull ,
16+ ne ,
1617 notInArray ,
1718 or ,
1819 type SQL ,
@@ -152,55 +153,64 @@ export async function touchDesktopDevice(deviceId: string): Promise<void> {
152153}
153154
154155/**
155- * The rows a device's inbox is built from: every unclaimed pending desktop call on its recent open
156- * runs, plus every call it claimed and has not yet acknowledged. Pending calls on stopped or ended
157- * runs are left out here, so they cannot crowd actionable rows past the limit. Ordered by
158- * persistence time, the order the device claims in.
156+ * The rows a device's inbox is built from, in two sets with their own limits so neither can crowd
157+ * out the other: unclaimed calls on its recent open runs that are offered or waiting for the
158+ * user's decision, and calls it claimed that Sim settled without its result and it has not yet
159+ * acknowledged (cancel items). A call the device is still running is never listed: it already
160+ * holds it. Ordered by persistence time, the order the device claims in.
159161 */
160162export async function listDesktopInboxRows ( identity : Omit < DesktopDeviceIdentity , 'sessionId' > ) {
161- return db
162- . select ( {
163- toolCallId : copilotAsyncToolCalls . toolCallId ,
164- toolName : copilotAsyncToolCalls . toolName ,
165- args : copilotAsyncToolCalls . args ,
166- status : copilotAsyncToolCalls . status ,
167- permissionRequestedAt : copilotAsyncToolCalls . permissionRequestedAt ,
168- permissionDecision : copilotAsyncToolCalls . permissionDecision ,
169- claimed : sql < boolean > `${ copilotAsyncToolCalls . executionOwnerToken } IS NOT NULL` ,
170- createdAt : copilotAsyncToolCalls . createdAt ,
171- chatId : copilotRuns . chatId ,
172- chatTitle : copilotChats . title ,
173- workspaceId : copilotRuns . workspaceId ,
174- } )
175- . from ( copilotRuns )
176- . innerJoin ( copilotAsyncToolCalls , eq ( copilotAsyncToolCalls . runId , copilotRuns . id ) )
177- . innerJoin ( copilotChats , eq ( copilotChats . id , copilotRuns . chatId ) )
178- . where (
179- and (
180- eq ( copilotRuns . desktopDeviceId , identity . deviceId ) ,
181- eq ( copilotRuns . userId , identity . userId ) ,
182- sql `${ copilotRuns . startedAt } > now() - make_interval(hours => ${ DESKTOP_INBOX_HORIZON_HOURS } )` ,
183- inArray ( copilotAsyncToolCalls . toolName , [ ...DESKTOP_TOOL_CALL_NAMES ] ) ,
184- or (
185- and (
186- eq ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . pending ) ,
187- isNull ( copilotAsyncToolCalls . executionOwnerToken ) ,
188- or (
189- sql `${ copilotAsyncToolCalls . pickupDeadlineAt } > clock_timestamp()` ,
190- awaitingPermission
191- ) ,
192- inArray ( copilotRuns . status , LIVE_RUN_STATUSES ) ,
193- isNull ( copilotRuns . toolAdmissionClosedAt )
194- ) ,
195- and (
196- isNotNull ( copilotAsyncToolCalls . executionOwnerToken ) ,
197- isNull ( copilotAsyncToolCalls . executionSettledAt )
198- )
163+ const rowsWhere = ( state : SQL | undefined ) =>
164+ db
165+ . select ( {
166+ toolCallId : copilotAsyncToolCalls . toolCallId ,
167+ toolName : copilotAsyncToolCalls . toolName ,
168+ args : copilotAsyncToolCalls . args ,
169+ status : copilotAsyncToolCalls . status ,
170+ permissionRequestedAt : copilotAsyncToolCalls . permissionRequestedAt ,
171+ permissionDecision : copilotAsyncToolCalls . permissionDecision ,
172+ claimed : sql < boolean > `${ copilotAsyncToolCalls . executionOwnerToken } IS NOT NULL` ,
173+ createdAt : copilotAsyncToolCalls . createdAt ,
174+ chatId : copilotRuns . chatId ,
175+ chatTitle : copilotChats . title ,
176+ workspaceId : copilotRuns . workspaceId ,
177+ } )
178+ . from ( copilotRuns )
179+ . innerJoin ( copilotAsyncToolCalls , eq ( copilotAsyncToolCalls . runId , copilotRuns . id ) )
180+ . innerJoin ( copilotChats , eq ( copilotChats . id , copilotRuns . chatId ) )
181+ . where (
182+ and (
183+ eq ( copilotRuns . desktopDeviceId , identity . deviceId ) ,
184+ eq ( copilotRuns . userId , identity . userId ) ,
185+ sql `${ copilotRuns . startedAt } > now() - make_interval(hours => ${ DESKTOP_INBOX_HORIZON_HOURS } )` ,
186+ inArray ( copilotAsyncToolCalls . toolName , [ ...DESKTOP_TOOL_CALL_NAMES ] ) ,
187+ state
199188 )
200189 )
201- )
202- . orderBy ( asc ( copilotAsyncToolCalls . createdAt ) , asc ( copilotAsyncToolCalls . toolCallId ) )
203- . limit ( INBOX_ROW_LIMIT )
190+ . orderBy ( asc ( copilotAsyncToolCalls . createdAt ) , asc ( copilotAsyncToolCalls . toolCallId ) )
191+ . limit ( INBOX_ROW_LIMIT )
192+ const [ waiting , cancelled ] = await Promise . all ( [
193+ rowsWhere (
194+ and (
195+ eq ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . pending ) ,
196+ isNull ( copilotAsyncToolCalls . executionOwnerToken ) ,
197+ or ( sql `${ copilotAsyncToolCalls . pickupDeadlineAt } > clock_timestamp()` , awaitingPermission ) ,
198+ inArray ( copilotRuns . status , LIVE_RUN_STATUSES ) ,
199+ isNull ( copilotRuns . toolAdmissionClosedAt )
200+ )
201+ ) ,
202+ rowsWhere (
203+ and (
204+ isNotNull ( copilotAsyncToolCalls . executionOwnerToken ) ,
205+ isNull ( copilotAsyncToolCalls . executionSettledAt ) ,
206+ ne ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . running )
207+ )
208+ ) ,
209+ ] )
210+ return [ ...waiting , ...cancelled ] . sort (
211+ ( a , b ) =>
212+ a . createdAt . getTime ( ) - b . createdAt . getTime ( ) || a . toolCallId . localeCompare ( b . toolCallId )
213+ )
204214}
205215
206216export type DesktopInboxRow = Awaited < ReturnType < typeof listDesktopInboxRows > > [ number ]
@@ -295,7 +305,9 @@ export async function getRunDesktopDeviceId(runId: string): Promise<string | nul
295305
296306/**
297307 * Offers a pending call on a bound run to its device by opening its pickup window. Only an
298- * unclaimed call can be offered, and only once, so a re-dispatched call keeps its first deadline.
308+ * unclaimed call the user is not still deciding on can be offered, and only once, so a
309+ * re-dispatched call keeps its first deadline; recording the user's decision clears it, so an
310+ * allowed call's window starts when it is offered after the answer.
299311 */
300312export async function offerDesktopToolCall ( input : {
301313 toolCallId : string
@@ -313,6 +325,7 @@ export async function offerDesktopToolCall(input: {
313325 eq ( copilotAsyncToolCalls . runId , input . runId ) ,
314326 eq ( copilotAsyncToolCalls . status , ASYNC_TOOL_STATUS . pending ) ,
315327 isNull ( copilotAsyncToolCalls . pickupDeadlineAt ) ,
328+ sql `NOT (${ awaitingPermission } )` ,
316329 sql `EXISTS (SELECT 1 FROM ${ copilotRuns } r WHERE r.id = ${ copilotAsyncToolCalls . runId } AND r.desktop_device_id IS NOT NULL)`
317330 )
318331 )
0 commit comments