@@ -309,6 +309,258 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
309309 ) ;
310310 } ) ;
311311
312+ it ( "uses the session model fallback chain when implicit compaction fails" , async ( ) => {
313+ resolveModelMock . mockImplementation ( ( provider = "openai" , modelId = "fake" ) => ( {
314+ model : { provider, api : "responses" , id : modelId , input : [ ] } ,
315+ error : null ,
316+ authStorage : { setRuntimeApiKey : vi . fn ( ) } ,
317+ modelRegistry : { } ,
318+ } ) ) ;
319+ sessionCompactImpl
320+ . mockRejectedValueOnce (
321+ Object . assign (
322+ new Error (
323+ "400 The response was filtered due to the prompt triggering Azure OpenAI's content management policy." ,
324+ ) ,
325+ { status : 400 } ,
326+ ) ,
327+ )
328+ . mockResolvedValueOnce ( {
329+ summary : "fallback summary" ,
330+ firstKeptEntryId : "entry-fallback" ,
331+ tokensBefore : 120 ,
332+ details : { ok : true } ,
333+ } ) ;
334+
335+ const result = await compactEmbeddedPiSessionDirect ( {
336+ sessionId : "session-1" ,
337+ sessionKey : TEST_SESSION_KEY ,
338+ sessionFile : "/tmp/session.jsonl" ,
339+ workspaceDir : "/tmp/workspace" ,
340+ provider : "openai" ,
341+ model : "gpt-primary" ,
342+ config : {
343+ agents : {
344+ defaults : {
345+ model : {
346+ primary : "openai/gpt-primary" ,
347+ fallbacks : [ "anthropic/claude-fallback" ] ,
348+ } ,
349+ } ,
350+ } ,
351+ } as never ,
352+ } ) ;
353+
354+ expect ( result . ok ) . toBe ( true ) ;
355+ expect ( result . result ?. summary ) . toBe ( "fallback summary" ) ;
356+ expect ( resolveModelMock ) . toHaveBeenCalledWith (
357+ "openai" ,
358+ "gpt-primary" ,
359+ expect . any ( String ) ,
360+ expect . anything ( ) ,
361+ ) ;
362+ expect ( resolveModelMock ) . toHaveBeenCalledWith (
363+ "anthropic" ,
364+ "claude-fallback" ,
365+ expect . any ( String ) ,
366+ expect . anything ( ) ,
367+ ) ;
368+ } ) ;
369+
370+ it ( "uses the session model fallback chain when overflow compaction fails" , async ( ) => {
371+ resolveModelMock . mockImplementation ( ( provider = "openai" , modelId = "fake" ) => ( {
372+ model : { provider, api : "responses" , id : modelId , input : [ ] } ,
373+ error : null ,
374+ authStorage : { setRuntimeApiKey : vi . fn ( ) } ,
375+ modelRegistry : { } ,
376+ } ) ) ;
377+ sessionCompactImpl
378+ . mockRejectedValueOnce (
379+ Object . assign ( new Error ( "primary compaction rate limited" ) , {
380+ status : 429 ,
381+ code : "rate_limit_exceeded" ,
382+ } ) ,
383+ )
384+ . mockResolvedValueOnce ( {
385+ summary : "overflow fallback summary" ,
386+ firstKeptEntryId : "entry-fallback" ,
387+ tokensBefore : 120 ,
388+ details : { ok : true } ,
389+ } ) ;
390+
391+ const result = await compactEmbeddedPiSessionDirect ( {
392+ sessionId : "session-1" ,
393+ sessionKey : TEST_SESSION_KEY ,
394+ sessionFile : "/tmp/session.jsonl" ,
395+ workspaceDir : "/tmp/workspace" ,
396+ provider : "openai" ,
397+ model : "gpt-primary" ,
398+ trigger : "overflow" ,
399+ modelFallbacksOverride : [ "anthropic/claude-fallback" ] ,
400+ config : {
401+ agents : {
402+ defaults : {
403+ model : {
404+ primary : "openai/gpt-primary" ,
405+ fallbacks : [ ] ,
406+ } ,
407+ } ,
408+ } ,
409+ } as never ,
410+ } ) ;
411+
412+ expect ( result . ok ) . toBe ( true ) ;
413+ expect ( result . result ?. summary ) . toBe ( "overflow fallback summary" ) ;
414+ expect ( resolveModelMock ) . toHaveBeenCalledWith (
415+ "openai" ,
416+ "gpt-primary" ,
417+ expect . any ( String ) ,
418+ expect . anything ( ) ,
419+ ) ;
420+ expect ( resolveModelMock ) . toHaveBeenCalledWith (
421+ "anthropic" ,
422+ "claude-fallback" ,
423+ expect . any ( String ) ,
424+ expect . anything ( ) ,
425+ ) ;
426+ } ) ;
427+
428+ it ( "keeps compaction fallback selection ephemeral" , async ( ) => {
429+ resolveModelMock . mockImplementation ( ( provider = "openai" , modelId = "fake" ) => ( {
430+ model : { provider, api : "responses" , id : modelId , input : [ ] } ,
431+ error : null ,
432+ authStorage : { setRuntimeApiKey : vi . fn ( ) } ,
433+ modelRegistry : { } ,
434+ } ) ) ;
435+ sessionCompactImpl
436+ . mockRejectedValueOnce ( Object . assign ( new Error ( "400 invalid request body" ) , { status : 400 } ) )
437+ . mockResolvedValueOnce ( {
438+ summary : "fallback summary" ,
439+ firstKeptEntryId : "entry-fallback" ,
440+ tokensBefore : 120 ,
441+ details : { ok : true } ,
442+ } ) ;
443+ const config = {
444+ agents : {
445+ defaults : {
446+ model : {
447+ primary : "openai/gpt-primary" ,
448+ fallbacks : [ "anthropic/claude-fallback" ] ,
449+ } ,
450+ } ,
451+ } ,
452+ sessions : {
453+ entries : {
454+ [ TEST_SESSION_KEY ] : {
455+ modelProvider : "openai" ,
456+ model : "gpt-primary" ,
457+ } ,
458+ } ,
459+ } ,
460+ } ;
461+ const configBefore = structuredClone ( config ) ;
462+
463+ const result = await compactEmbeddedPiSessionDirect ( {
464+ sessionId : "session-1" ,
465+ sessionKey : TEST_SESSION_KEY ,
466+ sessionFile : "/tmp/session.jsonl" ,
467+ workspaceDir : "/tmp/workspace" ,
468+ provider : "openai" ,
469+ model : "gpt-primary" ,
470+ config : config as never ,
471+ } ) ;
472+
473+ expect ( result . ok ) . toBe ( true ) ;
474+ expect ( config ) . toEqual ( configBefore ) ;
475+ } ) ;
476+
477+ it ( "preserves explicit compaction.model behavior without session fallback" , async ( ) => {
478+ resolveModelMock . mockImplementation ( ( provider = "openai" , modelId = "fake" ) => ( {
479+ model : { provider, api : "responses" , id : modelId , input : [ ] } ,
480+ error : null ,
481+ authStorage : { setRuntimeApiKey : vi . fn ( ) } ,
482+ modelRegistry : { } ,
483+ } ) ) ;
484+ sessionCompactImpl . mockRejectedValueOnce (
485+ Object . assign ( new Error ( "400 invalid request body" ) , { status : 400 } ) ,
486+ ) ;
487+
488+ const result = await compactEmbeddedPiSessionDirect ( {
489+ sessionId : "session-1" ,
490+ sessionKey : TEST_SESSION_KEY ,
491+ sessionFile : "/tmp/session.jsonl" ,
492+ workspaceDir : "/tmp/workspace" ,
493+ provider : "openai" ,
494+ model : "gpt-primary" ,
495+ config : {
496+ agents : {
497+ defaults : {
498+ model : {
499+ primary : "openai/gpt-primary" ,
500+ fallbacks : [ "anthropic/claude-fallback" ] ,
501+ } ,
502+ compaction : {
503+ model : "azure/compact-primary" ,
504+ } ,
505+ } ,
506+ } ,
507+ } as never ,
508+ } ) ;
509+
510+ expect ( result . ok ) . toBe ( false ) ;
511+ expect ( resolveModelMock ) . toHaveBeenCalledTimes ( 1 ) ;
512+ expect ( resolveModelMock ) . toHaveBeenCalledWith (
513+ "azure" ,
514+ "compact-primary" ,
515+ expect . any ( String ) ,
516+ expect . anything ( ) ,
517+ ) ;
518+ } ) ;
519+
520+ it ( "preserves compaction failure status and code metadata" , async ( ) => {
521+ resolveModelMock . mockImplementation ( ( provider = "openai" , modelId = "fake" ) => ( {
522+ model : { provider, api : "responses" , id : modelId , input : [ ] } ,
523+ error : null ,
524+ authStorage : { setRuntimeApiKey : vi . fn ( ) } ,
525+ modelRegistry : { } ,
526+ } ) ) ;
527+ sessionCompactImpl . mockRejectedValueOnce (
528+ Object . assign ( new Error ( "primary compaction rate limited" ) , {
529+ status : 429 ,
530+ code : "rate_limit_exceeded" ,
531+ } ) ,
532+ ) ;
533+
534+ const result = await compactEmbeddedPiSessionDirect ( {
535+ sessionId : "session-1" ,
536+ sessionKey : TEST_SESSION_KEY ,
537+ sessionFile : "/tmp/session.jsonl" ,
538+ workspaceDir : "/tmp/workspace" ,
539+ provider : "openai" ,
540+ model : "gpt-primary" ,
541+ config : {
542+ agents : {
543+ defaults : {
544+ compaction : {
545+ model : "openai/gpt-primary" ,
546+ } ,
547+ } ,
548+ } ,
549+ } as never ,
550+ } ) ;
551+
552+ expect ( result ) . toMatchObject ( {
553+ ok : false ,
554+ compacted : false ,
555+ failure : {
556+ reason : "rate_limit" ,
557+ status : 429 ,
558+ code : "rate_limit_exceeded" ,
559+ rawError : "primary compaction rate limited" ,
560+ } ,
561+ } ) ;
562+ } ) ;
563+
312564 it ( "emits internal + plugin compaction hooks with counts" , async ( ) => {
313565 hookRunner . hasHooks . mockReturnValue ( true ) ;
314566 await runCompactionHooks ( {
0 commit comments