@@ -998,4 +998,150 @@ describe("wrapCompactorWithCompletenessGate", () => {
998998 expect ( result . output ) . toHaveLength ( 1 ) ;
999999 expect ( result . output [ 0 ] ) . not . toBe ( turns [ 1 ] ) ;
10001000 } ) ;
1001+
1002+ test ( "second fold that grows the spine still compact-succeeds by adopting the prior handoff" , async ( ) => {
1003+ const { wrapCompactorWithCompletenessGate } =
1004+ await import ( "./compaction-archive.js" ) ;
1005+ const { createPruningCompactor } = await import ( "./compactor.js" ) ;
1006+ const { COMPACTED_PREFIX , buildHandoffFold } =
1007+ await import ( "./compaction-handoff.js" ) ;
1008+ const { archive } = memoryArchive ( ) ;
1009+
1010+ const first = buildHandoffFold (
1011+ [
1012+ {
1013+ role : "user" ,
1014+ content : [ { type : "text" , text : "Ship the widget." } ] ,
1015+ timestamp : 1 ,
1016+ } ,
1017+ ] ,
1018+ "first narrative" ,
1019+ ) ;
1020+ const priorSpine = first . spineText ;
1021+ await archive . recordAuthorizedPayload ( {
1022+ kind : "user_message" ,
1023+ payload : "Must never write to /tmp. [[evidence:decision|op|no-tmp]]" ,
1024+ } ) ;
1025+ await archive . recordAuthorizedPayload ( {
1026+ kind : "assistant_text" ,
1027+ payload : "ok" ,
1028+ } ) ;
1029+ await archive . recordAuthorizedPayload ( {
1030+ kind : "assistant_text" ,
1031+ payload : "working" ,
1032+ } ) ;
1033+
1034+ const inner = createPruningCompactor ( {
1035+ keepRecentTurns : 2 ,
1036+ maxAnchorTurns : 0 ,
1037+ summaryMaxChars : 500 ,
1038+ } ) ;
1039+ const wrapped = wrapCompactorWithCompletenessGate ( inner , archive ) ;
1040+ const turns : import ( "@intx/types/runtime" ) . ConversationTurn [ ] = [
1041+ {
1042+ role : "user" ,
1043+ content : [ { type : "text" , text : priorSpine } ] ,
1044+ timestamp : 1 ,
1045+ } ,
1046+ {
1047+ role : "user" ,
1048+ content : [ { type : "text" , text : "Continue the widget." } ] ,
1049+ timestamp : 2 ,
1050+ } ,
1051+ {
1052+ role : "assistant" ,
1053+ content : [ { type : "text" , text : "working" } ] ,
1054+ timestamp : 3 ,
1055+ } ,
1056+ {
1057+ role : "user" ,
1058+ content : [
1059+ {
1060+ type : "text" ,
1061+ text : "Must never write to /tmp. [[evidence:decision|op|no-tmp]]" ,
1062+ } ,
1063+ ] ,
1064+ timestamp : 4 ,
1065+ } ,
1066+ {
1067+ role : "assistant" ,
1068+ content : [ { type : "text" , text : "ok" } ] ,
1069+ timestamp : 5 ,
1070+ } ,
1071+ {
1072+ role : "user" ,
1073+ content : [ { type : "text" , text : "keep one" } ] ,
1074+ timestamp : 6 ,
1075+ } ,
1076+ {
1077+ role : "assistant" ,
1078+ content : [ { type : "text" , text : "keep two" } ] ,
1079+ timestamp : 7 ,
1080+ } ,
1081+ ] ;
1082+ const result = await wrapped . apply ( turns , ctx ) ;
1083+ expect ( result . record . reason ) . not . toBe ( "incomplete-evidence-archive" ) ;
1084+ const spine = result . output [ 0 ] ?. content . find ( ( b ) => b . type === "text" ) ;
1085+ expect ( spine ?. type ) . toBe ( "text" ) ;
1086+ if ( spine ?. type !== "text" ) throw new Error ( "unreachable" ) ;
1087+ expect ( spine . text . startsWith ( COMPACTED_PREFIX ) ) . toBe ( true ) ;
1088+ expect ( spine . text ) . toContain ( "Must never write to /tmp." ) ;
1089+ expect ( spine . text ) . toContain ( "[[evidence:decision|op|no-tmp]]" ) ;
1090+ } ) ;
1091+
1092+ test ( "pre-format fat Compacted prior context summaries still compact" , async ( ) => {
1093+ const { wrapCompactorWithCompletenessGate } =
1094+ await import ( "./compaction-archive.js" ) ;
1095+ const { createPruningCompactor } = await import ( "./compactor.js" ) ;
1096+ const { COMPACTED_PREFIX } = await import ( "./compaction-handoff.js" ) ;
1097+ const { archive } = memoryArchive ( ) ;
1098+ const fatSummary = `${ COMPACTED_PREFIX } \nLong pre-format narrative about the auth migration and every file that was touched.` ;
1099+ await archive . recordAuthorizedPayload ( {
1100+ kind : "user_message" ,
1101+ payload : "next ask after the old summary" ,
1102+ } ) ;
1103+ await archive . recordAuthorizedPayload ( {
1104+ kind : "assistant_text" ,
1105+ payload : "working" ,
1106+ } ) ;
1107+ const inner = createPruningCompactor ( {
1108+ keepRecentTurns : 2 ,
1109+ maxAnchorTurns : 0 ,
1110+ summaryMaxChars : 500 ,
1111+ } ) ;
1112+ const wrapped = wrapCompactorWithCompletenessGate ( inner , archive ) ;
1113+ const turns : import ( "@intx/types/runtime" ) . ConversationTurn [ ] = [
1114+ {
1115+ role : "user" ,
1116+ content : [ { type : "text" , text : fatSummary } ] ,
1117+ timestamp : 1 ,
1118+ } ,
1119+ {
1120+ role : "user" ,
1121+ content : [ { type : "text" , text : "next ask after the old summary" } ] ,
1122+ timestamp : 2 ,
1123+ } ,
1124+ {
1125+ role : "assistant" ,
1126+ content : [ { type : "text" , text : "working" } ] ,
1127+ timestamp : 3 ,
1128+ } ,
1129+ {
1130+ role : "user" ,
1131+ content : [ { type : "text" , text : "keep one" } ] ,
1132+ timestamp : 4 ,
1133+ } ,
1134+ {
1135+ role : "assistant" ,
1136+ content : [ { type : "text" , text : "keep two" } ] ,
1137+ timestamp : 5 ,
1138+ } ,
1139+ ] ;
1140+ const result = await wrapped . apply ( turns , ctx ) ;
1141+ expect ( result . record . reason ) . not . toBe ( "incomplete-evidence-archive" ) ;
1142+ const spine = result . output [ 0 ] ?. content . find ( ( b ) => b . type === "text" ) ;
1143+ expect ( spine ?. type ) . toBe ( "text" ) ;
1144+ if ( spine ?. type !== "text" ) throw new Error ( "unreachable" ) ;
1145+ expect ( spine . text . startsWith ( COMPACTED_PREFIX ) ) . toBe ( true ) ;
1146+ } ) ;
10011147} ) ;
0 commit comments