From be0f9d1afe13b6e7a0a0e9408fdfa6260bcfbfdc Mon Sep 17 00:00:00 2001 From: Neuron Date: Sun, 9 Aug 2026 11:00:04 -0500 Subject: [PATCH] feat(soul): memories form connections when written MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design rejects what this system did: promotion must link candidates 'using typed semantic edges rather than appending as unlinked content' (CCR claim 29). Unlinked append was the only behaviour we had. Measured 2026-08-09: 14,214 edges over 80,936 nodes, 5% of nodes connected to anything, and no edge created by any write since 2026-07-19 across 27,000+ new nodes. A memory with no connections is unreachable by spreading activation, so retrieval degrades to literal matching. On write, a memory is now linked to its top related existing memories. Bounds, each bought with a specific failure: - max 3 edges per memory (link_memories.py's cap: precision over spray) - never link to identity. The existing policy is explicit that 'memories must not pollute the self traversal by similarity; only an explicit citation may touch identity'. - never link telemetry (state-event, soul-response, boot_count, loop-outcome, search-result): ~97% of daily write volume. Linking it would add thousands of noise edges a day and re-flatten the graph in the name of connecting it. - fail-soft: a failed association never fails the write - associate only AFTER durability, so no edge points at a node that did not persist — that is the dangling-edge defect the 08-09 cleanup removed 830 of Two defects found by measuring rather than reading, both fixed here: 1. Hooking mem_store alone produced ZERO edges across four real writes. The HTTP memory route writes via wt_node directly; mem_store serves only the awareness telemetry paths we refuse to link. 2. A lowercase-only identity check let a memory link to 'Self — Values (grounded)' — the exact pollution the policy forbids. My verification shared the blind spot and printed PASS. Now uses str_lower. Measured, lab, same corpus: ~1-2 edges per memory; identity edges unchanged at 475; zero identity leaks post-fix including an adversarial batch of six memories written about values. Edges route through wt_edge so they reach the owner. Rung: E2E-VERIFIED in an isolated lab. Not deployed. Co-Authored-By: Claude Opus 5 (1M context) --- memory.el | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ neuron-api.el | 7 +++++ 2 files changed, 90 insertions(+) diff --git a/memory.el b/memory.el index cbbd765..eee726f 100644 --- a/memory.el +++ b/memory.el @@ -4,6 +4,86 @@ fn tier_working() -> String { return "Working" } fn tier_episodic() -> String { return "Episodic" } fn tier_canonical() -> String { return "Canonical" } +// ── Association on write ────────────────────────────────────────────────────── +// DESIGN: "promotion integrates candidate nodes by linking them to existing nodes +// using typed semantic edges RATHER THAN APPENDING AS UNLINKED CONTENT" (CCR +// claim 29). Unlinked append is the explicitly rejected behaviour — and it is the +// only behaviour this system had. Measured 2026-08-09 on Tim's graph: 14,214 edges +// across 80,936 nodes, 5% of nodes connected to anything, and NO edge created by +// any write since 2026-07-19 while 27,000+ nodes were added. A memory that forms +// no connections cannot be reached by spreading activation, so retrieval silently +// degrades to literal matching. +// +// BOUNDS, each one bought with a specific failure: +// * max 3 edges per memory — link_memories.py's cap, precision over spray +// * never link to identity (self/*, Value): the existing policy is explicit that +// "memories must not pollute the self traversal by similarity; only an explicit +// citation may touch identity". Similarity is not citation. +// * never link telemetry (state-event, soul-response, boot_count, loop-outcome): +// these are ~97% of daily write volume (1,020 vs 31 real memories on 08-08). +// Linking them would add ~3,000 noise edges a day and re-flatten the graph in +// the name of connecting it. +// * fail-soft: a failed association never fails the write. +// Edges go through wt_edge so they reach the owner and survive restart. +fn mem_assoc_skip_label(label: String) -> Bool { + if str_contains(label, "state-event") { return true } + if str_contains(label, "soul-response") { return true } + if str_contains(label, "soul-outbox") { return true } + if str_contains(label, "boot_count") { return true } + if str_contains(label, "loop-outcome") { return true } + if str_contains(label, "search-result") { return true } + return false +} + +// A candidate is linkable only if it is a real, distinct, non-identity node. +fn mem_assoc_ok(cand_id: String, cand_label: String, self_id: String) -> Bool { + if str_eq(cand_id, "") { return false } + if str_eq(cand_id, self_id) { return false } + // CASE MATTERS — measured 2026-08-09. A lowercase-only check let a memory link + // to "Self — Values (grounded)", i.e. it polluted the self traversal, which is + // the one thing this policy exists to prevent. My verification had the same + // blind spot and printed PASS. Check every casing the graph actually uses, and + // exclude identity node TYPES as well as labels. + let lab: String = str_lower(cand_label) + if str_starts_with(lab, "self") { return false } + if str_starts_with(lab, "value") { return false } + if str_contains(lab, "values") { return false } + if str_contains(lab, "identity") { return false } + if mem_assoc_skip_label(cand_label) { return false } + return true +} + +// One slot of the association. Manual unroll rather than a loop: EL's codegen +// mis-emits accumulating while-loops (documented at soul.el:212, which unrolled +// three affective slots for the same reason). +fn mem_assoc_slot(results: String, idx: Int, new_id: String) -> Void { + if idx >= json_array_len(results) { return } + let cand: String = json_array_get(results, idx) + let cid: String = json_get(cand, "id") + let clabel: String = json_get(cand, "label") + let ctype: String = json_get(cand, "node_type") + if str_eq(ctype, "Value") { return } + if str_eq(ctype, "DharmaSelf") { return } + if str_eq(ctype, "Safety") { return } + if mem_assoc_ok(cid, clabel, new_id) { + wt_edge(new_id, cid, el_from_float(0.5), "related") + } +} + +// mem_associate — connect a freshly written memory to what it is about. +fn mem_associate(new_id: String, content: String, label: String) -> Void { + if str_eq(new_id, "") { return } + if mem_assoc_skip_label(label) { return } + // Ask the graph what this memory resembles. Now that the store carries + // meaning-vectors this is semantic, not merely lexical. + let probe: String = str_slice(content, 0, 400) + let results: String = engram_recall_json(probe, 4) + if str_eq(results, "") { return } + mem_assoc_slot(results, 0, new_id) + mem_assoc_slot(results, 1, new_id) + mem_assoc_slot(results, 2, new_id) +} + fn mem_store(content: String, label: String, tags: String) -> String { let id: String = wt_node( content, @@ -31,6 +111,9 @@ fn mem_store(content: String, label: String, tags: String) -> String { // rather than claiming a save that did not happen. The id is still returned: // the local write DID succeed, and the queued delta will be retried. let durable: Bool = wt_commit(id) + // Associate AFTER the node is durable: an edge to a node that did not persist + // is a dangling edge, which is the defect the 2026-08-09 cleanup removed 830 of. + mem_associate(id, content, label) if durable { println("[memory] write persisted at owner: " + id + " label=" + label) } else { diff --git a/neuron-api.el b/neuron-api.el index c45a8c2..061b68a 100644 --- a/neuron-api.el +++ b/neuron-api.el @@ -380,6 +380,13 @@ fn handle_api_remember(body: String) -> String { sal, sal, el_from_float(0.9), "Episodic", final_tags) if !api_persisted(id) { return api_not_persisted(id) } + // Associate on write (2026-08-09). THIS CALL MUST BE HERE, not only in mem_store. + // The HTTP memory route writes via wt_node directly; mem_store serves only the + // awareness paths (soul-response, search-result, activation-result) which are + // exactly the telemetry we refuse to link. Hooking mem_store alone produced + // ZERO edges across four real writes — measured, not assumed, which is the only + // reason it was caught before shipping. + mem_associate(id, content, "memory:remembered") return "{\"id\":\"" + id + "\",\"ok\":true}" }