4747 "chatter_frequency_ms" : 1000 ,
4848 "noise_tolerance" : 0.2 ,
4949 "amplification_factor" : 1.5 ,
50+ "randomness_factor" : 0.3 ,
51+ "redundancy_handling" : "deduplicate" ,
52+ "gossip_decay" : "exponential" ,
5053 "chatter_nodes" : [
5154 {
5255 "id" : "chatter_orkid_tech" ,
159162 },
160163 ],
161164 "channels" : {
162- "critical" : {"latency_ms" : 5 , "reliability" : 0.99 , "capacity" : "unlimited" },
163- "high" : {"latency_ms" : 50 , "reliability" : 0.95 , "capacity" : "high" },
164- "medium" : {"latency_ms" : 200 , "reliability" : 0.90 , "capacity" : "medium" },
165- "low" : {"latency_ms" : 1000 , "reliability" : 0.85 , "capacity" : "low" },
165+ "critical" : {
166+ "latency_ms" : 5 ,
167+ "reliability" : 0.99 ,
168+ "capacity" : "unlimited" ,
169+ "ttl_seconds" : 30 ,
170+ "fanout" : 7 ,
171+ "gossip_probability" : 0.95 ,
172+ "max_hops" : 3 ,
173+ },
174+ "high" : {
175+ "latency_ms" : 50 ,
176+ "reliability" : 0.95 ,
177+ "capacity" : "high" ,
178+ "ttl_seconds" : 60 ,
179+ "fanout" : 5 ,
180+ "gossip_probability" : 0.85 ,
181+ "max_hops" : 3 ,
182+ },
183+ "medium" : {
184+ "latency_ms" : 200 ,
185+ "reliability" : 0.90 ,
186+ "capacity" : "medium" ,
187+ "ttl_seconds" : 120 ,
188+ "fanout" : 4 ,
189+ "gossip_probability" : 0.75 ,
190+ "max_hops" : 3 ,
191+ },
192+ "low" : {
193+ "latency_ms" : 1000 ,
194+ "reliability" : 0.85 ,
195+ "capacity" : "low" ,
196+ "ttl_seconds" : 300 ,
197+ "fanout" : 2 ,
198+ "gossip_probability" : 0.60 ,
199+ "max_hops" : 2 ,
200+ },
166201 },
167202}
168203
@@ -294,9 +329,11 @@ class GossipMessage:
294329 source_room : Optional [str ] = None
295330 priority : str = "normal"
296331 topic : Optional [str ] = None
332+ channel : str = "medium"
297333 hops : int = 0
298334 max_hops : int = 3
299335 ttl_seconds : int = 60
336+ gossip_probability : Optional [float ] = None
300337 _started_at : datetime = field (default_factory = lambda : datetime .now (timezone .utc ))
301338
302339 @property
@@ -316,9 +353,11 @@ def child(self, wing: str, room: Optional[str] = None) -> GossipMessage:
316353 source_room = room ,
317354 priority = self .priority ,
318355 topic = self .topic ,
356+ channel = self .channel ,
319357 hops = self .hops + 1 ,
320358 max_hops = self .max_hops ,
321359 ttl_seconds = self .ttl_seconds ,
360+ gossip_probability = self .gossip_probability ,
322361 _started_at = self ._started_at ,
323362 )
324363
@@ -372,20 +411,85 @@ def detect_topic(self, text: str, priority: Optional[str] = None) -> tuple[str,
372411 return "general" , priority
373412 return "general" , "normal"
374413
414+ def _channel_for_priority (self , priority : str ) -> str :
415+ """Map a message priority to a channel key."""
416+ mapping = {
417+ "critical" : "critical" ,
418+ "high" : "high" ,
419+ "medium" : "medium" ,
420+ "low" : "low" ,
421+ }
422+ return mapping .get (priority , "medium" )
423+
424+ def _channel_params (self , message : GossipMessage ) -> dict [str , Any ]:
425+ """Return channel-overridden propagation parameters."""
426+ channel = self .config .get ("channels" , {}).get (message .channel , {})
427+ defaults = {
428+ "ttl_seconds" : self .config .get ("ttl_seconds" , 60 ),
429+ "fanout" : self .config .get ("fanout" , 5 ),
430+ "gossip_probability" : self .config .get ("gossip_probability" , 0.7 ),
431+ "max_hops" : self .config .get ("max_hops" , 3 ),
432+ "randomness_factor" : self .config .get ("randomness_factor" , 0.3 ),
433+ "noise_tolerance" : self .config .get ("noise_tolerance" , 0.2 ),
434+ }
435+ if not isinstance (channel , dict ):
436+ return defaults
437+ return {** defaults , ** {k : v for k , v in channel .items () if k in defaults }}
438+
439+ def _random_walk_swap (
440+ self , message : GossipMessage , selected : list [ChatterNode ]
441+ ) -> list [ChatterNode ]:
442+ """Occasionally replace selected nodes with random off-radius nodes."""
443+ params = self ._channel_params (message )
444+ randomness = float (params .get ("randomness_factor" , 0.3 ))
445+ max_steps = 5
446+
447+ available = [n for n in self ._chatter_nodes if n not in selected ]
448+ if not available :
449+ return selected
450+
451+ swapped = list (selected )
452+ swaps = 0
453+ for i in range (len (swapped )):
454+ if random .random () < randomness and swaps < max_steps :
455+ replacement = random .choice (available )
456+ swapped [i ] = replacement
457+ available .remove (replacement )
458+ swaps += 1
459+ return swapped
460+
375461 def select_chatter_nodes (
376462 self ,
377463 message : GossipMessage ,
378464 fanout : Optional [int ] = None ,
379465 ) -> list [ChatterNode ]:
380- """Rank and select chatter nodes for a given message."""
381- fanout = fanout if fanout is not None else self .config .get ("fanout" , 5 )
466+ """Rank and select chatter nodes for a given message.
467+
468+ Selection applies the channel's fanout and noise tolerance, then
469+ performs a random-walk swap so gossip can occasionally escape the
470+ normal routing radius while still respecting the fanout budget.
471+ """
472+ params = self ._channel_params (message )
473+ fanout = (
474+ fanout
475+ if fanout is not None
476+ else params .get ("fanout" , self .config .get ("fanout" , 5 ))
477+ )
478+ noise_tolerance = float (params .get ("noise_tolerance" , 0.2 ))
479+
382480 scored = [
383481 (node , node .match_score (message .text , message .source_wing ))
384482 for node in self ._chatter_nodes
385483 ]
386- scored = [(n , s ) for n , s in scored if s > 0 ]
484+ # Noise tolerance: drop nodes that are not relevant enough.
485+ scored = [(n , s ) for n , s in scored if s >= noise_tolerance ]
387486 scored .sort (key = lambda x : x [1 ], reverse = True )
388- return [n for n , _ in scored [:fanout ]]
487+ selected = [n for n , _ in scored [:fanout ]]
488+
489+ # Random walk: occasionally swap a selected node for a random one.
490+ selected = self ._random_walk_swap (message , selected )
491+
492+ return selected
389493
390494 def _resolve_targets (
391495 self ,
@@ -437,14 +541,26 @@ def _resolve_targets(
437541 return unique
438542
439543 def _should_forward (self , message : GossipMessage , node : ChatterNode ) -> bool :
440- """Apply gossip probability, chatter level, and TTL checks."""
544+ """Apply gossip probability, chatter level, amplification, and TTL checks."""
441545 if message .is_expired ():
442546 return False
443547 if message .hops >= getattr (message , "max_hops" , self .config .get ("max_hops" , 3 )):
444548 return False
445- base = self .config .get ("gossip_probability" , 0.7 )
549+
550+ params = self ._channel_params (message )
551+ base = (
552+ message .gossip_probability
553+ if message .gossip_probability is not None
554+ else float (params .get ("gossip_probability" , self .config .get ("gossip_probability" , 0.7 )))
555+ )
446556 level_mult = CHATTER_LEVEL_MULTIPLIER .get (node .chatter_level , 1.0 )
447557 probability = min (1.0 , base * level_mult )
558+
559+ # Amplification factor boosts high-priority information.
560+ if message .priority in {"critical" , "high" }:
561+ amp = float (self .config .get ("amplification_factor" , 1.5 ))
562+ probability = min (1.0 , probability * amp )
563+
448564 return random .random () < probability
449565
450566 def _compute_valid_to (self , message : GossipMessage ) -> Optional [str ]:
@@ -476,7 +592,8 @@ def propagate(
476592 detected_topic , detected_priority = self .detect_topic (text , priority )
477593 priority = priority or detected_priority
478594
479- message = GossipMessage (
595+ # Build a preliminary message to resolve the channel and its parameters.
596+ preliminary = GossipMessage (
480597 subject = subject ,
481598 predicate = predicate ,
482599 obj = obj ,
@@ -487,6 +604,22 @@ def propagate(
487604 ttl_seconds = self .config .get ("ttl_seconds" , 60 ),
488605 max_hops = max_hops if max_hops is not None else self .config .get ("max_hops" , 3 ),
489606 )
607+ channel = self ._channel_for_priority (priority )
608+ params = self .config .get ("channels" , {}).get (channel , {})
609+
610+ message = GossipMessage (
611+ subject = subject ,
612+ predicate = predicate ,
613+ obj = obj ,
614+ source_wing = source_wing ,
615+ source_room = source_room ,
616+ priority = priority ,
617+ topic = detected_topic ,
618+ channel = channel ,
619+ ttl_seconds = params .get ("ttl_seconds" , preliminary .ttl_seconds ),
620+ max_hops = params .get ("max_hops" , preliminary .max_hops ),
621+ gossip_probability = params .get ("gossip_probability" ),
622+ )
490623
491624 selected = self .select_chatter_nodes (message , fanout = fanout )
492625 report : dict [str , Any ] = {
@@ -556,8 +689,13 @@ def chatter_status(self) -> dict[str, Any]:
556689 "max_hops" : self .config .get ("max_hops" , 3 ),
557690 "ttl_seconds" : self .config .get ("ttl_seconds" , 60 ),
558691 "fanout" : self .config .get ("fanout" , 5 ),
692+ "gossip_probability" : self .config .get ("gossip_probability" , 0.7 ),
693+ "randomness_factor" : self .config .get ("randomness_factor" , 0.3 ),
694+ "noise_tolerance" : self .config .get ("noise_tolerance" , 0.2 ),
695+ "amplification_factor" : self .config .get ("amplification_factor" , 1.5 ),
559696 "chatter_nodes" : [asdict (n ) for n in self ._chatter_nodes ],
560697 "topics" : self .config .get ("topics" , []),
698+ "channels" : self .config .get ("channels" , {}),
561699 }
562700
563701
0 commit comments