@@ -170,6 +170,8 @@ function setup(
170170 const busy : boolean [ ] = [ ]
171171 /** Calls whose result the executor reported as reaching the model, in order. */
172172 const delivered : string [ ] = [ ]
173+ /** Calls Sim finished with without their real result reaching the model, in order. */
174+ const notDelivered : string [ ] = [ ]
173175 const executor = new DesktopExecutor ( {
174176 client : sim . client ,
175177 journal,
@@ -180,12 +182,23 @@ function setup(
180182 onBusyChange : ( value ) => busy . push ( value ) ,
181183 onApprovals : ( items ) => approvals . push ( items ) ,
182184 onResultDelivered : ( toolCallId ) => delivered . push ( toolCallId ) ,
185+ onResultNotDelivered : ( toolCallId ) => notDelivered . push ( toolCallId ) ,
183186 ...( options . maxHeldCalls ? { maxHeldCalls : options . maxHeldCalls } : { } ) ,
184187 ...( options . deliveryAwakeLimitMs !== undefined
185188 ? { deliveryAwakeLimitMs : options . deliveryAwakeLimitMs }
186189 : { } ) ,
187190 } )
188- return { sim, journal, runner, executor, onUnregistered, busy, approvals, delivered }
191+ return {
192+ sim,
193+ journal,
194+ runner,
195+ executor,
196+ onUnregistered,
197+ busy,
198+ approvals,
199+ delivered,
200+ notDelivered,
201+ }
189202}
190203
191204describe ( 'claiming' , ( ) => {
@@ -207,13 +220,13 @@ describe('claiming', () => {
207220 } )
208221
209222 it ( 'reports a result as reaching the model only once Sim takes it as the call own' , async ( ) => {
210- const { sim, journal, runner, executor, delivered } = setup ( )
223+ const { sim, journal, runner, executor, delivered, notDelivered } = setup ( )
211224 runner . immediate = DONE
212225 // Sim settled this call first: the result never reached the model.
213226 sim . completionOutcome = 'superseded'
214227 sim . inbox = [ callItem ( 'call-superseded' , 'chat-a' ) ]
215228 await executor . reconcile ( )
216- await vi . waitFor ( ( ) => expect ( sim . completions ) . toHaveLength ( 1 ) )
229+ await vi . waitFor ( ( ) => expect ( notDelivered ) . toEqual ( [ 'call-superseded' ] ) )
217230 expect ( delivered ) . toEqual ( [ ] )
218231
219232 // Taken by Sim, even with nothing written locally (no OS encryption, say).
@@ -222,10 +235,57 @@ describe('claiming', () => {
222235 sim . inbox = [ callItem ( 'call-recorded' , 'chat-a' ) ]
223236 await executor . reconcile ( )
224237 await vi . waitFor ( ( ) => expect ( delivered ) . toEqual ( [ 'call-recorded' ] ) )
238+ expect ( notDelivered ) . toEqual ( [ 'call-superseded' ] )
239+ } )
240+
241+ it ( 'reports a result Sim settled first, while a failed send waited to retry, as never reaching the model' , async ( ) => {
242+ const { sim, runner, executor, delivered, notDelivered } = setup ( )
243+ // The terminal handed back a command still going, and the first send of that result fails.
244+ runner . immediate = { status : 'success' , message : 'running' , data : { status : 'running' } }
245+ sim . completeErrors = [ new DeviceRequestError ( 503 , 'unavailable' ) ]
246+ sim . inbox = [ callItem ( 'call-1' , 'chat-a' , 'terminal' ) ]
247+ await executor . reconcile ( )
248+ await vi . waitFor ( ( ) => expect ( runner . started ) . toEqual ( [ 'call-1' ] ) )
249+
250+ // The user pressed Stop meanwhile, so Sim settled the call before the retry reached it.
251+ sim . completionOutcome = 'superseded'
252+ sim . inbox = [ { kind : 'cancel' , toolCallId : 'call-1' } ]
253+ await executor . reconcile ( )
254+
255+ await vi . waitFor ( ( ) => expect ( notDelivered ) . toEqual ( [ 'call-1' ] ) )
256+ expect ( delivered ) . toEqual ( [ ] )
257+ } )
258+
259+ it ( 'reports a stand-in Sim holds as a duplicate as never reaching the model, within one app run' , async ( ) => {
260+ const { sim, runner, executor, delivered, notDelivered } = setup ( )
261+ // Too large to send, so Sim never took the real result; the stand-in's first answer was lost.
262+ runner . immediate = DONE
263+ sim . completeErrors = [
264+ new DeviceRequestError ( 413 , 'too large' ) ,
265+ new DeviceRequestError ( 0 , 'connection reset' ) ,
266+ ]
267+ sim . completionOutcome = 'duplicate'
268+ sim . inbox = [ callItem ( 'call-1' , 'chat-a' , 'terminal' ) ]
269+
270+ await executor . reconcile ( )
271+
272+ await vi . waitFor ( ( ) => expect ( notDelivered ) . toEqual ( [ 'call-1' ] ) )
273+ expect ( delivered ) . toEqual ( [ ] )
274+ } )
275+
276+ it ( 'reports a result Sim refused as never reaching the model' , async ( ) => {
277+ const { sim, runner, executor, notDelivered } = setup ( )
278+ runner . immediate = DONE
279+ sim . completeErrors = [ new DeviceRequestError ( 403 , 'forbidden' ) ]
280+ sim . inbox = [ callItem ( 'call-refused' , 'chat-a' ) ]
281+
282+ await executor . reconcile ( )
283+
284+ await vi . waitFor ( ( ) => expect ( notDelivered ) . toEqual ( [ 'call-refused' ] ) )
225285 } )
226286
227287 it ( 'reports no result that stands in for what the action produced as reaching the model' , async ( ) => {
228- const { sim, runner, executor, delivered } = setup ( )
288+ const { sim, runner, executor, delivered, notDelivered } = setup ( )
229289 const standIns : DesktopToolCompletion [ ] = [
230290 { status : 'cancelled' , message : 'Stopped.' } ,
231291 { status : 'error' , message : 'Too large.' , data : { resultOmitted : true } } ,
@@ -243,6 +303,7 @@ describe('claiming', () => {
243303 sim . inbox = [ callItem ( 'real' , 'chat-a' ) ]
244304 await executor . reconcile ( )
245305 await vi . waitFor ( ( ) => expect ( delivered ) . toEqual ( [ 'real' ] ) )
306+ expect ( notDelivered ) . toEqual ( [ 'stand-in-0' , 'stand-in-1' , 'stand-in-2' , 'stand-in-3' ] )
246307 } )
247308
248309 it ( 'claims a whole backlog at once, before any of it runs' , async ( ) => {
@@ -492,6 +553,55 @@ describe('restarting', () => {
492553 expect ( runner . started ) . toEqual ( [ ] )
493554 await vi . waitFor ( ( ) => expect ( journal . entries . size ) . toBe ( 0 ) )
494555 } )
556+
557+ it ( 'leaves open whether the model has a result when Sim already held one for the call' , async ( ) => {
558+ const { sim, journal, executor, delivered, notDelivered } = setup ( )
559+ // The previous run's send of the real result may have landed just before the app went down.
560+ sim . completionOutcome = 'duplicate'
561+ await journal . put ( { toolCallId : 'started-1' , state : 'started' , executionToken : 't-started' } )
562+
563+ await executor . recover ( )
564+
565+ await vi . waitFor ( ( ) => expect ( journal . entries . size ) . toBe ( 0 ) )
566+ expect ( sim . completions ) . toHaveLength ( 1 )
567+ expect ( delivered ) . toEqual ( [ ] )
568+ expect ( notDelivered ) . toEqual ( [ ] )
569+ } )
570+
571+ it ( 'still leaves a recovered result open when Sim held one, after parking it' , async ( ) => {
572+ const { sim, journal, executor, onUnregistered, delivered, notDelivered } = setup ( )
573+ sim . completeErrors = [ new DeviceRequestError ( 401 , 'unregistered' ) ]
574+ sim . completionOutcome = 'duplicate'
575+ await journal . put ( { toolCallId : 'started-1' , state : 'started' , executionToken : 't-started' } )
576+ await executor . recover ( )
577+ await vi . waitFor ( ( ) => expect ( onUnregistered ) . toHaveBeenCalledTimes ( 1 ) )
578+
579+ // Registered again.
580+ executor . resumeParked ( )
581+
582+ await vi . waitFor ( ( ) => expect ( sim . completions ) . toHaveLength ( 1 ) )
583+ await vi . waitFor ( ( ) => expect ( journal . entries . size ) . toBe ( 0 ) )
584+ expect ( delivered ) . toEqual ( [ ] )
585+ expect ( notDelivered ) . toEqual ( [ ] )
586+ } )
587+
588+ it ( 'reports a recovered result too large to send, then held as a duplicate, as never reaching the model' , async ( ) => {
589+ const { sim, journal, executor, delivered, notDelivered } = setup ( )
590+ // No run could have sent this result: it is too large every time.
591+ sim . completeErrors = [ new DeviceRequestError ( 413 , 'too large' ) ]
592+ sim . completionOutcome = 'duplicate'
593+ await journal . put ( {
594+ toolCallId : 'result-1' ,
595+ state : 'result' ,
596+ executionToken : 't' ,
597+ completion : DONE ,
598+ } )
599+
600+ await executor . recover ( )
601+
602+ await vi . waitFor ( ( ) => expect ( notDelivered ) . toEqual ( [ 'result-1' ] ) )
603+ expect ( delivered ) . toEqual ( [ ] )
604+ } )
495605} )
496606
497607describe ( 'delivery' , ( ) => {
0 commit comments