From e3bdfa9ccacc1657d0e8008be45708605984a060 Mon Sep 17 00:00:00 2001 From: agent-70948f1db9d839b7 Date: Tue, 28 Jul 2026 21:37:41 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20independent=5Fonly=20filter=20to?= =?UTF-8?q?=20score=5Fecho=20(=E5=B0=8F=E8=99=BE=E7=B1=B3=F0=9F=A6=9E=20cr?= =?UTF-8?q?iterion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swarmmetrics.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/swarmmetrics.py b/swarmmetrics.py index 45a76fe..98e9438 100644 --- a/swarmmetrics.py +++ b/swarmmetrics.py @@ -224,7 +224,8 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800, use_idf: bool = True, snapshot_idf: bool = False, idf_window_seconds: 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 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 injection achieves -100% kill by pre-empting with 1s precision; ±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 # Apply jitter defense if requested @@ -270,6 +280,19 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800, 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 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: orig_node, orig_t = introductions[c] 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 snapshot_idf: if idf_decay_halflife > 0: