5656 "randomness_factor" : 0.3 ,
5757 "redundancy_handling" : "deduplicate" ,
5858 "gossip_decay" : "exponential" ,
59+ "echo_chamber_reinforcement_count" : 3 ,
60+ "echo_chamber_attenuation" : 0.5 ,
61+ "echo_chamber_similarity_threshold" : 0.8 ,
5962 "chatter_nodes" : [],
6063 "topics" : [],
6164}
7578 "randomness_factor" : 0.3 ,
7679 "redundancy_handling" : "deduplicate" ,
7780 "gossip_decay" : "exponential" ,
81+ "echo_chamber_reinforcement_count" : 3 ,
82+ "echo_chamber_attenuation" : 0.5 ,
83+ "echo_chamber_similarity_threshold" : 0.8 ,
7884 "chatter_nodes" : [
7985 {
8086 "id" : "chatter_orkid_tech" ,
@@ -367,6 +373,7 @@ class GossipMessage:
367373 max_hops : int = 3
368374 ttl_seconds : int = 60
369375 gossip_probability : Optional [float ] = None
376+ path : list [str ] = field (default_factory = list )
370377 _started_at : datetime = field (default_factory = lambda : datetime .now (timezone .utc ))
371378
372379 @property
@@ -376,8 +383,11 @@ def text(self) -> str:
376383 def is_expired (self ) -> bool :
377384 return (datetime .now (timezone .utc ) - self ._started_at ).total_seconds () > self .ttl_seconds
378385
379- def child (self , wing : str , room : Optional [str ] = None ) -> GossipMessage :
380- """Return a copy with incremented hop count."""
386+ def child (self , wing : str , room : Optional [str ] = None , node_id : Optional [str ] = None ) -> GossipMessage :
387+ """Return a copy with incremented hop count and updated path."""
388+ new_path = list (self .path )
389+ if node_id :
390+ new_path .append (node_id )
381391 return GossipMessage (
382392 subject = self .subject ,
383393 predicate = self .predicate ,
@@ -391,6 +401,7 @@ def child(self, wing: str, room: Optional[str] = None) -> GossipMessage:
391401 max_hops = self .max_hops ,
392402 ttl_seconds = self .ttl_seconds ,
393403 gossip_probability = self .gossip_probability ,
404+ path = new_path ,
394405 _started_at = self ._started_at ,
395406 )
396407
@@ -649,8 +660,51 @@ def _resolve_targets(
649660 unique .append (t )
650661 return unique
651662
663+ def _attenuate_echo_chamber (
664+ self , message : GossipMessage , node : ChatterNode , probability : float
665+ ) -> float :
666+ """Dampen probability when the node has already seen similar messages.
667+
668+ Each path entry is either a node id or ``node_id:message_text``. The
669+ similarity threshold determines whether a prior visit counts as an echo.
670+ """
671+ threshold = float (
672+ self .config .get ("echo_chamber_similarity_threshold" , 0.8 )
673+ )
674+ reinforcement_count = int (
675+ self .config .get ("echo_chamber_reinforcement_count" , 3 )
676+ )
677+ attenuation = float (self .config .get ("echo_chamber_attenuation" , 0.5 ))
678+
679+ current_text = message .text
680+ current_tokens = set (current_text .lower ().split ())
681+ echo_visits = 0
682+
683+ for entry in message .path or []:
684+ if not entry :
685+ continue
686+ if ":" in entry :
687+ stored_id , stored_text = entry .split (":" , 1 )
688+ else :
689+ stored_id = entry
690+ stored_text = current_text
691+ if stored_id != node .id :
692+ continue
693+ stored_tokens = set (stored_text .lower ().split ())
694+ union = current_tokens | stored_tokens
695+ if not union :
696+ continue
697+ if len (current_tokens & stored_tokens ) / len (union ) >= threshold :
698+ echo_visits += 1
699+
700+ if echo_visits >= reinforcement_count :
701+ return 0.0
702+ for _ in range (echo_visits ):
703+ probability *= attenuation
704+ return probability
705+
652706 def _should_forward (self , message : GossipMessage , node : ChatterNode ) -> bool :
653- """Apply gossip probability, chatter level, amplification, and TTL checks."""
707+ """Apply gossip probability, chatter level, amplification, TTL, and echo checks."""
654708 if message .is_expired ():
655709 return False
656710 if message .hops >= getattr (message , "max_hops" , self .config .get ("max_hops" , 3 )):
@@ -670,6 +724,7 @@ def _should_forward(self, message: GossipMessage, node: ChatterNode) -> bool:
670724 amp = float (self .config .get ("amplification_factor" , 1.5 ))
671725 probability = min (1.0 , probability * amp )
672726
727+ probability = self ._attenuate_echo_chamber (message , node , probability )
673728 return random .random () < probability
674729
675730 def _compute_valid_to (self , message : GossipMessage ) -> Optional [str ]:
@@ -756,7 +811,7 @@ def _propagate_message(
756811 {"wing" : target_wing , "room" : target_room }
757812 )
758813 any_success = True
759- children .append (message .child (target_wing , target_room ))
814+ children .append (message .child (target_wing , target_room , node_id = node . id ))
760815 except Exception as exc :
761816 logger .debug (
762817 "gossip: kg add failed for %s/%s" , target_wing , target_room , exc_info = True
@@ -849,6 +904,15 @@ def chatter_status(self) -> dict[str, Any]:
849904 "randomness_factor" : self .config .get ("randomness_factor" , 0.3 ),
850905 "noise_tolerance" : self .config .get ("noise_tolerance" , 0.2 ),
851906 "amplification_factor" : self .config .get ("amplification_factor" , 1.5 ),
907+ "echo_chamber_reinforcement_count" : self .config .get (
908+ "echo_chamber_reinforcement_count" , 3
909+ ),
910+ "echo_chamber_attenuation" : self .config .get (
911+ "echo_chamber_attenuation" , 0.5
912+ ),
913+ "echo_chamber_similarity_threshold" : self .config .get (
914+ "echo_chamber_similarity_threshold" , 0.8
915+ ),
852916 "chatter_nodes" : [asdict (n ) for n in self ._chatter_nodes ],
853917 "topics" : self .config .get ("topics" , []),
854918 "channels" : self .config .get ("channels" , {}),
0 commit comments