← all documents · raw markdown · 20 KB

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

---

5. The structural audit — is it a script?

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

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 loopscalibration.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.