@@ -172,6 +172,77 @@ describe("SubAgentDirector tool failure recovery", () => {
172172 expect ( texts ?. [ 0 ] ) . toContain ( "report the blocker" ) ;
173173 } ) ;
174174
175+ test ( "N consecutive failed tools in one burst produce one coalesced intervention record" , async ( ) => {
176+ const director = new SubAgentDirector ( "system" , [ ] , undefined , 30 ) ;
177+ const caps = capabilities ( ) ;
178+ const records : { id : string ; count ?: number } [ ] = [ ] ;
179+ director . observeInterventions ( ( event ) => {
180+ records . push (
181+ event . count === undefined
182+ ? { id : event . id }
183+ : { id : event . id , count : event . count } ,
184+ ) ;
185+ } ) ;
186+
187+ await director . decide (
188+ inferenceDone ( [ "fail-a" , "fail-b" , "fail-c" ] ) ,
189+ state ,
190+ caps ,
191+ ) ;
192+ await director . decide ( toolDone ( "fail-a" , true ) , state , caps ) ;
193+ await director . decide ( toolDone ( "fail-b" , true ) , state , caps ) ;
194+ expect ( records ) . toEqual ( [ ] ) ;
195+
196+ const texts = ephemeralTexts (
197+ inferAction ( await director . decide ( toolDone ( "fail-c" , true ) , state , caps ) ) ,
198+ ) ;
199+ expect ( texts ) . toHaveLength ( 1 ) ;
200+ expect ( texts ?. [ 0 ] ) . toContain ( "A tool call failed" ) ;
201+ expect ( records ) . toEqual ( [ { id : "tool-failure-recovery" , count : 3 } ] ) ;
202+ } ) ;
203+
204+ test ( "a single failed tool writes one recovery record with count omitted" , async ( ) => {
205+ const director = new SubAgentDirector ( "system" , [ ] , undefined , 30 ) ;
206+ const caps = capabilities ( ) ;
207+ const records : { id : string ; class ?: string ; count ?: number } [ ] = [ ] ;
208+ director . observeInterventions ( ( event ) => {
209+ records . push (
210+ event . count === undefined
211+ ? { id : event . id , class : event . class }
212+ : { id : event . id , class : event . class , count : event . count } ,
213+ ) ;
214+ } ) ;
215+
216+ await director . decide ( inferenceDone ( [ "failed-call" ] ) , state , caps ) ;
217+ await director . decide ( toolDone ( "failed-call" , true ) , state , caps ) ;
218+ expect ( records ) . toEqual ( [ { id : "tool-failure-recovery" , class : "nudge" } ] ) ;
219+ } ) ;
220+
221+ test ( "a later failure burst writes a new coalesced record instead of stacking" , async ( ) => {
222+ const director = new SubAgentDirector ( "system" , [ ] , undefined , 30 ) ;
223+ const caps = capabilities ( ) ;
224+ const records : { id : string ; count ?: number } [ ] = [ ] ;
225+ director . observeInterventions ( ( event ) => {
226+ records . push (
227+ event . count === undefined
228+ ? { id : event . id }
229+ : { id : event . id , count : event . count } ,
230+ ) ;
231+ } ) ;
232+
233+ await director . decide ( inferenceDone ( [ "fail-a" , "fail-b" ] ) , state , caps ) ;
234+ await director . decide ( toolDone ( "fail-a" , true ) , state , caps ) ;
235+ await director . decide ( toolDone ( "fail-b" , true ) , state , caps ) ;
236+ expect ( records ) . toEqual ( [ { id : "tool-failure-recovery" , count : 2 } ] ) ;
237+
238+ await director . decide ( inferenceDone ( [ "fail-c" ] ) , state , caps ) ;
239+ await director . decide ( toolDone ( "fail-c" , true ) , state , caps ) ;
240+ expect ( records ) . toEqual ( [
241+ { id : "tool-failure-recovery" , count : 2 } ,
242+ { id : "tool-failure-recovery" } ,
243+ ] ) ;
244+ } ) ;
245+
175246 test ( "successful tool result has no ephemeral recovery turn" , async ( ) => {
176247 const director = new SubAgentDirector ( "system" , [ ] , undefined , 30 ) ;
177248 const caps = capabilities ( ) ;
@@ -221,6 +292,42 @@ describe("SubAgentDirector tool failure recovery", () => {
221292 expect ( ephemeralTexts ( infer ) ) . toBeUndefined ( ) ;
222293 } ) ;
223294
295+ test ( "compaction defers the coalesced recovery record until continuation infer" , async ( ) => {
296+ const director = new SubAgentDirector (
297+ "system" ,
298+ [ ] ,
299+ ( ) => undefined ,
300+ 30 ,
301+ ) ;
302+ const caps = capabilities ( ) ;
303+ const records : { id : string ; count ?: number } [ ] = [ ] ;
304+ director . observeInterventions ( ( event ) => {
305+ records . push (
306+ event . count === undefined
307+ ? { id : event . id }
308+ : { id : event . id , count : event . count } ,
309+ ) ;
310+ } ) ;
311+
312+ await director . decide (
313+ inferenceDone ( [ "fail-a" , "fail-b" ] , 999_999 ) ,
314+ longState ,
315+ caps ,
316+ ) ;
317+ await director . decide ( toolDone ( "fail-a" , true ) , longState , caps ) ;
318+ const compact = actions (
319+ await director . decide ( toolDone ( "fail-b" , true ) , longState , caps ) ,
320+ ) ;
321+ expect ( compact . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
322+ expect ( records ) . toEqual ( [ ] ) ;
323+
324+ const resumed = inferAction (
325+ await director . decide ( messageReceived ( "" ) , longState , caps ) ,
326+ ) ;
327+ expect ( ephemeralTexts ( resumed ) ?. [ 0 ] ) . toContain ( "A tool call failed" ) ;
328+ expect ( records ) . toEqual ( [ { id : "tool-failure-recovery" , count : 2 } ] ) ;
329+ } ) ;
330+
224331 test ( "retains recovery through compaction and consumes it once on continuation infer" , async ( ) => {
225332 let continuations = 0 ;
226333 const director = new SubAgentDirector (
0 commit comments