@@ -936,6 +936,166 @@ describe("compaction governor", () => {
936936 } ) ;
937937} ) ;
938938
939+ describe ( "post-compact above-threshold latch (CL-9006)" , ( ) => {
940+ // Wide resume gap: twice the old resume delta — a full warning band of
941+ // growth past the post-compact measurement before a still-over session may
942+ // fold again (danger-anchored; see COMPACTION_WIDE_RESUME_FRACTION).
943+ const wideDelta = resumeDelta * 2 ;
944+
945+ test ( "resume-delta-scale growth while still over threshold does not re-arm" , ( ) => {
946+ const governor = createCompactionGovernor ( ( ) => undefined ) ;
947+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
948+ expect (
949+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
950+ ) . not . toBeNull ( ) ;
951+
952+ // Post-compact measurement stays over the high watermark: the latch sets.
953+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
954+ // Growth by the old resume delta must NOT re-arm on its own.
955+ governor . noteInferenceDone (
956+ inferenceDone ( overThreshold + resumeDelta ) ,
957+ tenTurns ,
958+ ) ;
959+ expect (
960+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
961+ ) . toBeNull ( ) ;
962+ // The idle path shares the same latch.
963+ governor . noteIdleTurn ( inferenceDone ( overThreshold + resumeDelta ) , [
964+ { type : "reply" , content : "done" } ,
965+ ] ) ;
966+ expect (
967+ governor . interceptIdleContinuation ( emptyMessage ( ) , capabilities ) ,
968+ ) . toBeNull ( ) ;
969+ } ) ;
970+
971+ test ( "a wide resume gap while still over threshold re-arms" , ( ) => {
972+ const governor = createCompactionGovernor ( ( ) => undefined ) ;
973+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
974+ expect (
975+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
976+ ) . not . toBeNull ( ) ;
977+
978+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
979+ governor . noteInferenceDone (
980+ inferenceDone ( overThreshold + wideDelta ) ,
981+ tenTurns ,
982+ ) ;
983+ const actions = governor . interceptActions (
984+ toolDone ( ) ,
985+ inferAction ,
986+ capabilities ,
987+ ) ;
988+ expect ( actions ) . not . toBeNull ( ) ;
989+ expect ( actions ?. some ( ( a ) => a . type === "compact" ) ) . toBe ( true ) ;
990+ } ) ;
991+
992+ test ( "the consecutive-compact cap holds across tool-call occupancy" , ( ) => {
993+ const governor = createCompactionGovernor ( ( ) => undefined ) ;
994+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
995+ expect (
996+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
997+ ) . not . toBeNull ( ) ;
998+
999+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
1000+ governor . noteInferenceDone (
1001+ inferenceDone ( overThreshold + wideDelta ) ,
1002+ tenTurns ,
1003+ ) ;
1004+ expect (
1005+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
1006+ ) . not . toBeNull ( ) ;
1007+
1008+ // Post-compact measurement still over: tool-call occupancy must not reset
1009+ // the cap, even past a wide gap.
1010+ governor . noteInferenceDone (
1011+ inferenceDone ( overThreshold + wideDelta ) ,
1012+ tenTurns ,
1013+ ) ;
1014+ governor . noteInferenceDone (
1015+ inferenceDoneWithTools ( overThreshold + 2 * wideDelta ) ,
1016+ tenTurns ,
1017+ ) ;
1018+ expect (
1019+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
1020+ ) . toBeNull ( ) ;
1021+ // The idle path shares the same cap.
1022+ governor . noteIdleTurn (
1023+ inferenceDoneWithTools ( overThreshold + 2 * wideDelta ) ,
1024+ [ { type : "reply" , content : "done" } ] ,
1025+ ) ;
1026+ expect (
1027+ governor . interceptIdleContinuation ( emptyMessage ( ) , capabilities ) ,
1028+ ) . toBeNull ( ) ;
1029+ } ) ;
1030+
1031+ test ( "overflow recoveries are bounded across still-over measurements" , ( ) => {
1032+ const governor = createCompactionGovernor ( ( ) => undefined ) ;
1033+ expect (
1034+ governor . interceptOverflow ( overflowError ( ) , capabilities ) ,
1035+ ) . not . toBeNull ( ) ;
1036+ expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( "infer" ) ;
1037+
1038+ // Still-over post-compact measurement: no relief, budget stays spent.
1039+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
1040+ expect (
1041+ governor . interceptOverflow ( overflowError ( ) , capabilities ) ,
1042+ ) . not . toBeNull ( ) ;
1043+ expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( "infer" ) ;
1044+
1045+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
1046+ expect ( governor . interceptOverflow ( overflowError ( ) , capabilities ) ) . toBeNull ( ) ;
1047+ } ) ;
1048+
1049+ test ( "an under-threshold fold restores the overflow budget" , ( ) => {
1050+ const governor = createCompactionGovernor ( ( ) => undefined ) ;
1051+ expect (
1052+ governor . interceptOverflow ( overflowError ( ) , capabilities ) ,
1053+ ) . not . toBeNull ( ) ;
1054+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
1055+ expect (
1056+ governor . interceptOverflow ( overflowError ( ) , capabilities ) ,
1057+ ) . not . toBeNull ( ) ;
1058+ // Fold evidence: usage back under the watermark restores the budget.
1059+ governor . noteInferenceDone ( inferenceDone ( 1000 ) , tenTurns ) ;
1060+ expect (
1061+ governor . interceptOverflow ( overflowError ( ) , capabilities ) ,
1062+ ) . not . toBeNull ( ) ;
1063+ } ) ;
1064+
1065+ test ( "auto re-arm after an operator compact uses the identical latch" , ( ) => {
1066+ const governor = createCompactionGovernor ( ( ) => undefined ) ;
1067+ governor . noteInferenceDone ( inferenceDone ( 1000 ) , tenTurns ) ;
1068+ expect ( governor . requestManual ( "" , { inFlight : true } ) ) . toBe ( "armed" ) ;
1069+ expect (
1070+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
1071+ ) . not . toBeNull ( ) ;
1072+
1073+ // Post-operator-compact measurement stays over: small growth must not
1074+ // re-arm the automatic path.
1075+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
1076+ governor . noteInferenceDone (
1077+ inferenceDone ( overThreshold + resumeDelta ) ,
1078+ tenTurns ,
1079+ ) ;
1080+ expect (
1081+ governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ,
1082+ ) . toBeNull ( ) ;
1083+
1084+ // Wide gap re-arms identically to the automatic path.
1085+ governor . noteInferenceDone (
1086+ inferenceDone ( overThreshold + wideDelta ) ,
1087+ tenTurns ,
1088+ ) ;
1089+ const actions = governor . interceptActions (
1090+ toolDone ( ) ,
1091+ inferAction ,
1092+ capabilities ,
1093+ ) ;
1094+ expect ( actions ) . not . toBeNull ( ) ;
1095+ expect ( actions ?. some ( ( a ) => a . type === "compact" ) ) . toBe ( true ) ;
1096+ } ) ;
1097+ } ) ;
1098+
9391099describe ( "cache expiry never folds (CL-8914)" , ( ) => {
9401100 const MINUTE_MS = 60_000 ;
9411101
0 commit comments