5353 "chatter_frequency_ms" : 1000 ,
5454 "noise_tolerance" : 0.2 ,
5555 "amplification_factor" : 1.5 ,
56+ "randomness_factor" : 0.3 ,
57+ "redundancy_handling" : "deduplicate" ,
58+ "gossip_decay" : "exponential" ,
5659 "chatter_nodes" : [],
5760 "topics" : [],
5861}
6972 "chatter_frequency_ms" : 1000 ,
7073 "noise_tolerance" : 0.2 ,
7174 "amplification_factor" : 1.5 ,
75+ "randomness_factor" : 0.3 ,
76+ "redundancy_handling" : "deduplicate" ,
77+ "gossip_decay" : "exponential" ,
7278 "chatter_nodes" : [
7379 {
7480 "id" : "chatter_orkid_tech" ,
188194 },
189195 ],
190196 "channels" : {
191- "critical" : {"latency_ms" : 5 , "reliability" : 0.99 , "capacity" : "unlimited" },
192- "high" : {"latency_ms" : 50 , "reliability" : 0.95 , "capacity" : "high" },
193- "medium" : {"latency_ms" : 200 , "reliability" : 0.90 , "capacity" : "medium" },
194- "low" : {"latency_ms" : 1000 , "reliability" : 0.85 , "capacity" : "low" },
197+ "critical" : {
198+ "latency_ms" : 5 ,
199+ "reliability" : 0.99 ,
200+ "capacity" : "unlimited" ,
201+ "ttl_seconds" : 30 ,
202+ "fanout" : 7 ,
203+ "gossip_probability" : 0.95 ,
204+ "max_hops" : 3 ,
205+ },
206+ "high" : {
207+ "latency_ms" : 50 ,
208+ "reliability" : 0.95 ,
209+ "capacity" : "high" ,
210+ "ttl_seconds" : 60 ,
211+ "fanout" : 5 ,
212+ "gossip_probability" : 0.85 ,
213+ "max_hops" : 3 ,
214+ },
215+ "medium" : {
216+ "latency_ms" : 200 ,
217+ "reliability" : 0.90 ,
218+ "capacity" : "medium" ,
219+ "ttl_seconds" : 120 ,
220+ "fanout" : 4 ,
221+ "gossip_probability" : 0.75 ,
222+ "max_hops" : 3 ,
223+ },
224+ "low" : {
225+ "latency_ms" : 1000 ,
226+ "reliability" : 0.85 ,
227+ "capacity" : "low" ,
228+ "ttl_seconds" : 300 ,
229+ "fanout" : 2 ,
230+ "gossip_probability" : 0.60 ,
231+ "max_hops" : 2 ,
232+ },
195233 },
196234}
197235
@@ -324,9 +362,11 @@ class GossipMessage:
324362 source_room : Optional [str ] = None
325363 priority : str = "normal"
326364 topic : Optional [str ] = None
365+ channel : str = "medium"
327366 hops : int = 0
328367 max_hops : int = 3
329368 ttl_seconds : int = 60
369+ gossip_probability : Optional [float ] = None
330370 _started_at : datetime = field (default_factory = lambda : datetime .now (timezone .utc ))
331371
332372 @property
@@ -346,9 +386,11 @@ def child(self, wing: str, room: Optional[str] = None) -> GossipMessage:
346386 source_room = room ,
347387 priority = self .priority ,
348388 topic = self .topic ,
389+ channel = self .channel ,
349390 hops = self .hops + 1 ,
350391 max_hops = self .max_hops ,
351392 ttl_seconds = self .ttl_seconds ,
393+ gossip_probability = self .gossip_probability ,
352394 _started_at = self ._started_at ,
353395 )
354396
@@ -444,6 +486,62 @@ def _get_hallway_context(
444486
445487 return room_scores , related
446488
489+ def _channel_for_priority (self , priority : str ) -> str :
490+ """Map a message priority to a channel key."""
491+ mapping = {
492+ "critical" : "critical" ,
493+ "high" : "high" ,
494+ "medium" : "medium" ,
495+ "low" : "low" ,
496+ }
497+ return mapping .get (priority , "medium" )
498+
499+ def _channel_params (self , message : GossipMessage ) -> dict [str , Any ]:
500+ """Return channel-overridden propagation parameters."""
501+ channel = self .config .get ("channels" , {}).get (message .channel , {})
502+ defaults = {
503+ "ttl_seconds" : self .config .get ("ttl_seconds" , 60 ),
504+ "fanout" : self .config .get ("fanout" , 5 ),
505+ "gossip_probability" : self .config .get ("gossip_probability" , 0.7 ),
506+ "max_hops" : self .config .get ("max_hops" , 3 ),
507+ "randomness_factor" : self .config .get ("randomness_factor" , 0.3 ),
508+ "noise_tolerance" : self .config .get ("noise_tolerance" , 0.2 ),
509+ }
510+ if not isinstance (channel , dict ):
511+ return defaults
512+ return {** defaults , ** {k : v for k , v in channel .items () if k in defaults }}
513+
514+ def _random_walk_swap (
515+ self , message : GossipMessage , selected : list [ChatterNode ]
516+ ) -> list [ChatterNode ]:
517+ """Occasionally replace selected nodes with random off-radius nodes."""
518+ params = self ._channel_params (message )
519+ randomness = float (params .get ("randomness_factor" , 0.3 ))
520+ max_steps = 5
521+
522+ if not message .source_wing :
523+ return selected
524+
525+ source = normalize_wing_name (message .source_wing )
526+ off_radius = [
527+ n
528+ for n in self ._chatter_nodes
529+ if n not in selected
530+ and source not in {normalize_wing_name (w ) for w in n .gossip_radius }
531+ ]
532+ if not off_radius :
533+ return selected
534+
535+ swapped = list (selected )
536+ swaps = 0
537+ for i in range (len (swapped )):
538+ if random .random () < randomness and swaps < max_steps :
539+ replacement = random .choice (off_radius )
540+ swapped [i ] = replacement
541+ off_radius .remove (replacement )
542+ swaps += 1
543+ return swapped
544+
447545 def select_chatter_nodes (
448546 self ,
449547 message : GossipMessage ,
@@ -452,11 +550,18 @@ def select_chatter_nodes(
452550 """Rank and select chatter nodes for a given message.
453551
454552 Selection combines the base specialty/topic score with within-wing
455- hallway context: chatter nodes in the source wing whose ``rooms``
456- overlap with the co-occurrence rooms get a boost, as do nodes whose
457- specialties overlap with related entities.
553+ hallway context and the active channel's fanout, noise tolerance, and
554+ random-walk swap parameters. Nodes whose ``rooms`` overlap with the
555+ co-occurrence rooms or whose specialties match related entities get a
556+ boost before the noise tolerance filter is applied.
458557 """
459- fanout = fanout if fanout is not None else self .config .get ("fanout" , 5 )
558+ params = self ._channel_params (message )
559+ fanout = (
560+ fanout
561+ if fanout is not None
562+ else params .get ("fanout" , self .config .get ("fanout" , 5 ))
563+ )
564+ noise_tolerance = float (params .get ("noise_tolerance" , 0.2 ))
460565 room_scores , related_entities = self ._get_hallway_context (message )
461566
462567 scored = []
@@ -467,7 +572,7 @@ def select_chatter_nodes(
467572 if message .source_wing and normalize_wing_name (
468573 message .source_wing
469574 ) == normalize_wing_name (node .wing ):
470- # Boost by room-level affinity. A node's hall and the
575+ # Boost by room-level affinity. A node's hall and the
471576 # co-occurrence rooms are in different namespaces, so we match
472577 # rooms to the node's ``rooms`` list rather than to ``hall``.
473578 if room_scores and node .rooms :
@@ -485,11 +590,15 @@ def select_chatter_nodes(
485590 score += min (0.3 , len (overlaps ) * 0.1 )
486591
487592 score = max (0.0 , min (2.0 , score ))
488- if score > 0 :
593+ if score >= noise_tolerance :
489594 scored .append ((node , score ))
490-
491595 scored .sort (key = lambda x : x [1 ], reverse = True )
492- return [n for n , _ in scored [:fanout ]]
596+ selected = [n for n , _ in scored [:fanout ]]
597+
598+ # Random walk: occasionally swap a selected node for a random one.
599+ selected = self ._random_walk_swap (message , selected )
600+
601+ return selected
493602
494603 def _resolve_targets (
495604 self ,
@@ -541,14 +650,26 @@ def _resolve_targets(
541650 return unique
542651
543652 def _should_forward (self , message : GossipMessage , node : ChatterNode ) -> bool :
544- """Apply gossip probability, chatter level, and TTL checks."""
653+ """Apply gossip probability, chatter level, amplification, and TTL checks."""
545654 if message .is_expired ():
546655 return False
547656 if message .hops >= getattr (message , "max_hops" , self .config .get ("max_hops" , 3 )):
548657 return False
549- base = self .config .get ("gossip_probability" , 0.7 )
658+
659+ params = self ._channel_params (message )
660+ base = (
661+ message .gossip_probability
662+ if message .gossip_probability is not None
663+ else float (params .get ("gossip_probability" , self .config .get ("gossip_probability" , 0.7 )))
664+ )
550665 level_mult = CHATTER_LEVEL_MULTIPLIER .get (node .chatter_level , 1.0 )
551666 probability = min (1.0 , base * level_mult )
667+
668+ # Amplification factor boosts high-priority information.
669+ if message .priority in {"critical" , "high" }:
670+ amp = float (self .config .get ("amplification_factor" , 1.5 ))
671+ probability = min (1.0 , probability * amp )
672+
552673 return random .random () < probability
553674
554675 def _compute_valid_to (self , message : GossipMessage ) -> Optional [str ]:
@@ -676,7 +797,8 @@ def propagate(
676797 detected_topic , detected_priority = self .detect_topic (text , priority )
677798 priority = priority or detected_priority
678799
679- message = GossipMessage (
800+ # Build a preliminary message to resolve the channel and its parameters.
801+ preliminary = GossipMessage (
680802 subject = subject ,
681803 predicate = predicate ,
682804 obj = obj ,
@@ -687,6 +809,29 @@ def propagate(
687809 ttl_seconds = self .config .get ("ttl_seconds" , 60 ),
688810 max_hops = max_hops if max_hops is not None else self .config .get ("max_hops" , 3 ),
689811 )
812+ channel = self ._channel_for_priority (priority )
813+ params = self .config .get ("channels" , {}).get (channel , {})
814+
815+ # Preserve explicit caller arguments over channel defaults.
816+ final_max_hops = (
817+ max_hops
818+ if max_hops is not None
819+ else params .get ("max_hops" , preliminary .max_hops )
820+ )
821+
822+ message = GossipMessage (
823+ subject = subject ,
824+ predicate = predicate ,
825+ obj = obj ,
826+ source_wing = source_wing ,
827+ source_room = source_room ,
828+ priority = priority ,
829+ topic = detected_topic ,
830+ channel = channel ,
831+ ttl_seconds = params .get ("ttl_seconds" , preliminary .ttl_seconds ),
832+ max_hops = final_max_hops ,
833+ gossip_probability = params .get ("gossip_probability" ),
834+ )
690835
691836 report , _ = self ._propagate_message (
692837 message , fanout = fanout , room_graph = room_graph , kg_path = kg_path
@@ -700,8 +845,13 @@ def chatter_status(self) -> dict[str, Any]:
700845 "max_hops" : self .config .get ("max_hops" , 3 ),
701846 "ttl_seconds" : self .config .get ("ttl_seconds" , 60 ),
702847 "fanout" : self .config .get ("fanout" , 5 ),
848+ "gossip_probability" : self .config .get ("gossip_probability" , 0.7 ),
849+ "randomness_factor" : self .config .get ("randomness_factor" , 0.3 ),
850+ "noise_tolerance" : self .config .get ("noise_tolerance" , 0.2 ),
851+ "amplification_factor" : self .config .get ("amplification_factor" , 1.5 ),
703852 "chatter_nodes" : [asdict (n ) for n in self ._chatter_nodes ],
704853 "topics" : self .config .get ("topics" , []),
854+ "channels" : self .config .get ("channels" , {}),
705855 }
706856
707857 def analytics (self , as_of : str = None ) -> dict [str , Any ]:
0 commit comments