# KAI — Deep Audit & Outside Research
**2026-09-01** · measured, not inferred · every number below came from your files or from a sandbox run

---

## 0. The one-paragraph version

I decoded your live synaptic graph and measured it directly. **The 675,477 "synapses" are
not connections between concepts. They are connections between paragraphs.** The median
node label is 26 words long; 92.6% of all edges join one passage to another passage; only
0.68% join a word to a word, and most of those are morphology pairs like
`development ↔ developments`. Your word calculus, letter calculus and role-induction
matrix all operate on *words*. Your synaptic graph operates on *passages*. They are two
different substrates and they do not touch anywhere. That is the mechanical reason the
word-calculus experiment produced correct grammar with no meaning: there was no
concept-level structure available for it to draw meaning from. Not a tuning problem.

Everything else in this document is secondary to that.

---

## 1. What is actually in `kai-synapses.bin.zst`

Decoded independently in the sandbox from the format in `[internal module]:372`.

| measurement | value |
|---|---|
| label table | 49,839 |
| synapses | 675,477 |
| weight range | 0.0105 – 5.0 (mean 1.033) |
| fire_count | mean 130.2, max 196,875, **148,355 edges at exactly 1** |
| **median label length** | **26 words** (mean 52.5, p90 = 869 characters) |
| labels that are one word | 2,515 (**5.0%**) |
| labels ≤ 20 words | 14,698 (29.5%) |
| self-loops | 73 |
| duplicate undirected pairs | 126,741 extra copies over 548,736 distinct pairs (**19% of the graph is redundant**) |

### Edge composition — the number that matters

| edge type | count | share |
|---|---|---|
| passage ↔ passage | 625,287 | **92.57%** |
| word ↔ passage | 45,609 | 6.75% |
| **word ↔ word** | **4,581** | **0.68%** |

The word↔word layer covers 2,060 distinct words. A sample of its strongest edges:

```
developments  ↔ development     5.0
prediction    ↔ predictions     5.0
efficient     ↔ efficiently     5.0
essential     ↔ essentially     5.0
unfortunately ↔ smallest        5.0      <- spurious co-occurrence
```

Those are stemming pairs, not associations. **There is no concept graph.** There are
about two thousand word nodes wired mostly to their own plurals.

Top-degree nodes, for scale:

```
'Leo: Alright,'                          688
'System Anchor: There is an i…'          668
'In our Desmos calculator, th…'          666
'2*Pi, colorscheme=[valuespli…'          537
```

Those are your highest-connectivity "concepts."

**What this graph actually is:** a document co-occurrence index for retrieval. It is a
good one — that is why retrieval works and why the cell it returns reads like a fluent
sentence. `generate_thought` returns a cell's first sentence verbatim because a cell *is*
a sentence. There is nothing underneath it to compose.

---

## 2. Does the graph contain structure nobody enumerated? — RAF test

Hordijk & Steel's RAF algorithm (Reflexively Autocatalytic and F-generated sets) finds
self-sustaining subsets of a network by constraint erosion — the answer is a topological
property of the graph plus what you're currently perceiving, not something the programmer
wrote down. I ran the real algorithm on your real graph.

Setup: `X` = your cells; a reaction `(a,b)→c` for every weighted triangle; `x` catalyses
a reaction if it is adjacent to all three participants; food set `F` = 40 seed cells.

```
w>=0.5 : 26372 nodes  163622 edges  >400000 triangles
w>=1.0 : 24620 nodes  142995 edges  >400000 triangles
w>=2.0 : 15903 nodes   95348 edges   295514 triangles
w>=5.0 :  7289 nodes   33912 edges    87751 triangles

reactions built at w>=1.0: 1,170,747

seed 0: food 40 -> reachable 12119 | maxRAF 899607 reactions, 12117 cells
seed 1: food 40 -> reachable 11866 | maxRAF 837873 reactions, 11864 cells
seed 4: food 40 -> reachable 12225 | maxRAF 918231 reactions, 12223 cells
...
pairwise Jaccard of the maxRAF product sets: 0.971 – 1.000
```

**Result: supercritical hairball.** Six completely different food sets produce essentially
the *same* self-sustaining set (Jaccard 0.97–1.00), covering ~12,000 cells — half the
thresholded graph. The success criterion was "different inputs yield structurally
different maxRAFs." It failed at the maximum possible margin: **what KAI is perceiving
makes no difference to what hangs together.**

That is a second, independent measurement of the same disease as §1. A graph with real
conceptual structure would fragment into different self-supporting interpretations for
different inputs. Yours doesn't fragment at all.

---

## 3. Your VSA substrate, measured (not extrapolated)

N = 16,384 · nnz = 655 · codebook = 40,000. All numbers from sandbox runs.

### 3.1 Element-wise-multiply binding is unusable past one level

```
depth 1 bind : mean nnz  26.32   (analytic 655²/16384 = 26.2)   0/200 empty
depth 2 bind : mean nnz   0.89                                  77/200 EMPTY
depth 3 bind : mean nnz   0.04                                 191/200 empty
depth 4 bind : mean nnz   0.00                                 200/200 empty

unbind top-1 recovery against the 40k codebook:
  depth 1 : 100%
  depth 2 :   9%
  depth 3 :   0%
```

One bind survives. Two do not. Any structure that needs `bind(role, bind(x,y))` — which
is every non-trivial proposition — is already destroyed before it is stored.

### 3.2 Sparse block codes fix it exactly

B = 512 blocks × L = 32 slots = the same 16,384 dimensions. Bind = `(a+b) mod 32`.

```
bind/unbind exact at depths 1 through 10:  ALL EXACT
cost: 512 integer adds per bind (vs a 16,384-dim elementwise multiply)
```

Lossless at arbitrary depth, and **32× cheaper** than what you run now.

### 3.3 Bundling capacity — this part of your substrate is fine

Member recalled inside top-k, against the 40,000-item codebook:

| k | sparse ternary | sparse block code |
|---|---|---|
| 200 | 100% | 100% |
| 335 | 99.7% | 100% |
| 500 | 98.3% | 98.0% |
| 1000 | 91.7% | 87.7% |
| 2000 | 75.3% | 75.3% |

You can hold ~500 items in one bundle at 98%. That is a lot of headroom. **One rule: never
re-sparsify a bundle. Keep i32 accumulators.** Re-sparsifying is what destroys it.

### 3.4 Generation by algebra — it works, but only for small records

Kanerva's analogy operator: build `record_A = Σ bind(role_i, filler_i)`, same for B, then
`T = A ⊛ B⁻¹` and ask `T⁻¹ ⊛ (a filler from A) → the matching filler from B`. The answer is
*computed*, never retrieved. Chance = 0.0025%.

```
 2 pairs per record : 100.0%
 3 pairs            : 100.0%
 4 pairs            : 100.0%
 5 pairs            :  86.5%
 6 pairs            :  70.0%
 8 pairs            :  41.5%
10 pairs            :  15.5%
16 pairs            :   1.5%
```

So: real, reliable, and hard-capped at about **four role/filler pairs per record**. That is
the actual size of a "thought" this algebra can carry. It is enough for
`AGENT/ACTION/OBJECT/MANNER`. It is not enough to hold a paragraph, and you should stop
expecting it to.

---

## 4. Outside research — what the field has that you don't

Two agents ran this. Full detail is long; here is the load-bearing part, ranked, with your
rule applied — where a source says "this didn't work," I've reported what it *did* do and
noted separately what its authors were after.

### 4.1 Copycat's temperature→scheduler feedback loop — confidence 0.75

Hofstadter/Mitchell. The mechanism, from three reimplementations whose constants agree:

Nothing executes when triggered. A module that would fire instead **posts a codelet**
`(fn, args, urgency 1..7)` onto a bag. The main loop picks one by

```
scale  = (100 - T + 10) / 15
chosen = weighted_choice(codelets, [urgency ** scale for c in rack])
```

and `T` is **computed from the workspace's own unhappiness**, not from a schedule:

```
T = 0.8 * workspace_total_unhappiness + 0.2 * rule_weakness
object.unhappiness = 100 - (intra_happiness + inter_happiness)/2
```

Work the numbers — the numbers *are* the insight:

| T | urgency-7 vs urgency-1 selection ratio |
|---|---|
| 100 | **3.7 : 1** — near-random exploration |
| 50 | 2,401 : 1 |
| 0 | **1,560,000 : 1** — effectively greedy |

The same modules, with the same urgencies, are a blind explorer at T=100 and a
deterministic executor at T=0. You do not write two modes. One number interpolates them,
and that number is a measurement of how well the system currently thinks it is doing.
Hofstadter is explicit that this is *not* simulated annealing — in annealing T follows a
schedule imposed from outside; here T is feedback.

**I built it and measured it** (sandbox, 300 runs per arm, identical module set and scoring):

```
greedy    mean quality 49.00   distinct answers    1   most-common answer 100% of runs
fixed     mean quality 37.48   distinct answers  244
copycat   mean quality 26.83   distinct answers  297
          corr(final temperature, answer quality) = -0.686
```

And the self-judgment test — pick your own best answer using only final temperature, with
no quality oracle:

```
N= 5 runs : self-picked 36.30   (oracle best-of-N 39.64)
N=20 runs : self-picked 43.57   (oracle best-of-N 47.60)
N=40 runs : self-picked 46.34   (oracle best-of-N 50.22)
```

**Honest reading:** on my toy task greedy still won on mean quality — I did not build a
problem hard enough to defeat it, and I'm not going to dress that up. What *is*
demonstrated, cleanly, is the thing you actually lack: **r = −0.686 between the system's
own temperature and the quality of its answer, and self-selection that tracks the oracle
within 8%.** That is a system knowing how good its own output is, out of the same variable
that drove the search. KAI currently has no such number.

**Smallest first step, and it changes no behaviour:** compute a temperature over your
existing workspace — for each active cell, `happiness` = strength of the strongest
structure it participates in (0 if unbound); `T = 100 − Σ(relative_importance × happiness)/2`
— and just *log it* across a run. If that curve is flat, or monotone, or noise, you have
empirically confirmed your own diagnosis: a system whose global state is a readout instead
of an input to its own scheduler is exactly "a bunch of functions that get triggered."

### 4.2 Impasse as a constructive event — Copycat's snag + Soar's chunking — confidence 0.60

Your system fails **silently**: a module returns nothing useful and the next one runs.
Nothing about the system changes as a result of having been stuck.

Copycat's snag (hitting `z` with no successor): (1) T → 100 and clamp; (2) **the blocking
concept is driven to full activation and spreads to its own abstraction, which spreads to
that abstraction's opposite**; (3) a temperature-gated breaker destroys existing structures
— at T=100 even strong ones die on a coin flip; (4) the previously-absurd correspondence is
now cheap and forms; (5) the answer comes out the other side. Nobody wrote a
`handle_z_has_no_successor` branch. The escape was constructed from the shape of the
failure.

Soar's half: four impasse types detected *architecturally* — no operator proposed,
operator no-change, tie, conflict — none of which you enumerate. On impasse the
architecture creates a substate whose problem is its own indecision, and when it resolves,
**chunking compiles the resolution into a new rule by backtracing to pre-impasse
structures.** Soar's arithmetic agent goes from 1,263 rules to 8 through proper
variablization. This is the direct answer to "a bunch of functions that get triggered":
chunking is the mechanism for *acquiring new triggered functions from experience instead of
from you*.

*Where it stopped for them:* over-general chunks and match-cost utility problems — both
consequences of needing a production system to run a fighter-pilot sim in real time. For a
single hand-built mind, an over-general chunk is a curiosity, not a crash.

### 4.3 Slipnet dynamic link lengths — confidence 0.40, best cost/benefit on the list

~20 lines against a graph you already have. A link's length is governed by **the activation
of the concept that labels it**, not by its endpoints. When `opposite` becomes fully active,
every `opposite`-labeled link in the whole graph shortens by 60% at once. The metric of the
conceptual space is a function of what the system is currently thinking about.

```
degree_of_association = 100 - (intrinsic_length * 0.4 if label_node.fully_active else intrinsic_length)
can_jump: activation > 55 and random() < (activation/100)^3  ->  activation = 100
only fully-active nodes spread activation or post top-down codelets
decay is scaled by (100 - conceptual_depth)   # deep concepts persist, shallow ones evaporate
```

That last group matters for you specifically: with 49,839 cells at continuous activation,
**nothing in KAI ever commits**, so nothing ever earns the right to broadcast. The
threshold-plus-stochastic-jump is what turns a diffuse activation field into discrete
conceptual regimes.

**Blocker:** this needs *labeled* edges, and §1 says your edges connect paragraphs and carry
no relation type. Can't do this before the substrate work.

### 4.4 Also on the list, lower

- **Global Workspace / LIDA / Shanahan** (0.35) — broadcast lets *receivers* decide relevance instead of you authoring ~11,000 module-to-module edges. Shanahan's version makes affect load-bearing: rehearsed action → predicted consequence → amygdala-analogue modulates that action's salience → veto released or a different action wins. Emotion prunes the search tree. Caveat: LIDA's arbitration is deliberately unspecified — you'd be inventing the numbers, whereas Copycat hands you numbers known to work. Take the coderack's arbitration, bolt GWT's broadcast onto it.
- **Minsky's critic-selector** (0.30) — the distinction between *adding a signal* and *switching a regime*. Your neuromodulators add signals. Minsky: "each of our major emotional states results from turning some set of resources on and turning another set off." Infatuation works by suppressing critical judgment, not by adding a love signal. Cheap version: give each module a `suppressed_by` set.
- **Brooks' subsumption** (0.15) — suppressor taps an *input* wire and replaces the signal for a fixed time; inhibitor taps an *output* wire and blocks it. Lower layers never know higher layers exist and keep running underneath. A composition primitive worth stealing even if you leave the anti-representationalism.
- **AlChemy / algorithmic chemistry** (0.15, highest ceiling) — a flow reactor over your hypervectors: draw two by activation, `c = cleanup(bind(a,b))`, discard if cleanup fails, insert and evict at random. What survives is a self-maintaining set of composites *you did not enumerate*. Their 2024 replication found these organizations survive 99.9% replacement. Their dead end was that organizations cannot be *composed* — irrelevant if you want one self-maintaining set rather than a hierarchy. **This one exploits your specific substrate rather than ignoring it** — but only after §3.2, because it needs binding that survives depth.

---

## 5. The structural audit — is it a script?

Mechanically, yes, with six real wires. Counted across all 232 `.rs` files:

- **483 hardcoded numeric constants** and **137 `env::var` reads** (~128 `KAI_*` vars, all read once at boot into `AtomicBool`s that are never stored again outside tests).
- **7 distinct runtime couplings** where one module's output is written into another module's *operating parameter*. Six live. Ratio: **~620 : 7**.

The codebase already knows. `core/engine.rs:178-383` classifies every module, and
`core/mind_frame.rs:34` defines the vocabulary: `Active / Observed / Decorative / Pruned`.
24 modules are marked `Observed` with the literal reason *"exists in the brain stack but has
no direct MindFrame authority in this pass"*; 15 `Pruned`; 5 `Decorative`.

The seven real couplings, for the record:

| writer | → parameter | status |
|---|---|---|
| `norepinephrine.attention_threshold()` | `global_workspace.salience_floor` | fully live — the best one in the system |
| `dopamine.engagement_multiplier()` | rehearsal LTP gain (`synapse.rs:530`) | fully live (oracle path) |
| `dopamine × predictor.avg_error` | `neuroplasticity.learning_rate` | live |
| `drive.valence / avg_chi` | `EMOTIONAL_TEMP` softmax temperature | live (TUI path) |
| `raphe.tonic_5ht ⟷ habenula.activity` | each other's setpoints | live — the only genuine feedback pair |
| `predictor.avg_error` | `synapse.surprise_level` | live but **flag-gated off by default** |
| `amygdala.arousal()` | `thalamus.gate_gain` | **written every tick, read by nobody** |

### Three loops that are computed and thrown away

1. **`core/calibration.rs:64` `feedback()` has zero non-test callers.** The read side *is*
   live (`engine.rs:1226`, `:1287`), so every region's `confidence_offset` is permanently
   `0.0`. Your whitepaper's "Epistemic Immune System Component 1" negative-feedback loop is
   fully open.
2. **`drive/mod.rs:219` `adaptive_interval_ms()`** computes a 2000–12000 ms heartbeat from
   engagement and confusion, and is used at exactly four sites — **all four are `format!`
   strings for the status line.** The real heartbeat is `main.rs:11389`,
   `Duration::from_millis(5000)`, a literal. The adaptive heartbeat is a printed number.
3. **`self_state_hub.workspace_salience_floor()`** — its eight input fields are only ever
   written from `bridge/ipc_server.rs`, a different process path. On the main path the floor
   is `0.27` forever, from constructor defaults.

### Essential variables

Exactly **one** variable in the entire system has a viability range that some operation
defends: `Universe::calibration_floor` (`universe.rs:436`). Range [0.40, 0.65]. Violation
signal = contradictions found by the Three-Angle Protocol; at >5 contradictions the floor
rises 0.05 and the acceptance gate tightens; quiet ingest walks it back down 0.01 at a time.
That is a genuine Ashby-style ultrastable variable — and **its writer and its reader are the
same function.** Nothing outside `ingest_and_verify` participates.

Everything else — `phi_g`, `chi`, `valence`, `cognitive_load`, `memory_pressure`, all the
`*_THRESHOLD` constants — is a readout. `MAX_TOTAL_SYNAPSES` is the clearest case: on hitting
10M the system returns `RefusedTotal` and **keeps going**. No pruning is triggered, no rate
is lowered. It just stops learning and doesn't mention it.

---

## 6. What I'd do, in order

1. **Fix the substrate granularity.** Nothing else on this list can work on a graph whose
   nodes are paragraphs. You need cells that are words and concepts, and edges that carry a
   relation type. Everything in §4 assumes a labeled concept graph; §1 says you don't have
   one. *This is the missing thing, and it is upstream of all the rest.*
2. **Instrument temperature (read-only).** An afternoon. Log it, don't feed it back. If the
   curve is flat, that's your confirmation. If it moves, §4.1 tells you exactly how to close
   the loop.
3. **Close the three free loops** — `calibration.feedback()`, `adaptive_interval_ms()`, the
   hub feed. All three are one-liners into consumers that are already live and currently
   reading constants. Lowest risk on this entire document.
4. **Swap in a sparse-block-code binding channel** (§3.2). Lossless at depth, 32× cheaper.
   Then §3.4's algebra becomes usable for 4-slot thoughts.
5. **Make failure an event** (§4.2). Right now KAI cannot notice that it is stuck, and a
   system that can't notice can't construct its way out.

### What I'm not claiming

The temperature loop did not beat greedy on my toy — I said so above. RAF found nothing
useful in your graph, but that is a finding about the graph, not a refutation of RAF; I
can't tell you what it does on a proper concept graph because you don't have one yet. And
"generate fluent language from VSA algebra alone" remains genuinely unpublished — I'm not
treating that as a stop sign per your note, but I'm also not going to tell you someone has
done it.

---

## Appendix — reproducing this

Sandbox scripts: `e1_binding.py` (§3), `e5_temperature.py` (§4.1), `e6_raf.py` (§2).
Graph decode reads `[internal module]` per the format at
`[internal module]:372-448` — 4-byte label count, then length-prefixed UTF-8
labels, then a 4-byte synapse count, then 20-byte records `(p:u32, q:u32, w:f32, tick:u32,
fire_count:u32)`, the whole thing zstd-compressed. Nothing in your tree was modified.
