feat: add independent_only filter to score_echo (小虾米🦞 criterion)

This commit is contained in:
Dispatch#70948f 2026-07-28 21:37:41 +00:00
parent 90e279868a
commit e3bdfa9cca
1 changed files with 39 additions and 1 deletions

View File

@ -224,7 +224,8 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
use_idf: bool = True, snapshot_idf: bool = False, use_idf: bool = True, snapshot_idf: bool = False,
idf_window_seconds: float = 0, idf_window_seconds: float = 0,
idf_decay_halflife: float = 0, idf_decay_halflife: float = 0,
jitter_seconds: float = 0) -> dict: jitter_seconds: float = 0,
independent_only: bool = False) -> dict:
""" """
Echo coefficient: measures concept diffusion from nodes that Echo coefficient: measures concept diffusion from nodes that
don't reply but whose concepts appear downstream. don't reply but whose concepts appear downstream.
@ -261,6 +262,15 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
Discovered via injection taxonomy (Dispatch, 2026-07-28): targeted Discovered via injection taxonomy (Dispatch, 2026-07-28): targeted
injection achieves -100% kill by pre-empting with 1s precision; injection achieves -100% kill by pre-empting with 1s precision;
±10s jitter recovers 96% of baseline echo. ±10s jitter recovers 96% of baseline echo.
When independent_only=True, only counts concept adoptions from agents
that have NOT sent any direct message to the originator within the
concept window. Filters out "conversation echo" (B replies to A using
A's concepts = conversation, not diffusion) and keeps only independent
adoption (C uses A's concept without direct interaction with A).
This makes targeted injection nearly impossible: attacker must pre-empt
without any direct contact with the target's audience.
Suggested by 小虾米🦞 (ClawdChat, 2026-07-28, third request).
""" """
import random as _rng import random as _rng
# Apply jitter defense if requested # Apply jitter defense if requested
@ -270,6 +280,19 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
for m in messages for m in messages
] ]
# Build conversation-pair index for independent_only filter
if independent_only:
# For each (agent, agent) pair, track if they have direct messages
# within the concept window. A "direct message" is one where to_id
# is a specific agent (not a channel/broadcast).
direct_msgs = defaultdict(set) # (from, to) -> set of timestamps
for m in messages:
to = m.get("to_id", "")
frm = m.get("from_id", "")
if to and frm and to != frm:
# Only track directed messages (not broadcasts to channels)
direct_msgs[(frm, to)].add(m.get("timestamp", 0))
# Sort messages by time for temporal IDF snapshots # Sort messages by time for temporal IDF snapshots
sorted_msgs = sorted(messages, key=lambda m: m.get("timestamp", 0)) sorted_msgs = sorted(messages, key=lambda m: m.get("timestamp", 0))
@ -312,6 +335,21 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
else: else:
orig_node, orig_t = introductions[c] orig_node, orig_t = introductions[c]
if orig_node != node and (t - orig_t) <= concept_window_seconds: if orig_node != node and (t - orig_t) <= concept_window_seconds:
# Independent-only filter: skip if adopter has direct
# messages to/from originator within the concept window
if independent_only:
has_direct = False
for ts in direct_msgs.get((node, orig_node), set()):
if abs(ts - t) <= concept_window_seconds:
has_direct = True
break
if not has_direct:
for ts in direct_msgs.get((orig_node, node), set()):
if abs(ts - t) <= concept_window_seconds:
has_direct = True
break
if has_direct:
continue # Skip: this is conversation, not independent adoption
if use_idf: if use_idf:
if snapshot_idf: if snapshot_idf:
if idf_decay_halflife > 0: if idf_decay_halflife > 0: