@@ -245,6 +245,91 @@ test("buildCompressibleRanges: does NOT split before a group reaches 3 messages"
245245 assert . equal ( ranges . compressible [ 0 ] ! . count , 3 ) ;
246246} ) ;
247247
248+ // ─── Surface-replacing hosts (#207): array-adjacency segmentation ─────────────
249+
250+ test ( "buildCompressibleRanges: ref-map holes do NOT fragment ranges (surface-replace host)" , ( ) => {
251+ // Turn 1 assigns m00001..m00005. A compression then durably replaces b..d on
252+ // the surface: they leave the next turn's message array while their refs stay
253+ // assigned (append-only map, never pruned). Ref-arithmetic segmentation saw
254+ // 5 > 1+1 and emitted two singletons; array adjacency sees a and e as array
255+ // neighbors and emits one range.
256+ const a = msg ( "a" , "x" . repeat ( 2000 ) ) ;
257+ const e = msg ( "e" , "v" . repeat ( 2000 ) ) ;
258+ const state = assignAll ( [ a , msg ( "b" , "y" ) , msg ( "c" , "z" ) , msg ( "d" , "w" ) , e ] ) ;
259+ const ranges = buildCompressibleRanges ( [ a , e ] , state , config ( ) ) ;
260+ assert . equal ( ranges . compressible . length , 1 , "holes in the ref map must not split the range" ) ;
261+ assert . equal ( ranges . compressible [ 0 ] ! . startRef , "m00001" ) ;
262+ assert . equal ( ranges . compressible [ 0 ] ! . endRef , "m00005" ) ;
263+ assert . equal ( ranges . compressible [ 0 ] ! . count , 2 ) ;
264+ } ) ;
265+
266+ test ( "buildCompressibleRanges: mid-array summary node extends the range, never a descending pair" , ( ) => {
267+ // Surface-replace host inserts the model's summary node at the replaced span's
268+ // position. assignRefs gives it a fresh HIGH ref (m00006) while the trailing
269+ // message keeps m00005 — ref arithmetic flushed the head and emitted the
270+ // nonsense pair m00006..m00005. Array adjacency treats the node as a regular
271+ // entry: one ascending range over the whole span.
272+ const a = msg ( "a" , "x" . repeat ( 2000 ) , "assistant" ) ;
273+ const d = msg ( "d" , "w" . repeat ( 2000 ) , "assistant" ) ;
274+ const e = msg ( "e" , "v" . repeat ( 2000 ) , "assistant" ) ;
275+ const summary = msg ( "s" , "Summary of the compressed span: did the work." , "assistant" ) ;
276+ const s1 = assignAll ( [ a , msg ( "b" , "y" ) , msg ( "c" , "z" ) , d , e ] ) ;
277+ const state = assignAll ( [ a , summary , d , e ] , s1 ) ;
278+ assert . equal ( state . messageRefs . byRaw [ "s" ] , "m00006" , "summary node gets a fresh high ref" ) ;
279+ const ranges = buildCompressibleRanges ( [ a , summary , d , e ] , state , config ( ) ) ;
280+ assert . equal ( ranges . compressible . length , 1 , "mid-array summary node must not flush the range" ) ;
281+ assert . equal ( ranges . compressible [ 0 ] ! . startRef , "m00001" ) ;
282+ assert . equal ( ranges . compressible [ 0 ] ! . endRef , "m00005" ) ;
283+ assert . equal ( ranges . compressible [ 0 ] ! . count , 4 ) ;
284+ } ) ;
285+
286+ test ( "buildCompressibleRanges: synthetic (covered) summary node still splits ranges" , ( ) => {
287+ // A summary node detected via the "[Compressed conversation section]" prefix
288+ // (or active-block coverage) is intentionally skipped — a skipped numbered
289+ // message between two entries is a real interruption, unlike a numeric hole
290+ // where nothing is physically present between them.
291+ const a = msg ( "a" , "x" . repeat ( 2000 ) ) ;
292+ const e = msg ( "e" , "v" . repeat ( 2000 ) ) ;
293+ const synthetic = msg ( "s" , "[Compressed conversation section] earlier work summarized." , "assistant" ) ;
294+ const s1 = assignAll ( [ a , msg ( "b" , "y" ) , msg ( "c" , "z" ) , msg ( "d" , "w" ) , e ] ) ;
295+ const state = assignAll ( [ a , synthetic , e ] , s1 ) ;
296+ const ranges = buildCompressibleRanges ( [ a , synthetic , e ] , state , config ( ) ) ;
297+ assert . equal ( ranges . compressible . length , 2 , "covered span must not be folded into the new range" ) ;
298+ assert . equal ( ranges . compressible [ 0 ] ! . startRef , "m00001" ) ;
299+ assert . equal ( ranges . compressible [ 0 ] ! . endRef , "m00001" ) ;
300+ assert . equal ( ranges . compressible [ 1 ] ! . startRef , "m00005" ) ;
301+ assert . equal ( ranges . compressible [ 1 ] ! . endRef , "m00005" ) ;
302+ } ) ;
303+
304+ test ( "buildCompressibleRanges: protected groups segment by array adjacency too" , ( ) => {
305+ // A compressible message physically between two protected tool-calls splits
306+ // them (dense-host behavior preserved); a bare ref-map hole does not.
307+ const p1 = toolMsg ( "p1" , "skill" ) ;
308+ const p2 = toolMsg ( "p2" , "skill" ) ;
309+ const dense = createInitialState ( ) ;
310+ dense . messageRefs = {
311+ byRaw : { p1 : "m00001" , x : "m00002" , p2 : "m00003" } ,
312+ byRef : { m00001 : "p1" , m00002 : "x" , m00003 : "p2" } ,
313+ } ;
314+ const denseRanges = buildCompressibleRanges (
315+ [ p1 , msg ( "x" , "y" ) , p2 ] ,
316+ dense ,
317+ config ( { protectedTools : [ "skill" ] } ) ,
318+ ) ;
319+ assert . equal ( denseRanges . protected . length , 2 , "interleaved non-protected message splits protected groups" ) ;
320+
321+ const holed = createInitialState ( ) ;
322+ holed . messageRefs = {
323+ byRaw : { p1 : "m00001" , p2 : "m00005" } ,
324+ byRef : { m00001 : "p1" , m00005 : "p2" } ,
325+ } ;
326+ const holedRanges = buildCompressibleRanges ( [ p1 , p2 ] , holed , config ( { protectedTools : [ "skill" ] } ) ) ;
327+ assert . equal ( holedRanges . protected . length , 1 , "hole in the ref map must not split the protected group" ) ;
328+ assert . equal ( holedRanges . protected [ 0 ] ! . count , 2 ) ;
329+ assert . equal ( holedRanges . protected [ 0 ] ! . startRef , "m00001" ) ;
330+ assert . equal ( holedRanges . protected [ 0 ] ! . endRef , "m00005" ) ;
331+ } ) ;
332+
248333// ─── Integration: 19-token bug fix ─────────────────────────────────────────────
249334
250335test ( "integration: tiny ranges are suppressed — fixes the 19-token compression bug" , ( ) => {
0 commit comments