feat(soul): memories form connections when written
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user