@@ -103,8 +103,8 @@ describe("compaction governor", () => {
103103 expect ( actions ?. some ( ( a ) => a . type === "infer" ) ) . toBe ( false ) ;
104104 expect ( continuations ) . toBe ( 1 ) ;
105105
106- expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( true ) ;
107- expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( false ) ;
106+ expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( "infer" ) ;
107+ expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBeNull ( ) ;
108108 } ) ;
109109
110110 test ( "stays inert below the threshold or with few turns" , ( ) => {
@@ -134,7 +134,7 @@ describe("compaction governor", () => {
134134 test ( "recovers from context overflow a bounded number of times" , ( ) => {
135135 const governor = createCompactionGovernor ( ( ) => { } ) ;
136136 expect ( governor . interceptOverflow ( overflowError ( ) , capabilities ) ) . not . toBeNull ( ) ;
137- expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( true ) ;
137+ expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( "infer" ) ;
138138 expect ( governor . interceptOverflow ( overflowError ( ) , capabilities ) ) . not . toBeNull ( ) ;
139139 expect ( governor . interceptOverflow ( overflowError ( ) , capabilities ) ) . toBeNull ( ) ;
140140
@@ -162,6 +162,38 @@ describe("compaction governor", () => {
162162 expect ( governor . interceptIdleContinuation ( emptyMessage ( ) , capabilities ) ) . toBeNull ( ) ;
163163 } ) ;
164164
165+ // Idle compact with an empty continuation previously left postCompactInfer
166+ // unset, so resumeAfterCompact never fired and notePostCompact never ran —
167+ // the Ctx meter stayed on pre-compact lastTurnUsage until the next user turn.
168+ test ( "idle empty compact syncs the meter after shrink without a following user turn" , ( ) => {
169+ let continuations = 0 ;
170+ const governor = createCompactionGovernor ( ( ) => continuations ++ ) ;
171+ const large = turnsOfLength ( 10 , 200 ) ;
172+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , large ) ;
173+ expect ( governor . usingEstimate ) . toBe ( false ) ;
174+ const before = governor . estimatedTokens ;
175+
176+ governor . noteIdleTurn ( inferenceDone ( overThreshold ) , [ { type : "reply" , content : "done" } ] ) ;
177+ expect ( continuations ) . toBe ( 1 ) ;
178+
179+ const actions = governor . interceptIdleContinuation ( emptyMessage ( ) , capabilities ) ;
180+ expect ( actions ) . toEqual ( [
181+ { type : "compact" , compactor : "pruning-compactor" , reason : "context-threshold" } ,
182+ ] as ReactorAction [ ] ) ;
183+ // A second continuation re-enters decide after the compact cycle so the
184+ // governor can adopt the shrunk turns — without starting a new inference.
185+ expect ( continuations ) . toBe ( 2 ) ;
186+
187+ const shrunk = turnsOfLength ( 3 , 20 ) ;
188+ // resumeAfterCompact must arm the meter-only path (not infer) for empty idle.
189+ expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( "meter" ) ;
190+ governor . notePostCompact ( shrunk ) ;
191+
192+ expect ( governor . usingEstimate ) . toBe ( true ) ;
193+ expect ( governor . estimatedTokens ) . toBeLessThan ( before ) ;
194+ expect ( governor . estimatedTokens ) . toBe ( governor . syncFromTurns ( shrunk ) ) ;
195+ } ) ;
196+
165197 test ( "an operator message that races the idle continuation still compacts, then re-infers" , ( ) => {
166198 let continuations = 0 ;
167199 const governor = createCompactionGovernor ( ( ) => continuations ++ ) ;
@@ -177,7 +209,7 @@ describe("compaction governor", () => {
177209 // A second continuation is requested so the operator message gets answered
178210 // after the compact cycle.
179211 expect ( continuations ) . toBe ( 2 ) ;
180- expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( true ) ;
212+ expect ( governor . resumeAfterCompact ( emptyMessage ( ) ) ) . toBe ( "infer" ) ;
181213 } ) ;
182214
183215 test ( "idle turns with follow-up work or under threshold never arm idle compaction" , ( ) => {
@@ -337,6 +369,25 @@ describe("compaction governor", () => {
337369 expect ( governor . interceptActions ( toolDone ( ) , inferAction , capabilities ) ) . toBeNull ( ) ;
338370 } ) ;
339371
372+ test ( "notePostCompact syncs the shrunk turns and keeps the estimate authoritative until the next inference.done" , ( ) => {
373+ const governor = createCompactionGovernor ( ( ) => { } ) ;
374+ const large = turnsOfLength ( 10 , 200 ) ;
375+ governor . noteInferenceDone ( inferenceDone ( overThreshold ) , large ) ;
376+ expect ( governor . usingEstimate ) . toBe ( false ) ;
377+ const before = governor . estimatedTokens ;
378+
379+ const shrunk = turnsOfLength ( 3 , 20 ) ;
380+ governor . notePostCompact ( shrunk ) ;
381+
382+ expect ( governor . usingEstimate ) . toBe ( true ) ;
383+ expect ( governor . estimatedTokens ) . toBeLessThan ( before ) ;
384+ expect ( governor . estimatedTokens ) . toBe ( governor . syncFromTurns ( shrunk ) ) ;
385+
386+ // Provider-reported usage on the next turn clears the estimate flag.
387+ governor . noteInferenceDone ( inferenceDone ( 1000 ) , shrunk ) ;
388+ expect ( governor . usingEstimate ) . toBe ( false ) ;
389+ } ) ;
390+
340391 test ( "does not re-arm after a compact that remains over the high watermark" , ( ) => {
341392 const governor = createCompactionGovernor ( ( ) => { } ) ;
342393 governor . noteInferenceDone ( inferenceDone ( overThreshold ) , tenTurns ) ;
0 commit comments