Skip to content

Commit d6d1947

Browse files
committed
fix(desktop): refuse a claim past its pickup window and leave executor leases to their own settlement
A device could claim an offered call after its pickup deadline and before the wait's next check settled it, running an action the turn was about to report as not started. The claim and the inbox now treat a closed window as no longer offered. The generic Sim lease sweep no longer settles a desktop executor's lapsed lease with the Sim interrupted result: the bound wait and the stale-execution cron settle it as outcome unknown, so the model is told the action may already have taken effect.
1 parent cb0dde1 commit d6d1947

3 files changed

Lines changed: 25 additions & 2 deletions

File tree

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ import {
5555
} from '@/lib/desktop/executor/doorbell'
5656
import { DesktopCallRevokedError } from '@/lib/desktop/executor/errors'
5757
import { SIM_TOOL_EXECUTION_VERSION } from '@/lib/mothership/async-runs/lifecycle'
58-
import { requestRunStop } from '@/lib/mothership/async-runs/repository'
58+
import {
59+
requestRunStop,
60+
revokeExpiredSimToolExecutions,
61+
} from '@/lib/mothership/async-runs/repository'
5962
import { prePersistClientExecutableToolCall, sseHandlers } from '@/lib/mothership/request/handlers'
6063
import { waitForClientToolCompletion } from '@/lib/mothership/request/tools/client'
6164
import { TraceCollector } from '@/lib/mothership/request/trace'
@@ -438,13 +441,15 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut
438441
await offered(toolCallId)
439442
expect((await storedCall(toolCallId)).status).toBe('pending')
440443
await lapse(toolCallId, 'pickup')
444+
/** Before the wait's next check settles it, the closed window already refuses the device. */
445+
expect((await desktop.pull()).items).toEqual([])
446+
await expect(desktop.claim(toolCallId)).rejects.toThrow('no longer waiting')
441447
await answer
442448

443449
expect(resultOf(context, toolCallId)).toMatchObject({
444450
success: false,
445451
output: { notStarted: true, reason: 'not_responding' },
446452
})
447-
await expect(desktop.claim(toolCallId)).rejects.toThrow('no longer waiting')
448453
},
449454
TURN_WAIT_MS
450455
)
@@ -468,6 +473,9 @@ describe.runIf(Boolean(redisUrl))("a turn bound to a desktop's background execut
468473
expect((await storedCall(toolCallId)).status).toBe('running')
469474

470475
await lapse(toolCallId, 'lease')
476+
/** A Sim tool waiting on the same run sweeps its expired executions; this one is not its. */
477+
await revokeExpiredSimToolExecutions({ runId: run.runId, userId })
478+
expect((await storedCall(toolCallId)).status).toBe('running')
471479
await answer
472480
const late = await desktop.complete(toolCallId, executionToken, { output: 'built' })
473481

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ export async function listDesktopInboxRows(identity: Omit<DesktopDeviceIdentity,
132132
and(
133133
eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.pending),
134134
isNull(copilotAsyncToolCalls.executionOwnerToken),
135+
or(
136+
isNull(copilotAsyncToolCalls.pickupDeadlineAt),
137+
sql`${copilotAsyncToolCalls.pickupDeadlineAt} > clock_timestamp()`
138+
),
135139
inArray(copilotRuns.status, LIVE_RUN_STATUSES),
136140
isNull(copilotRuns.toolAdmissionClosedAt)
137141
),

‎apps/sim/lib/mothership/async-runs/repository.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,11 @@ export async function claimDesktopToolCall(
815815
and(
816816
thisCall,
817817
eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.pending),
818+
// An offered call whose pickup window closed belongs to its not-started settlement.
819+
or(
820+
isNull(copilotAsyncToolCalls.pickupDeadlineAt),
821+
sql`${copilotAsyncToolCalls.pickupDeadlineAt} > clock_timestamp()`
822+
),
818823
or(
819824
and(
820825
isNull(copilotAsyncToolCalls.permissionRequestedAt),
@@ -903,6 +908,12 @@ async function revokeExpiredExecutions(tx: RunAdmissionTransaction, scope: SQL)
903908
isNull(copilotAsyncToolCalls.executionSettledAt),
904909
isNull(copilotAsyncToolCalls.executionRevokedAt),
905910
isNull(copilotAsyncToolCalls.clientWorkflowExecutionId),
911+
// A desktop executor's lapsed lease is settled by its own wait and the stale-execution cron,
912+
// with a result that warns the action may already have taken effect.
913+
or(
914+
isNull(copilotAsyncToolCalls.claimedBy),
915+
notInArray(copilotAsyncToolCalls.claimedBy, Object.values(DESKTOP_TOOL_CLAIM_OWNER))
916+
),
906917
sql`${copilotAsyncToolCalls.executionLeaseExpiresAt} <= clock_timestamp()`
907918
)
908919
)

0 commit comments

Comments
 (0)