@@ -295,6 +295,179 @@ describe("rewriteTranscriptEntriesInSessionManager", () => {
295295 }
296296 expect ( replayedAssistant . content ) . toEqual ( [ { type : "text" , text : "summarized" } ] ) ;
297297 } ) ;
298+
299+ it ( "dedupes byte-identical replayed messages so suffix replay does not clone user entries" , ( ) => {
300+ const sessionManager = SessionManager . inMemory ( ) ;
301+ const entryIds = appendSessionMessages ( sessionManager , [
302+ asAppendMessage ( { role : "user" , content : "anchor" , timestamp : 1 } ) ,
303+ asAppendMessage ( {
304+ role : "toolResult" ,
305+ toolCallId : "call_1" ,
306+ toolName : "read" ,
307+ content : createTextContent ( "x" . repeat ( 8_000 ) ) ,
308+ isError : false ,
309+ timestamp : 2 ,
310+ } ) ,
311+ asAppendMessage ( { role : "user" , content : "duplicate user message" , timestamp : 3 } ) ,
312+ asAppendMessage ( { role : "user" , content : "duplicate user message" , timestamp : 3 } ) ,
313+ asAppendMessage ( { role : "assistant" , content : createTextContent ( "tail" ) , timestamp : 4 } ) ,
314+ ] ) ;
315+
316+ const result = rewriteTranscriptEntriesInSessionManager ( {
317+ sessionManager,
318+ replacements : [
319+ {
320+ entryId : entryIds [ 1 ] ,
321+ message : createToolResultReplacement ( "read" , "[externalized file_123]" , 2 ) ,
322+ } ,
323+ ] ,
324+ } ) ;
325+
326+ expect ( result . changed ) . toBe ( true ) ;
327+ const duplicateCount = getBranchMessages ( sessionManager ) . filter (
328+ ( message ) => message . role === "user" && message . content === "duplicate user message" ,
329+ ) . length ;
330+ expect ( duplicateCount ) . toBe ( 1 ) ;
331+ } ) ;
332+
333+ it ( "keeps distinct compactions with identical summaries but different kept boundaries" , ( ) => {
334+ const sessionManager = SessionManager . inMemory ( ) ;
335+ const entryIds = appendSessionMessages ( sessionManager , [
336+ asAppendMessage ( {
337+ role : "toolResult" ,
338+ toolCallId : "call_1" ,
339+ toolName : "read" ,
340+ content : createTextContent ( "x" . repeat ( 8_000 ) ) ,
341+ isError : false ,
342+ timestamp : 1 ,
343+ } ) ,
344+ asAppendMessage ( { role : "assistant" , content : createTextContent ( "kept one" ) , timestamp : 2 } ) ,
345+ asAppendMessage ( { role : "assistant" , content : createTextContent ( "kept two" ) , timestamp : 3 } ) ,
346+ ] ) ;
347+ const keptOneId = entryIds [ 1 ] ;
348+ const keptTwoId = entryIds [ 2 ] ;
349+ // Two compactions sharing summary/tokens/details/fromHook but pointing at
350+ // different kept boundaries must not collapse during replay.
351+ sessionManager . appendCompaction ( "summary" , keptOneId , 100 ) ;
352+ sessionManager . appendCompaction ( "summary" , keptTwoId , 100 ) ;
353+
354+ const result = rewriteTranscriptEntriesInSessionManager ( {
355+ sessionManager,
356+ replacements : [
357+ {
358+ entryId : entryIds [ 0 ] ,
359+ message : createToolResultReplacement ( "read" , "[externalized file_123]" , 1 ) ,
360+ } ,
361+ ] ,
362+ } ) ;
363+
364+ expect ( result . changed ) . toBe ( true ) ;
365+ const compactions = sessionManager
366+ . getBranch ( )
367+ . filter ( ( entry ) => entry . type === "compaction" ) ;
368+ expect ( compactions ) . toHaveLength ( 2 ) ;
369+ const keptBoundaries = new Set (
370+ compactions . map ( ( entry ) => ( entry . type === "compaction" ? entry . firstKeptEntryId : null ) ) ,
371+ ) ;
372+ expect ( keptBoundaries . size ) . toBe ( 2 ) ;
373+ } ) ;
374+
375+ it ( "preserves legitimate repeated user messages that differ only by timestamp" , ( ) => {
376+ const sessionManager = SessionManager . inMemory ( ) ;
377+ const entryIds = appendSessionMessages ( sessionManager , [
378+ asAppendMessage ( { role : "user" , content : "anchor" , timestamp : 1 } ) ,
379+ asAppendMessage ( {
380+ role : "toolResult" ,
381+ toolCallId : "call_1" ,
382+ toolName : "read" ,
383+ content : createTextContent ( "x" . repeat ( 8_000 ) ) ,
384+ isError : false ,
385+ timestamp : 2 ,
386+ } ) ,
387+ asAppendMessage ( { role : "user" , content : "same question" , timestamp : 3 } ) ,
388+ asAppendMessage ( { role : "user" , content : "same question" , timestamp : 4 } ) ,
389+ asAppendMessage ( { role : "assistant" , content : createTextContent ( "tail" ) , timestamp : 5 } ) ,
390+ ] ) ;
391+
392+ const result = rewriteTranscriptEntriesInSessionManager ( {
393+ sessionManager,
394+ replacements : [
395+ {
396+ entryId : entryIds [ 1 ] ,
397+ message : createToolResultReplacement ( "read" , "[externalized file_123]" , 2 ) ,
398+ } ,
399+ ] ,
400+ } ) ;
401+
402+ expect ( result . changed ) . toBe ( true ) ;
403+ // Two genuine repeats (distinct timestamps) must both survive — only the
404+ // byte-identical recovery clone is collapsed.
405+ const repeated = getBranchMessages ( sessionManager ) . filter (
406+ ( message ) => message . role === "user" && message . content === "same question" ,
407+ ) ;
408+ expect ( repeated ) . toHaveLength ( 2 ) ;
409+ } ) ;
410+
411+ it ( "preserves byte-identical repeated assistant and tool-result entries during replay" , ( ) => {
412+ const sessionManager = SessionManager . inMemory ( ) ;
413+ const entryIds = appendSessionMessages ( sessionManager , [
414+ asAppendMessage ( { role : "user" , content : "anchor" , timestamp : 1 } ) ,
415+ asAppendMessage ( {
416+ role : "toolResult" ,
417+ toolCallId : "call_1" ,
418+ toolName : "read" ,
419+ content : createTextContent ( "x" . repeat ( 8_000 ) ) ,
420+ isError : false ,
421+ timestamp : 2 ,
422+ } ) ,
423+ asAppendMessage ( { role : "assistant" , content : createTextContent ( "repeat" ) , timestamp : 3 } ) ,
424+ asAppendMessage ( { role : "assistant" , content : createTextContent ( "repeat" ) , timestamp : 3 } ) ,
425+ asAppendMessage ( {
426+ role : "toolResult" ,
427+ toolCallId : "call_2" ,
428+ toolName : "exec" ,
429+ content : createTextContent ( "same output" ) ,
430+ isError : false ,
431+ timestamp : 4 ,
432+ } ) ,
433+ asAppendMessage ( {
434+ role : "toolResult" ,
435+ toolCallId : "call_2" ,
436+ toolName : "exec" ,
437+ content : createTextContent ( "same output" ) ,
438+ isError : false ,
439+ timestamp : 4 ,
440+ } ) ,
441+ ] ) ;
442+
443+ const result = rewriteTranscriptEntriesInSessionManager ( {
444+ sessionManager,
445+ replacements : [
446+ {
447+ entryId : entryIds [ 1 ] ,
448+ message : createToolResultReplacement ( "read" , "[externalized file_123]" , 2 ) ,
449+ } ,
450+ ] ,
451+ } ) ;
452+
453+ expect ( result . changed ) . toBe ( true ) ;
454+ const branchMessages = getBranchMessages ( sessionManager ) ;
455+ const assistantRepeats = branchMessages . filter (
456+ ( message ) =>
457+ message . role === "assistant" &&
458+ Array . isArray ( message . content ) &&
459+ message . content . some ( ( part ) => part . type === "text" && part . text === "repeat" ) ,
460+ ) ;
461+ const toolRepeats = branchMessages . filter (
462+ ( message ) =>
463+ message . role === "toolResult" &&
464+ Array . isArray ( message . content ) &&
465+ message . content . some ( ( part ) => part . type === "text" && part . text === "same output" ) ,
466+ ) ;
467+ // Identical assistant/tool payloads can be legitimate history; never deduped.
468+ expect ( assistantRepeats ) . toHaveLength ( 2 ) ;
469+ expect ( toolRepeats ) . toHaveLength ( 2 ) ;
470+ } ) ;
298471} ) ;
299472
300473describe ( "rewriteTranscriptEntriesInSessionFile" , ( ) => {
0 commit comments