@@ -985,6 +985,111 @@ describe("createSessionPruningCompactor stub fallback", () => {
985985 ) ;
986986 expect ( handoffs ) . toEqual ( [ ] ) ;
987987 } ) ;
988+
989+ test ( "abort then reset during certifyRange does not record a phantom handoff" , async ( ) => {
990+ const notices : string [ ] = [ ] ;
991+ const folds : { stub : boolean } [ ] = [ ] ;
992+ const summarize = createModelSummarizer ( {
993+ getSource : ( ) =>
994+ ( {
995+ id : "test" ,
996+ provider : "openai" ,
997+ model : "test-model" ,
998+ baseURL : "http://localhost:1" ,
999+ credentialId : "test" ,
1000+ } ) as never ,
1001+ complete : async ( ) => {
1002+ throw new Error ( "model unreachable" ) ;
1003+ } ,
1004+ } ) ;
1005+ const dir = await mkdtemp ( join ( tmpdir ( ) , "compaction-gate-abort-reset-" ) ) ;
1006+ const blobs = new Map < string , Uint8Array > ( ) ;
1007+ const archive = createCompactionArchive ( {
1008+ sessionId : "sess-gate-abort-reset" ,
1009+ contextDir : dir ,
1010+ writeBlob : async ( key , bytes ) => {
1011+ blobs . set ( key , bytes ) ;
1012+ } ,
1013+ readBlob : async ( key ) => {
1014+ const bytes = blobs . get ( key ) ;
1015+ if ( bytes === undefined ) throw new Error ( `missing ${ key } ` ) ;
1016+ return bytes ;
1017+ } ,
1018+ } ) ;
1019+ const now = Date . now ( ) ;
1020+ const many = Array . from ( { length : 8 } , ( _ , i ) => ( {
1021+ role : ( i % 2 === 0 ? "user" : "assistant" ) as "user" | "assistant" ,
1022+ content : [ { type : "text" as const , text : `t${ i } ` } ] ,
1023+ timestamp : now ,
1024+ } ) ) ;
1025+ for ( const turn of many ) {
1026+ const block = turn . content [ 0 ] ;
1027+ if ( block ?. type !== "text" ) continue ;
1028+ await archive . recordAuthorizedPayload ( {
1029+ kind : turn . role === "assistant" ? "assistant_text" : "user_message" ,
1030+ payload : block . text ,
1031+ } ) ;
1032+ }
1033+ const lifecycle = createCompactionLifecycle ( ) ;
1034+ const getSignal = ( ) => lifecycle . getSignal ( ) ;
1035+ const isAborted = ( ) => lifecycle . getSignal ( ) . aborted ;
1036+ let releaseGated : ( ) => void = ( ) => undefined ;
1037+ const gatedFinished = new Promise < void > ( ( resolve ) => {
1038+ releaseGated = resolve ;
1039+ } ) ;
1040+ const abortingArchive = {
1041+ ...archive ,
1042+ certifyRange : async ( ids : readonly string [ ] ) => {
1043+ const certificate = await archive . certifyRange ( ids ) ;
1044+ lifecycle . abortCompaction ( "operator interrupt" ) ;
1045+ // onBuilt reset() mints a fresh controller; the in-flight apply
1046+ // must still treat this compact as aborted.
1047+ lifecycle . reset ( ) ;
1048+ return certificate ;
1049+ } ,
1050+ } ;
1051+ const wrapped = lifecycle . wrapCompactor (
1052+ createSessionPruningCompactor ( {
1053+ summarize,
1054+ onFolded : ( info ) => folds . push ( info ) ,
1055+ onFailure : ( text ) => notices . push ( text ) ,
1056+ getSignal,
1057+ isAborted,
1058+ compactionShape : { tailBudgetTokens : 1 } ,
1059+ wrapPruning : ( pruning ) => {
1060+ const gated = wrapCompactorWithCompletenessGate (
1061+ pruning ,
1062+ abortingArchive ,
1063+ { getSignal, isAborted } ,
1064+ ) ;
1065+ return {
1066+ name : gated . name ,
1067+ version : gated . version ,
1068+ apply : async ( turns , ctx ) => {
1069+ try {
1070+ return await gated . apply ( turns , ctx ) ;
1071+ } finally {
1072+ releaseGated ( ) ;
1073+ }
1074+ } ,
1075+ } ;
1076+ } ,
1077+ } ) ,
1078+ ) ;
1079+ const result = await wrapped . apply ( many as never , {
1080+ state : { } as never ,
1081+ trigger : "test" ,
1082+ } ) ;
1083+ await gatedFinished ;
1084+ expect ( result . output ) . toBe ( many ) ;
1085+ expect ( result . record . reason ) . toBe ( COMPACTION_ABORTED_REASON ) ;
1086+ expect ( folds ) . toEqual ( [ ] ) ;
1087+ expect ( notices ) . toEqual ( [ ] ) ;
1088+ const handoffs = ( await archive . listOccurrences ( ) ) . filter (
1089+ ( occurrence ) => occurrence . provenance === "compaction-handoff" ,
1090+ ) ;
1091+ expect ( handoffs ) . toEqual ( [ ] ) ;
1092+ } ) ;
9881093} ) ;
9891094
9901095describe ( "buildCompactionContinuationMessage" , ( ) => {
0 commit comments