@@ -4,7 +4,7 @@ import os from "node:os";
44import path from "node:path" ;
55import { createPruningCompactor } from "./compactor.js" ;
66import { condenseTurns } from "./summarizer.js" ;
7- import { HANDOFF_LATEST_KEY } from "./compaction-handoff.js" ;
7+ import { COMPACTED_PREFIX , HANDOFF_LATEST_KEY } from "./compaction-handoff.js" ;
88import {
99 createCompactionArchive ,
1010 wrapCompactorWithCompletenessGate ,
@@ -786,6 +786,27 @@ describe("CL-9007 budgeted tail (shared auto+manual pipeline)", () => {
786786 . join ( "\n" ) ;
787787 }
788788
789+ function liveTokenEstimate ( turns : ConversationTurn [ ] ) : number {
790+ let chars = 0 ;
791+ for ( const turn of turns ) {
792+ for ( const block of turn . content ) {
793+ if ( block . type === "text" ) chars += block . text . length ;
794+ else if ( block . type === "tool_call" )
795+ chars += JSON . stringify ( block . arguments ) . length ;
796+ else if ( block . type === "tool_result" ) {
797+ for ( const part of block . content ) {
798+ if ( part . type === "text" ) chars += part . text . length ;
799+ }
800+ }
801+ }
802+ }
803+ return Math . ceil ( chars / 4 ) ;
804+ }
805+
806+ function countStructuredTailExcerpts ( text : string ) : number {
807+ return text . match ( / \[ t a i l - s h o r t e n e d \d + → / g) ?. length ?? 0 ;
808+ }
809+
789810 test ( "large tool outputs in the tail are shortened rather than copied verbatim" , async ( ) => {
790811 const result = await tailCompactor ( ) . apply ( tailSession ( ) , mockStrategyCtx ) ;
791812 expect ( result . record . reason . startsWith ( "compacted" ) ) . toBe ( true ) ;
@@ -840,6 +861,57 @@ describe("CL-9007 budgeted tail (shared auto+manual pipeline)", () => {
840861 compactionShape : { tailBudgetTokens : 7500 , pairSafe : true } ,
841862 } ) ;
842863 } ) ;
864+
865+ test ( "budget-swallow still emits excerpted tail copies; live tokens ≤ budget; shortenedToolOutputs matches live sentinels" , async ( ) => {
866+ const dump = "z" . repeat ( 12_000 ) ;
867+ const turns : ConversationTurn [ ] = [
868+ textTurn ( "user" , "do the work" ) ,
869+ ...pairTurns ( "a" , "read_file" , { path : "src/a.ts" } , dump ) ,
870+ ...pairTurns ( "b" , "read_file" , { path : "src/b.ts" } , dump ) ,
871+ ...pairTurns ( "c" , "read_file" , { path : "src/c.ts" } , dump ) ,
872+ textTurn ( "user" , "newest ask" ) ,
873+ textTurn ( "assistant" , "newest reply" ) ,
874+ ] ;
875+ const result = await createPruningCompactor ( {
876+ keepRecentTurns : 2 ,
877+ compactionShape : { tailBudgetTokens : 7500 } ,
878+ summarize : async ( ) => {
879+ throw new Error ( "must not invent a summary on the budget-swallow path" ) ;
880+ } ,
881+ } ) . apply ( turns , mockStrategyCtx ) ;
882+
883+ expect ( result . record . reason ) . toBe ( "no compaction needed" ) ;
884+ expect ( allText ( result . output ) ) . not . toContain ( COMPACTED_PREFIX ) ;
885+ const live = liveResultText ( result . output ) ;
886+ expect ( live ) . not . toContain ( dump ) ;
887+ expect ( result . record . decisions ) . toMatchObject ( { shortenedToolOutputs : 3 } ) ;
888+ expect ( countStructuredTailExcerpts ( live ) ) . toBe ( 3 ) ;
889+ expect ( liveTokenEstimate ( result . output ) ) . toBeLessThanOrEqual ( 7500 ) ;
890+ } ) ;
891+
892+ test ( "a body containing the substring but not the structured marker is still excerpted" , async ( ) => {
893+ const bait = "[tail-shortened " ;
894+ const body = `${ bait } in the docs\n${ "z" . repeat ( 8000 ) } ` ;
895+ expect ( body . includes ( bait ) ) . toBe ( true ) ;
896+ expect ( / \[ t a i l - s h o r t e n e d \d + → / . test ( body ) ) . toBe ( false ) ;
897+
898+ const turns : ConversationTurn [ ] = [
899+ textTurn ( "user" , "goal" ) ,
900+ ...pairTurns ( "bait" , "read_file" , { path : "src/bait.ts" } , body ) ,
901+ textTurn ( "user" , "newest ask" ) ,
902+ textTurn ( "assistant" , "newest reply" ) ,
903+ ] ;
904+ const result = await createPruningCompactor ( {
905+ keepRecentTurns : 2 ,
906+ compactionShape : { tailBudgetTokens : 7500 } ,
907+ } ) . apply ( turns , mockStrategyCtx ) ;
908+
909+ const live = liveResultText ( result . output ) ;
910+ expect ( live ) . toMatch ( / \[ t a i l - s h o r t e n e d \d + → / ) ;
911+ expect ( live ) . not . toContain ( "z" . repeat ( 8000 ) ) ;
912+ expect ( result . record . decisions ) . toMatchObject ( { shortenedToolOutputs : 1 } ) ;
913+ expect ( countStructuredTailExcerpts ( live ) ) . toBe ( 1 ) ;
914+ } ) ;
843915} ) ;
844916
845917describe ( "continuation facts survive many folds" , ( ) => {
0 commit comments