@@ -125,6 +125,36 @@ describe("terminal failure record", () => {
125125 expect ( Object . isFrozen ( snap ?. failure ) ) . toBe ( true ) ;
126126 } ) ;
127127
128+ test ( "a loop_guard with a retryable provider cause is recovery-eligible" , ( ) => {
129+ const { sessions } = store ( ) ;
130+ const id = startRunning ( sessions ) ;
131+ sessions . fail ( id , "upstream overloaded" , {
132+ failure_class : "loop_guard" ,
133+ cause : createResolvedProviderFailureError ( "test-provider" , {
134+ category : "retryable" ,
135+ message : "upstream overloaded" ,
136+ } ) ,
137+ } ) ;
138+ const failure = sessions . get ( id ) ?. failure ;
139+ expect ( failure ?. failure_class ) . toBe ( "loop_guard" ) ;
140+ expect ( failure ?. recovery ) . toEqual ( { available : true , reason : "eligible" } ) ;
141+ expect ( "cause" in ( failure ?? { } ) ) . toBe ( false ) ;
142+ } ) ;
143+
144+ test ( "a loop_guard without a retryable cause is not recovery-eligible" , ( ) => {
145+ const { sessions } = store ( ) ;
146+ const id = startRunning ( sessions ) ;
147+ sessions . fail ( id , "doom" , {
148+ failure_class : "loop_guard" ,
149+ cause : new Error ( "doom" ) ,
150+ } ) ;
151+ expect ( sessions . get ( id ) ?. failure ?. failure_class ) . toBe ( "loop_guard" ) ;
152+ expect ( sessions . get ( id ) ?. failure ?. recovery ) . toEqual ( {
153+ available : false ,
154+ reason : "not_retryable" ,
155+ } ) ;
156+ } ) ;
157+
128158 test ( "fail without a classification records error" , ( ) => {
129159 const { sessions } = store ( ) ;
130160 const id = startRunning ( sessions ) ;
@@ -415,30 +445,47 @@ describe("backstop close honesty", () => {
415445 expect ( sessions . get ( id ) ?. failure ?. cleanup ) . toBe ( "released" ) ;
416446 } ) ;
417447
418- test ( "a rejecting backstop close records partial cleanup " , async ( ) => {
448+ test ( "a rejecting backstop close does not delay the failed lane " , async ( ) => {
419449 const { sessions } = store ( ) ;
420- const id = retainedWithClose ( sessions , async ( ) => {
421- throw new Error ( "child still live" ) ;
422- } ) ;
423- await sessions . fail ( id , "boom" , { failure_class : "error" } ) ;
450+ let rejectClose : ( err : Error ) => void = ( ) => undefined ;
451+ const id = retainedWithClose (
452+ sessions ,
453+ ( ) =>
454+ new Promise < void > ( ( _resolve , reject ) => {
455+ rejectClose = reject ;
456+ } ) ,
457+ ) ;
458+ const failing = sessions . fail ( id , "boom" , { failure_class : "error" } ) ;
424459 expect ( sessions . get ( id ) ?. lifecycle . state ) . toBe ( "failed" ) ;
425- expect ( sessions . get ( id ) ?. failure ?. cleanup ) . toBe ( "partial" ) ;
460+ const record = sessions . get ( id ) ?. failure ;
461+ expect ( record ?. cleanup ) . toBe ( "released" ) ;
462+ rejectClose ( new Error ( "child still live" ) ) ;
463+ await failing ;
464+ expect ( sessions . get ( id ) ?. failure ) . toBe (
465+ record as NonNullable < typeof record > ,
466+ ) ;
426467 } ) ;
427468
428- test ( "a backstop close that hangs past its deadline records partial cleanup" , async ( ) => {
429- const sessions = createSubAgentSessionStore ( {
430- now : ( ) => T0 ,
431- backstopCloseDeadlineMs : 20 ,
432- } ) ;
469+ test ( "a hanging backstop close does not delay the failed lane" , async ( ) => {
470+ const { sessions } = store ( ) ;
433471 const id = retainedWithClose (
434472 sessions ,
435473 ( ) => new Promise < void > ( ( ) => undefined ) ,
436474 ) ;
437- await sessions . fail ( id , "boom" , { failure_class : "error" } ) ;
438- expect ( sessions . get ( id ) ?. failure ?. cleanup ) . toBe ( "partial" ) ;
475+ const settled = await Promise . race ( [
476+ sessions
477+ . fail ( id , "boom" , { failure_class : "error" } )
478+ . then ( ( ) => "settled" as const ) ,
479+ new Promise < "stalled" > ( ( resolve ) =>
480+ setTimeout ( ( ) => resolve ( "stalled" ) , 200 ) ,
481+ ) ,
482+ ] ) ;
483+ expect ( settled ) . toBe ( "settled" ) ;
484+ expect ( sessions . get ( id ) ?. lifecycle . state ) . toBe ( "failed" ) ;
485+ expect ( sessions . get ( id ) ?. failure ?. cleanup ) . toBe ( "released" ) ;
439486 } ) ;
440487
441- test ( "the record is not written until the close settles, and never changes after" , async ( ) => {
488+ test ( "the record is written before the close settles, and never changes after" , async ( ) => {
442489 const { sessions } = store ( ) ;
443490 let finishClose : ( ) => void = ( ) => undefined ;
444491 const id = retainedWithClose (
@@ -449,11 +496,11 @@ describe("backstop close honesty", () => {
449496 } ) ,
450497 ) ;
451498 const failing = sessions . fail ( id , "boom" , { failure_class : "error" } ) ;
452- expect ( sessions . get ( id ) ?. failure ) . toBeUndefined ( ) ;
453- finishClose ( ) ;
454- await failing ;
499+ expect ( sessions . get ( id ) ?. lifecycle . state ) . toBe ( "failed" ) ;
455500 const record = sessions . get ( id ) ?. failure ;
456501 expect ( record ?. cleanup ) . toBe ( "released" ) ;
502+ finishClose ( ) ;
503+ await failing ;
457504 await settleTick ( ) ;
458505 expect ( sessions . get ( id ) ?. failure ) . toBe (
459506 record as NonNullable < typeof record > ,
0 commit comments