@@ -382,7 +382,12 @@ function identityKey(entry) {
382382// its own injections with it. No allowlist of reminder texts: the flip
383383// evidence already covers four reminder kinds, and a pattern list would
384384// be the next mole (directive, part A).
385- const VOLATILE_WRAP_REGEX = / ^ < s y s t e m - r e m i n d e r > \n [ \s \S ] * \n < \/ s y s t e m - r e m i n d e r > \s * $ / ;
385+ //
386+ // Captures the inner text (group 1) so the SAME regex serves both
387+ // isVolatileBlock's boolean test (unaffected by adding a group) and
388+ // suppression's unwrapVolatileText below — one pattern, not a second
389+ // derivation of it (dev-loop.md, "never hand-roll identity in a probe").
390+ const VOLATILE_WRAP_REGEX = / ^ < s y s t e m - r e m i n d e r > \n ( [ \s \S ] * ) \n < \/ s y s t e m - r e m i n d e r > \s * $ / ;
386391
387392// A text block is volatile iff it is entirely a system-reminder wrap OR
388393// empty — the observed flip alternates a reminder block with an
@@ -604,6 +609,70 @@ function pinnedForwardForm(stored, incomingMsg) {
604609 return stored . m ?? stripVolatileBlocks ( incomingMsg ) ;
605610}
606611
612+ // --- Reminder-swap suppression (#76606, decision B) ---
613+ //
614+ // CC sometimes migrates a hook reminder OUT of the user message that
615+ // carries it and INTO a standalone message of its own — measured directly
616+ // (capture s-633915a8, n=26->28): message[30]'s <system-reminder>-wrapped
617+ // block is gone from message[30] and its inner text, wrapper stripped,
618+ // is the entire content of a new message[31] (role system). Pinning above
619+ // restores message[30]'s first-seen bytes, reminder included; treating the
620+ // new standalone as ordinary tail growth then forwards the SAME text a
621+ // second time, and because it lands mid-array the cache's
622+ // longest-identical-prefix boundary moves to right before it — everything
623+ // after is re-billed (measured: cacheRead 15424 / cacheCreation 124025).
624+ //
625+ // Strip the wrapper for comparison ONLY — never for what gets forwarded;
626+ // the pin already owns that. Reuses VOLATILE_WRAP_REGEX's capture group
627+ // rather than a second regex, per the same rule cited above it.
628+ function unwrapVolatileText ( block ) {
629+ if ( ! block || typeof block !== "object" || block . type !== "text" || typeof block . text !== "string" ) {
630+ return block ;
631+ }
632+ const m = VOLATILE_WRAP_REGEX . exec ( block . text ) ;
633+ return m ? { type : "text" , text : m [ 1 ] } : block ;
634+ }
635+
636+ // The set of block identities this extension is CURRENTLY restoring —
637+ // i.e. present in a LIVE (not dropped) canonical entry's stored first-seen
638+ // form. Dropped entries are excluded on purpose: their content is not being
639+ // served anywhere, so a new standalone message matching a dropped block
640+ // must flow through normally rather than being silently discarded with no
641+ // copy left at all. Scoped to VOLATILE blocks only (isVolatileBlock), since
642+ // those are what buildPinEntry ever stores `m` for and what the measured
643+ // migration shape moves — a plain text block coincidentally matching a
644+ // pinned message's ordinary content is not this class.
645+ function pinnedBlockHashes ( priorCanonical ) {
646+ const hashes = new Set ( ) ;
647+ if ( ! Array . isArray ( priorCanonical ) ) return hashes ;
648+ for ( const entry of priorCanonical ) {
649+ if ( entry . d || ! entry . m || ! Array . isArray ( entry . m . content ) ) continue ;
650+ for ( const block of entry . m . content ) {
651+ if ( ! isVolatileBlock ( block ) ) continue ;
652+ const h = hashMessageContent ( { content : [ unwrapVolatileText ( block ) ] } ) ;
653+ if ( h !== null ) hashes . add ( h ) ;
654+ }
655+ }
656+ return hashes ;
657+ }
658+
659+ // Is `msg` a suppressible duplicate of a currently-pinned block? Narrow by
660+ // definition (BACKLOG #76606 part (c)): STANDALONE only (single block after
661+ // the same string->one-block fold canonicalMessageShape already applies
662+ // elsewhere in this file), and its wrapper-stripped bytes must exactly
663+ // equal a hash in `pinnedHashes` — never a positional or role heuristic.
664+ // Returns the matched hash (for telemetry) or null (genuine content: no
665+ // suppression, existing rules apply unchanged).
666+ export function findSuppressibleDuplicate ( msg , pinnedHashes ) {
667+ const shaped = canonicalMessageShape ( msg ) ;
668+ if ( ! Array . isArray ( shaped . content ) || shaped . content . length !== 1 ) return null ;
669+ const h = hashMessageContent ( { content : [ unwrapVolatileText ( shaped . content [ 0 ] ) ] } ) ;
670+ if ( h === null || ! pinnedHashes . has ( h ) ) return null ;
671+ return h ;
672+ }
673+
674+ export { pinnedBlockHashes } ;
675+
607676// Pin-mode classification. Differences from classifyInsertion:
608677// - identities exclude volatile blocks (flip absorption);
609678// - canonical entries missing from incoming are marked dropped
@@ -743,22 +812,47 @@ export function classifyPinned(messages, priorCanonical) {
743812 return resetKeepingPins ( "assistant-interleaved" ) ;
744813 }
745814
815+ // Suppress a NEW entry that duplicates a block this extension is already
816+ // restoring elsewhere (see the block comment above findSuppressibleDuplicate).
817+ // Assistant entries are excluded on principle even though the measured
818+ // shape never produces one — silently dropping the model's own prior
819+ // output is a correctness question this extension has no business
820+ // deciding, unlike a hook reminder it already owns via the pin.
821+ // Genuine change (normalized bytes differ from every pinned block):
822+ // findSuppressibleDuplicate returns null, the entry is untouched here,
823+ // and whatever the existing rules above already decided (append/splice/
824+ // edit-shaped reset) stands — no new reset path is introduced.
825+ const pinnedHashes = pinnedBlockHashes ( priorCanonical ) ;
826+ const suppressions = [ ] ;
827+ for ( const e of newEntries ) {
828+ if ( e . r === "assistant" ) continue ;
829+ const h = findSuppressibleDuplicate ( messages [ e . index ] , pinnedHashes ) ;
830+ if ( h !== null ) suppressions . push ( { index : e . index , hash : h } ) ;
831+ }
832+ const suppressedIdx = new Set ( suppressions . map ( ( s ) => s . index ) ) ;
833+
746834 // Forwarded order is the INCOMING order, not "survivors then new". The two
747835 // agree for a plain append; they diverge when CC splices an entry
748836 // mid-history, and concatenating new entries at the end would then reorder
749837 // real content — the very thing this extension exists to prevent.
750838 let pinApplied = 0 ;
751839 const matchedByIdx = new Map ( matched . map ( ( { ci, idx } ) => [ idx , ci ] ) ) ;
752- const finalMessages = incoming . map ( ( e ) => {
840+ const finalMessages = [ ] ;
841+ for ( const e of incoming ) {
842+ if ( suppressedIdx . has ( e . index ) ) continue ; // the pinned inline form already carries these bytes
753843 const ci = matchedByIdx . get ( e . index ) ;
754- if ( ci === undefined ) return messages [ e . index ] ;
844+ if ( ci === undefined ) {
845+ finalMessages . push ( messages [ e . index ] ) ;
846+ continue ;
847+ }
755848 const fwd = pinnedForwardForm ( priorCanonical [ ci ] , messages [ e . index ] ) ;
756849 if ( fwd !== messages [ e . index ] && JSON . stringify ( fwd ) !== JSON . stringify ( messages [ e . index ] ) ) {
757850 pinApplied ++ ;
758- return fwd ;
851+ finalMessages . push ( fwd ) ;
852+ } else {
853+ finalMessages . push ( messages [ e . index ] ) ;
759854 }
760- return messages [ e . index ] ;
761- } ) ;
855+ }
762856
763857 if ( ! validateToolAdjacency ( finalMessages ) ) {
764858 return { action : "reset" , resetReason : "adjacency-violation" , canonicalEntries : freshEntries ( ) } ;
@@ -801,11 +895,21 @@ export function classifyPinned(messages, priorCanonical) {
801895 const canonicalEntries = [ ] ;
802896 for ( const trailing of droppedAfter . get ( - 1 ) ?? [ ] ) canonicalEntries . push ( trailing ) ;
803897 for ( const e of incoming ) {
898+ // A suppressed entry was never forwarded, so it gets no canonical
899+ // identity — the invariant just below states the canonical must
900+ // describe the wire we just forwarded, and this entry isn't on it.
901+ // Recomputed fresh on every request (findSuppressibleDuplicate against
902+ // the currently-live pins), so leaving no trace here is not a gap: CC
903+ // keeps re-sending the duplicate as long as it believes it's part of
904+ // history, and it is re-detected and re-suppressed every time — no
905+ // persisted "suppressed" marker is needed for the suppression to stay
906+ // stable across subsequent requests.
907+ if ( suppressedIdx . has ( e . index ) ) continue ;
804908 canonicalEntries . push ( canonByIdx . get ( e . index ) ?? newByIdx . get ( e . index ) ) ;
805909 for ( const trailing of droppedAfter . get ( e . index ) ?? [ ] ) canonicalEntries . push ( trailing ) ;
806910 }
807911
808- const changed = splicedEntries . length > 0 || pinApplied > 0 ;
912+ const changed = splicedEntries . length > 0 || pinApplied > 0 || suppressions . length > 0 ;
809913 return {
810914 action : changed ? "normalized" : "append-only" ,
811915 messages : finalMessages ,
@@ -840,9 +944,14 @@ export function classifyPinned(messages, priorCanonical) {
840944 }
841945 return null ;
842946 } ) ( ) ,
843- inserted : newEntries . length ,
947+ // `inserted` counts what actually landed on the wire — a suppressed
948+ // entry was a new entry CC sent but never one we forwarded, so it must
949+ // not inflate this the way it would inflate a real insertion count.
950+ inserted : newEntries . length - suppressions . length ,
844951 pinned : pinApplied ,
845952 dropped : droppedNow . size ,
953+ suppressed : suppressions . length ,
954+ suppressions,
846955 } ;
847956}
848957
@@ -904,7 +1013,18 @@ export default {
9041013 canonLive : result . canonicalEntries ?. filter ( ( e ) => ! e . d ) . length ?? 0 ,
9051014 msgs : messages . length ,
9061015 canonOrderViolation : result . canonOrderViolation ?? null ,
907- ...( pin ? { pinned : result . pinned ?? 0 , dropped : result . dropped ?? 0 } : { } ) ,
1016+ // `suppressions` (not just the count) rides on the stats object —
1017+ // tools/replay.mjs's safety-gate exemption reads the incoming
1018+ // indices from here to declare them, the same way it already reads
1019+ // deferred-tool-rewrite's tool_addition shape.
1020+ ...( pin
1021+ ? {
1022+ pinned : result . pinned ?? 0 ,
1023+ dropped : result . dropped ?? 0 ,
1024+ suppressed : result . suppressed ?? 0 ,
1025+ suppressions : result . suppressions ?? [ ] ,
1026+ }
1027+ : { } ) ,
9081028 } ;
9091029
9101030 await appendTelemetry (
@@ -917,16 +1037,38 @@ export default {
9171037 action : result . action ,
9181038 inserted : result . inserted ?? 0 ,
9191039 ...( result . resetReason ? { resetReason : result . resetReason } : { } ) ,
920- ...( pin ? { pinned : result . pinned ?? 0 , dropped : result . dropped ?? 0 } : { } ) ,
1040+ ...( pin ? { pinned : result . pinned ?? 0 , dropped : result . dropped ?? 0 , suppressed : result . suppressed ?? 0 } : { } ) ,
9211041 } ,
9221042 fs ,
9231043 ) ;
9241044
1045+ // One event line PER SUPPRESSION (not aggregated into the summary
1046+ // line above), to the same file/format — the pattern every other
1047+ // record in this log already uses, just one call per occurrence
1048+ // instead of once per request.
1049+ if ( pin && Array . isArray ( result . suppressions ) && result . suppressions . length ) {
1050+ for ( const s of result . suppressions ) {
1051+ await appendTelemetry (
1052+ dir ,
1053+ sessionKey ,
1054+ {
1055+ ts : new Date ( ) . toISOString ( ) ,
1056+ key : sessionKey ,
1057+ sid : sessionId ,
1058+ event : "suppressed-duplicate" ,
1059+ index : s . index ,
1060+ hash : s . hash ,
1061+ } ,
1062+ fs ,
1063+ ) ;
1064+ }
1065+ }
1066+
9251067 if ( isDebug ( ) ) {
9261068 process . stderr . write (
9271069 `[insertion-normalize] action=${ result . action } inserted=${ result . inserted ?? 0 } ` +
9281070 ( result . resetReason ? ` reason=${ result . resetReason } ` : "" ) +
929- ( pin ? ` pinned=${ result . pinned ?? 0 } dropped=${ result . dropped ?? 0 } ` : "" ) +
1071+ ( pin ? ` pinned=${ result . pinned ?? 0 } dropped=${ result . dropped ?? 0 } suppressed= ${ result . suppressed ?? 0 } ` : "" ) +
9301072 "\n" ,
9311073 ) ;
9321074 }
0 commit comments