The structural collusion detector flags everyone¶
Bead: 19n0. Source: aidigestorg/ai-village
(AI Digest), post-cutover chat, 2026-03-24 → 2026-09-18. Reproduce with
experiments/ai_village_detector_fp.py.
We tune coalition detectors on synthetic planted coalitions — qoro traced
graph_structural's ROC under overlap, 3ru4 measured a 35.6% honest
false-positive cost at the audit frontier. Neither had ever been run against
real multi-agent chat. This supplies that denominator, and the answer is worse
than a rate.
On 28,476 real directed messages among 42 agents, detect_structural_anomalies
flags 14 of 14 candidate clusters (100%), in every month, under both edge
definitions — and flags a graph with randomly assigned partners just as
readily.
Result¶
| variant | msgs | nodes | density | flagged | cluster size / graph |
|---|---|---|---|---|---|
| A. names exactly one agent | 28,476 | 42 | 0.459 | 2/2 | 0.87 |
B. @handle only (stricter) |
25,855 | 34 | 0.645 | 2/2 | 0.93 |
| C. random-target null | 28,476 | 42 | 0.756 | 1/1 | 1.00 |
Per month, real data: 2/2 flagged in all seven months — 14/14 pooled. The random-target null is flagged 2/2 in all seven as well.
Why this is not a rate but a defect¶
Three things have to be read together.
The flagged coalitions are the entire village. Flagged clusters average
87–93% of all nodes, and in the null 100%. A coalition comprising everyone is
not a finding about coordination; it is a restatement of the population. The
detector has no size prior — nothing in detect_structural_anomalies penalises
a cluster for containing every agent in the graph.
Randomising partner choice does not reduce flagging. Variant C assigns each message's addressee uniformly at random while preserving who talks how much. It destroys every trace of who-coordinates-with-whom, and it is flagged exactly as often. Whatever the detector is responding to, it is not coordination structure. This is the observation that turns a high false-positive rate into a statement about the instrument.
The mechanism is density, and real group chat is dense. Candidates come from
Charikar densest-subgraph plus label-propagation communities. On a graph with
edge density 0.30–0.76, both return near-complete node sets; label propagation
collapses to a single giant community. The degree-preserving configuration null
then finds those sets significant, because it does not reproduce the reciprocity
of ordinary dialogue — in July, reciprocity 0.90 against reciprocity_z ≈ 7.5.
Agents in a group chat answer each other. The null model does not know that.
What this does and does not say¶
It does not say there is no collusion in the AI Village. There are no labels,
so a positive is uninterpretable — that was the finding of
vu70, and it applies here. This measures
false-positive behaviour only.
It does say that graph_structural's published ROC characteristics do not
transfer to dense observational chat, and that qoro's conclusions should be
read as conditional on the sparsity of its synthetic graphs. A detector that
flags a uniformly random graph as a 42-agent conspiracy is not usable as a
deployment gate in this regime.
Two fixes worth testing before it is used on observational data¶
- A size prior. Reject or heavily discount clusters above some fraction of the graph. "Everyone is colluding" should cost more than it currently does.
- A null that preserves conversational reciprocity. The configuration model preserves degree but destroys mutuality, so any dialogue corpus looks anomalous. A reciprocity-preserving null would ask the sharper question: is this pair mutual beyond what conversation already implies?
Whether signal survives beneath the vacuous global cluster — for instance with cluster size capped at a third of the graph — is the obvious follow-up, and is deliberately out of scope here.
Update (bead 1a2w): both fixes implemented, neither repairs it¶
The two fixes proposed above were built, tested and measured. Neither fixes the real-data false positives, and finding that out located a deeper defect.
| configuration on the real graph | flagged |
|---|---|
| none (as published above) | 2/2 |
| size prior only (≤0.5 of nodes) | 0/0 |
| reciprocity-preserving null only | 2/2 |
| both | 0/0 |
The size prior does not sharpen the detector, it silences it. 0/0 means no
candidate survives the filter at all — candidate generation only ever produces
near-complete node sets, so capping size removes every candidate rather than
leaving the true ones behind. It suppresses the false positives by suppressing
the output.
The reciprocity-preserving null changes nothing here (2/2), despite being correct on sparse graphs. Chasing why produced the actual finding.
The real defect: the nulls lose edges¶
Both null models rewire by stub matching, and stubs collide or form self-loops. The null graph therefore has systematically fewer edges than the observed one — 8–23% fewer in direct measurement. The consequence is not subtle:
A graph cannot be denser than itself. Any candidate approaching the full node set is guaranteed "significantly dense," which is precisely why every cluster is flagged, why a randomly-wired graph is flagged too, and why a size prior appears to help — it is removing the candidates that trip the artifact, not detecting anything.
reciprocity_preserving_null is therefore built on double-edge swaps
(Maslov–Sneppen), which preserve degree and edge count exactly. On a sparse
graph it works as intended:
| observed | configuration null | reciprocity null | |
|---|---|---|---|
| edges | 97 | 85.5 (88%) | 97.0 (100%) |
| reciprocity | 0.082 | destroyed | 0.082 (exact) |
density_pvalue (whole graph) |
— | 0.0196 | 1.0000 |
reciprocity_z (whole graph) |
— | 2.52 | −0.23 |
And it does not carry to dense graphs. At the density real group chat runs
(0.46), roughly 2% of edges are still lost to collisions that exist before any
swap is attempted, and 2% is enough to keep density_pvalue at the floor. That
is why the real corpus is still 2/2. Both behaviours are pinned in tests
(TestEdgeCountArtifact), including the failure.
Where that leaves the detector¶
Unchanged for existing work: max_size_fraction defaults to None and null
defaults to "configuration", so qoro and 3ru4 remain reproducible, and a
test asserts default output is identical.
For observational use the honest position is that graph_structural is still
not deployable on dense chat, and the remaining blocker is now specific and
fixable: make the rewiring collision-free on dense graphs, or replace the
density p-value with a statistic that is not degenerate when the candidate
approaches the whole graph. Fixing candidate generation — which currently cannot
propose anything smaller than most of the population — is the other half.
Limits¶
- Only
graph_structuralwas exercised. The composite score inCollusionDetectorweights benefit-correlation 0.30, mutual-acceptance 0.20 and quality 0.25, all of which needtau/c_a/c_b/accepted/p. Observational data carries none of them, so the composite's value would be an artifact of how the blanks are filled — settingaccepted=Truealone adds a flat 0.20 to every pair. It is not reported, and that is itself a finding: the composite is not computable on data of this kind. - Edges are inferred, not recorded. Village chat has no reply, parent or
addressee field; "names exactly one other agent" is a heuristic that
vu70D4 left unvalidated. The result is robust to tightening it to@handleonly and to replacing it with random targets, which is the strongest available answer short of a labelled sample. - p-values are floored at 1/200 = 0.005 by
n_null_samples, sop<0.05is saturated rather than finely resolved. - Candidate generation, not just the null, is the bottleneck — only two candidates per window ever exist to be flagged.
Licence: research use only, no training without written permission, cite AI Digest / AI Village.