@@ -28,6 +28,11 @@ const logger = createLogger('DesktopExecutor')
2828const DEFAULT_MAX_HELD_CALLS = 32
2929/** Ceiling on the wait between attempts to deliver one result. */
3030const DELIVERY_RETRY_MAX_MS = 30_000
31+ /**
32+ * How long a result that keeps failing to reach Sim holds the machine awake. Delivery keeps
33+ * retrying after this; it just stops counting as work that needs the machine awake.
34+ */
35+ const DELIVERY_AWAKE_LIMIT_MS = 10 * 60_000
3136
3237const NOT_STARTED_AFTER_RESTART =
3338 'Not run: this action never started, because the Sim desktop app restarted before it began. Nothing happened on the user’s computer. Do not retry it in this turn; tell the user, who can ask again.'
@@ -65,6 +70,8 @@ export interface DesktopExecutorOptions {
6570 maxHeldCalls ?: number
6671 /** First delivery retry delay; tests shorten it. */
6772 retryBaseMs ?: number
73+ /** How long a failing delivery keeps the machine awake; tests shorten it. */
74+ deliveryAwakeLimitMs ?: number
6875}
6976
7077type HeldPhase = 'queued' | 'running' | 'reporting'
@@ -95,7 +102,9 @@ export class DesktopExecutor {
95102 /** The journal walk after a restart; sign-out waits for it before clearing the journal. */
96103 private recovering : Promise < void > | null = null
97104 /** Results a previous app run left, still on their way to Sim. */
98- private recoveredInFlight = 0
105+ private readonly recoveringIds = new Set < string > ( )
106+ /** Results that have failed to reach Sim for longer than {@link DELIVERY_AWAKE_LIMIT_MS}. */
107+ private readonly stalledDeliveries = new Set < string > ( )
99108 /** Results Sim refused because it no longer recognized this device; sent once it registers again. */
100109 private readonly parked = new Map <
101110 string ,
@@ -154,10 +163,10 @@ export class DesktopExecutor {
154163 state : entry . state ,
155164 } )
156165 // Held awake like a running call: the result exists only on this machine until Sim has it.
157- this . recoveredInFlight += 1
166+ this . recoveringIds . add ( entry . toolCallId )
158167 this . updateBusy ( )
159168 void this . deliver ( entry . toolCallId , entry . executionToken , completion ) . finally ( ( ) => {
160- this . recoveredInFlight -= 1
169+ this . recoveringIds . delete ( entry . toolCallId )
161170 this . updateBusy ( )
162171 } )
163172 }
@@ -357,9 +366,25 @@ export class DesktopExecutor {
357366 completion : DesktopToolCompletion
358367 ) : Promise < void > {
359368 if ( this . disposed ) return
360- let pending = completion
361369 // Best effort: unrecorded, a crash reports the call from its `started` entry as outcome unknown.
362370 await this . record ( { toolCallId, state : 'result' , executionToken, completion } )
371+ const sendingSince = Date . now ( )
372+ try {
373+ await this . sendResult ( toolCallId , executionToken , completion , sendingSince )
374+ } finally {
375+ // Whoever holds the delivery (a held call, a recovered result) updates the busy state as it
376+ // lets go; doing it here first would flash the machine awake again in between.
377+ this . stalledDeliveries . delete ( toolCallId )
378+ }
379+ }
380+
381+ private async sendResult (
382+ toolCallId : string ,
383+ executionToken : string ,
384+ completion : DesktopToolCompletion ,
385+ sendingSince : number
386+ ) : Promise < void > {
387+ let pending = completion
363388 for ( let attempt = 1 ; ! this . disposed ; attempt ++ ) {
364389 try {
365390 const outcome = await this . options . client . complete ( {
@@ -407,6 +432,15 @@ export class DesktopExecutor {
407432 } )
408433 break
409434 }
435+ if (
436+ Date . now ( ) - sendingSince >
437+ ( this . options . deliveryAwakeLimitMs ?? DELIVERY_AWAKE_LIMIT_MS ) &&
438+ ! this . stalledDeliveries . has ( toolCallId )
439+ ) {
440+ // Still retried, but no longer a reason to keep the machine awake.
441+ this . stalledDeliveries . add ( toolCallId )
442+ this . updateBusy ( )
443+ }
410444 await sleep (
411445 backoffWithJitter ( attempt , error . retryAfterMs , {
412446 baseMs : this . options . retryBaseMs ,
@@ -478,7 +512,9 @@ export class DesktopExecutor {
478512 private updateBusy ( ) : void {
479513 // A disposed executor reports idle once, at dispose; a delivery that settles later must not
480514 // speak for the executor that replaced it.
481- const busy = ! this . disposed && ( this . held . size > 0 || this . recoveredInFlight > 0 )
515+ const awake = ( toolCallId : string ) => ! this . stalledDeliveries . has ( toolCallId )
516+ const busy =
517+ ! this . disposed && ( [ ...this . held . keys ( ) ] . some ( awake ) || [ ...this . recoveringIds ] . some ( awake ) )
482518 if ( busy === this . busy ) return
483519 this . busy = busy
484520 this . options . onBusyChange ?.( busy )
0 commit comments