Skip to content

Commit ea6cd54

Browse files
committed
fix(desktop): limit the overdue sweep over desktop calls only, and bound it by deadline
The sweep's tool-name filter admitted read, grep and glob calls on Sim's own files, which the settlement then skipped, so enough of them could fill every batch and starve real desktop calls. Queries now use the SQL form of isDesktopToolCall (a desktop tool by name, or a VFS read of a granted local folder) before their limit. The sweep's horizon is now on the deadline that lapsed, not on the run's start: Sim does not enforce a run's wall clock, so a long-lived run's recently overdue call is still settled.
1 parent 8224475 commit ea6cd54

3 files changed

Lines changed: 88 additions & 11 deletions

File tree

‎apps/sim/lib/desktop/executor/bound-turn.integration.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,61 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut
764764
TURN_WAIT_MS
765765
)
766766

767+
it(
768+
'reaches an abandoned desktop call however many older Sim-file reads the run left pending',
769+
async () => {
770+
const desktop = await signedInDesktop()
771+
const run = await boundRun(desktop)
772+
await db.insert(copilotAsyncToolCalls).values(
773+
Array.from({ length: 201 }, () => ({
774+
runId: run.runId,
775+
toolCallId: generateId(),
776+
toolName: 'read',
777+
args: { path: 'workspace/notes.md' },
778+
createdAt: new Date(Date.now() - 900_000),
779+
}))
780+
)
781+
const abandoned = generateId()
782+
await db.insert(copilotAsyncToolCalls).values({
783+
runId: run.runId,
784+
toolCallId: abandoned,
785+
toolName: 'browser_click',
786+
args: { ref: 'e1' },
787+
createdAt: new Date(Date.now() - 600_000),
788+
})
789+
790+
await runCleanupStaleExecutions()
791+
792+
expect((await storedCall(abandoned)).status).toBe('failed')
793+
},
794+
TURN_WAIT_MS
795+
)
796+
797+
it(
798+
'settles a call whose window lapsed recently on a run that started long ago',
799+
async () => {
800+
const desktop = await signedInDesktop()
801+
const run = await boundRun(desktop)
802+
await db
803+
.update(copilotRuns)
804+
.set({ startedAt: new Date(Date.now() - 2 * 24 * 3_600_000) })
805+
.where(eq(copilotRuns.id, run.runId))
806+
const toolCallId = generateId()
807+
await db.insert(copilotAsyncToolCalls).values({
808+
runId: run.runId,
809+
toolCallId,
810+
toolName: 'browser_click',
811+
args: { ref: 'e1' },
812+
createdAt: new Date(Date.now() - 600_000),
813+
})
814+
815+
await runCleanupStaleExecutions()
816+
817+
expect((await storedCall(toolCallId)).status).toBe('failed')
818+
},
819+
TURN_WAIT_MS
820+
)
821+
767822
it(
768823
'keeps the pickup window of a call that was never gated when a decision is posted for it',
769824
async () => {

‎apps/sim/lib/desktop/executor/repository.ts‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ import {
3131
isTerminalAsyncStatus,
3232
} from '@/lib/mothership/async-runs/lifecycle'
3333
import { DESKTOP_TOOL_PICKUP_GRACE_MS } from '@/lib/mothership/constants'
34-
import { DESKTOP_TOOL_CALL_NAMES } from '@/lib/mothership/tools/desktop-tools'
34+
import { NAMED_DESKTOP_TOOL_NAMES } from '@/lib/mothership/tools/desktop-tools'
35+
import { USER_LOCAL_VFS_ROOT } from '@/lib/mothership/tools/local-filesystem'
3536

3637
const LIVE_RUN_STATUSES: CopilotRunStatus[] = ['active', 'paused_waiting_for_tool', 'resuming']
3738

@@ -61,6 +62,26 @@ function pickupOverdueAt(at: SQL) {
6162
sql`${pickupDeadline} <= ${at}`
6263
)
6364
}
65+
66+
function isUserLocalVfsPath(path: SQL) {
67+
return sql`(${path} = ${USER_LOCAL_VFS_ROOT} OR ${path} LIKE ${`${USER_LOCAL_VFS_ROOT}/%`})`
68+
}
69+
70+
/**
71+
* The SQL form of `isDesktopToolCall`, so a query limits only over calls the desktop runs: a
72+
* desktop tool by name, or a VFS read of a granted local folder (not a read of Sim's own files).
73+
*/
74+
const isDesktopToolCallRow = or(
75+
inArray(copilotAsyncToolCalls.toolName, [...NAMED_DESKTOP_TOOL_NAMES]),
76+
and(
77+
inArray(copilotAsyncToolCalls.toolName, ['read', 'grep']),
78+
isUserLocalVfsPath(sql`${copilotAsyncToolCalls.args}->>'path'`)
79+
),
80+
and(
81+
eq(copilotAsyncToolCalls.toolName, 'glob'),
82+
isUserLocalVfsPath(sql`${copilotAsyncToolCalls.args}->>'pattern'`)
83+
)
84+
)
6485
const INBOX_ROW_LIMIT = 500
6586

6687
export interface DesktopDeviceRegistration {
@@ -183,7 +204,7 @@ export async function listDesktopInboxRows(identity: Omit<DesktopDeviceIdentity,
183204
eq(copilotRuns.desktopDeviceId, identity.deviceId),
184205
eq(copilotRuns.userId, identity.userId),
185206
sql`${copilotRuns.startedAt} > now() - make_interval(hours => ${DESKTOP_INBOX_HORIZON_HOURS})`,
186-
inArray(copilotAsyncToolCalls.toolName, [...DESKTOP_TOOL_CALL_NAMES]),
207+
isDesktopToolCallRow,
187208
state
188209
)
189210
)
@@ -366,7 +387,7 @@ export async function getDesktopToolCallDeadlines(toolCallId: string) {
366387
and(
367388
eq(copilotAsyncToolCalls.toolCallId, toolCallId),
368389
isNotNull(copilotRuns.desktopDeviceId),
369-
inArray(copilotAsyncToolCalls.toolName, [...DESKTOP_TOOL_CALL_NAMES])
390+
isDesktopToolCallRow
370391
)
371392
)
372393
.limit(1)
@@ -380,28 +401,29 @@ export type DesktopToolCallDeadlines = NonNullable<
380401
/**
381402
* Bound desktop calls a deadline passed for at least `slackMs` ago: unclaimed past their pickup
382403
* deadline (offered or not), or claimed by the executor with a lapsed lease. A live waiter settles
383-
* these within its 5 s poll, so anything this finds lost its waiter. Only runs inside the inbox's
384-
* horizon are scanned, oldest calls first.
404+
* these within its 5 s poll, so anything this finds lost its waiter. Only deadlines that lapsed
405+
* within the inbox's horizon are scanned, whatever the run's age, oldest calls first.
385406
*/
386407
export async function listOverdueDesktopToolCalls(input: { slackMs: number; limit: number }) {
387408
const overdue = sql`clock_timestamp() - ${input.slackMs} * interval '1 millisecond'`
409+
const horizon = sql`clock_timestamp() - make_interval(hours => ${DESKTOP_INBOX_HORIZON_HOURS})`
388410
const rows = await db
389411
.select({ toolCallId: copilotAsyncToolCalls.toolCallId })
390412
.from(copilotAsyncToolCalls)
391413
.innerJoin(copilotRuns, eq(copilotRuns.id, copilotAsyncToolCalls.runId))
392414
.where(
393415
and(
394416
isNotNull(copilotRuns.desktopDeviceId),
395-
sql`${copilotRuns.startedAt} > now() - make_interval(hours => ${DESKTOP_INBOX_HORIZON_HOURS})`,
396-
inArray(copilotAsyncToolCalls.toolName, [...DESKTOP_TOOL_CALL_NAMES]),
417+
isDesktopToolCallRow,
397418
or(
398-
pickupOverdueAt(overdue),
419+
and(pickupOverdueAt(overdue), sql`${pickupDeadline} > ${horizon}`),
399420
and(
400421
eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.running),
401422
inArray(copilotAsyncToolCalls.claimedBy, Object.values(DESKTOP_TOOL_CLAIM_OWNER)),
402423
isNotNull(copilotAsyncToolCalls.executionOwnerToken),
403424
isNull(copilotAsyncToolCalls.executionRevokedAt),
404-
sql`${copilotAsyncToolCalls.executionLeaseExpiresAt} < ${overdue}`
425+
sql`${copilotAsyncToolCalls.executionLeaseExpiresAt} < ${overdue}`,
426+
sql`${copilotAsyncToolCalls.executionLeaseExpiresAt} > ${horizon}`
405427
)
406428
)
407429
)

‎apps/sim/lib/mothership/tools/desktop-tools.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export function isClaimedOnPickup(
6868
)
6969
}
7070

71-
/** Every tool name {@link isDesktopToolCall} can accept, for narrowing a query before it. */
72-
export const DESKTOP_TOOL_CALL_NAMES = [...DESKTOP_TOOL_NAMES, 'read', 'grep', 'glob']
71+
/** The tools that run through the desktop app by name alone; local VFS reads also depend on args. */
72+
export const NAMED_DESKTOP_TOOL_NAMES: readonly string[] = [...DESKTOP_TOOL_NAMES]
7373

7474
/**
7575
* The claim owner a desktop background executor records. It is the per-surface owner the chat

0 commit comments

Comments
 (0)