@@ -82,6 +82,14 @@ function inferAction(result: ReactorAction | ReactorAction[]): Extract<ReactorAc
8282 return infer ;
8383}
8484
85+ function overflowError ( message = "context window exceeded" ) : ReactorInboundEvent {
86+ return {
87+ type : "inference.error" ,
88+ error : { category : "context_overflow" , message } ,
89+ partial : { text : "" } ,
90+ } as unknown as ReactorInboundEvent ;
91+ }
92+
8593function ephemeralTexts ( infer : Extract < ReactorAction , { type : "infer" } > ) : string [ ] | undefined {
8694 const options = infer . options as
8795 | { ephemeralTurns ?: Array < { content : Array < { text ?: string } > } > }
@@ -231,4 +239,109 @@ describe("SubAgentDirector tool failure recovery", () => {
231239 expect ( texts ?. [ 0 ] ) . toContain ( "A tool call failed" ) ;
232240 expect ( texts ?. [ 0 ] ) . not . toContain ( "re-reading the same paths" ) ;
233241 } ) ;
242+
243+ test ( "overflow after consume restores recovery once on continuation infer" , async ( ) => {
244+ let continuations = 0 ;
245+ const director = new SubAgentDirector (
246+ "system" ,
247+ [ ] ,
248+ ( ) => {
249+ continuations ++ ;
250+ } ,
251+ 30 ,
252+ ) ;
253+ const caps = capabilities ( ) ;
254+
255+ await director . decide ( inferenceDone ( [ "failed-then-overflow" ] ) , state , caps ) ;
256+ const texts = ephemeralTexts (
257+ inferAction ( await director . decide ( toolDone ( "failed-then-overflow" , true ) , state , caps ) ) ,
258+ ) ;
259+ expect ( texts ) . toHaveLength ( 1 ) ;
260+ expect ( texts ?. [ 0 ] ) . toContain ( "A tool call failed" ) ;
261+
262+ const compact = actions ( await director . decide ( overflowError ( ) , state , caps ) ) ;
263+ expect ( compact . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
264+ expect ( compact ) . toEqual ( [
265+ { type : "compact" , compactor : "pruning-compactor" , reason : "context-overflow" } ,
266+ ] ) ;
267+ expect ( continuations ) . toBe ( 1 ) ;
268+
269+ const resumed = inferAction ( await director . decide ( messageReceived ( "" ) , state , caps ) ) ;
270+ const resumedTexts = ephemeralTexts ( resumed ) ;
271+ expect ( resumedTexts ) . toHaveLength ( 1 ) ;
272+ expect ( resumedTexts ?. [ 0 ] ) . toContain ( "A tool call failed" ) ;
273+
274+ const later = inferAction ( await director . decide ( messageReceived ( "" ) , state , caps ) ) ;
275+ expect ( ephemeralTexts ( later ) ) . toBeUndefined ( ) ;
276+ } ) ;
277+
278+ test ( "successful nudged infer then later overflow does not resurrect recovery" , async ( ) => {
279+ let continuations = 0 ;
280+ const director = new SubAgentDirector (
281+ "system" ,
282+ [ ] ,
283+ ( ) => {
284+ continuations ++ ;
285+ } ,
286+ 30 ,
287+ ) ;
288+ const caps = capabilities ( ) ;
289+
290+ await director . decide ( inferenceDone ( [ "failed-then-done" ] ) , state , caps ) ;
291+ const recovered = ephemeralTexts (
292+ inferAction ( await director . decide ( toolDone ( "failed-then-done" , true ) , state , caps ) ) ,
293+ ) ;
294+ expect ( recovered ) . toHaveLength ( 1 ) ;
295+ expect ( recovered ?. [ 0 ] ) . toContain ( "A tool call failed" ) ;
296+
297+ await director . decide ( inferenceDone ( [ "later-success" ] ) , state , caps ) ;
298+ const afterSuccess = inferAction (
299+ await director . decide ( toolDone ( "later-success" ) , state , caps ) ,
300+ ) ;
301+ expect ( ephemeralTexts ( afterSuccess ) ) . toBeUndefined ( ) ;
302+
303+ const compact = actions ( await director . decide ( overflowError ( ) , state , caps ) ) ;
304+ expect ( compact . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
305+ expect ( compact ) . toEqual ( [
306+ { type : "compact" , compactor : "pruning-compactor" , reason : "context-overflow" } ,
307+ ] ) ;
308+ expect ( continuations ) . toBe ( 1 ) ;
309+
310+ const resumed = inferAction ( await director . decide ( messageReceived ( "" ) , state , caps ) ) ;
311+ expect ( ephemeralTexts ( resumed ) ) . toBeUndefined ( ) ;
312+ } ) ;
313+
314+ test ( "retains wrap-up through compaction and consumes it once on continuation infer" , async ( ) => {
315+ let continuations = 0 ;
316+ const director = new SubAgentDirector (
317+ "system" ,
318+ [ ] ,
319+ ( ) => {
320+ continuations ++ ;
321+ } ,
322+ 3 ,
323+ ) ;
324+ const caps = capabilities ( ) ;
325+
326+ await director . decide ( inferenceDone ( [ "near-budget-success" ] , 999_999 ) , longState , caps ) ;
327+ const compact = actions (
328+ await director . decide ( toolDone ( "near-budget-success" ) , longState , caps ) ,
329+ ) ;
330+ expect ( compact . some ( ( action ) => action . type === "infer" ) ) . toBe ( false ) ;
331+ expect ( compact ) . toEqual ( [
332+ { type : "checkpoint" , message : "tool-done" } ,
333+ { type : "compact" , compactor : "pruning-compactor" , reason : "context-threshold" } ,
334+ ] ) ;
335+ expect ( continuations ) . toBe ( 1 ) ;
336+
337+ const resumed = inferAction ( await director . decide ( messageReceived ( "" ) , longState , caps ) ) ;
338+ const resumedTexts = ephemeralTexts ( resumed ) ;
339+ expect ( resumedTexts ) . toHaveLength ( 1 ) ;
340+ expect ( resumedTexts ?. [ 0 ] ) . toContain ( "close to your turn budget" ) ;
341+ expect ( resumedTexts ?. [ 0 ] ) . toContain ( "write your final report now" ) ;
342+ expect ( resumedTexts ?. [ 0 ] ) . not . toContain ( "A tool call failed" ) ;
343+
344+ const later = inferAction ( await director . decide ( messageReceived ( "" ) , longState , caps ) ) ;
345+ expect ( ephemeralTexts ( later ) ) . toBeUndefined ( ) ;
346+ } ) ;
234347} ) ;
0 commit comments