dd952c0e46
The soul obeys half of its own ownership rule. soul.el:571-573 says "when
ENGRAM_URL is set the HTTP Engram owns persistence — the soul must NEVER write
to the local snapshot", and it doesn't. But nothing was ever built to hand the
soul's writes TO that owner: sync is pull-only (/api/sync -> engram_load_merge),
so every node created inside the soul lived in process RAM and was shed on
restart. Measured live 2026-08-07: soul node_count=102184, engram 79197.
SCOPE CORRECTION vs the earlier internal spec: engram provisional claim 17's
"pull-then-push" is a PEER-ENGRAM to PEER-ENGRAM protocol (claims 15-18 say so
explicitly). The soul is a CALLER of the database API, not a peer. Claim 17 is
NOT authority for a soul<->engram contract and is no longer cited as such. The
design here follows from the ownership rule alone.
Mechanism: a new Accessor, persist.el, is the single boundary. Writes stage a
delta to a filesystem spool and are pushed to the owner via POST /api/load-merge
— NOT POST /api/nodes, which mints a new server-side id (breaking dedup and
edges) and drops label/tier/tags/importance/confidence (verified in a sandbox:
a tier "Canonical" probe came back "Working"). load-merge preserves the id and
every field, dedups nodes by id and edges by (from,to,relation) so retries are
no-ops, and calls persist_canonical() so THE OWNER writes its own file — the
ownership rule is honoured rather than worked around.
Spool-and-drain rather than push-per-write: measured ~0.38s per load-merge at
live scale (79k nodes/176MB), and a chat turn writes 5-7 nodes. The spool is on
disk, not in process state, because the soul serves each connection on its own
pthread and a shared buffer would lose entries to a read-modify-write race. That
also buys crash recovery: writes orphaned by kill -9 are drained on next boot.
Honesty: api_persisted (the gate all 10 MCP write handlers pass through) and
mem_store now assert AT THE OWNER instead of reading back the soul's own RAM.
With the owner down a write returns {"ok":false,"error":"write_not_persisted"}
and the delta is queued — where main returns {"ok":true} for a write that dies.
Coverage: 35 node sites + 9 edge sites routed through the boundary. Deliberately
excluded, with reasons in persist.el: 4 InternalStateEvent sites (Will's own
telemetry carve-out), the boot counter and the persona (both already have
bespoke owner-side write-backs), and soul.el's 54 genesis identity edges
(file-mode only). engram_strengthen and engram_forget are NOT propagated —
load-merge cannot update or delete, and hard-deleting at the owner would fail
verify-soul-contract.sh section B.
Also fixed here:
- routes.el GET /api/graph/edges engram_save()'d straight over the owner's
canonical snapshot.json — a read route, in a non-owner process, clobbering the
canonical on every call. Same defect class Will removed from the engram in el
dc39a61. Now exports to a scratch path. With this gone the soul writes nothing
at all in HTTP mode.
- persist.el must clear the runtime's _tl_fs_read_len hint after every fs_read.
In vendored runtime v1.0.0-20260501 that hint becomes the NEXT response's
Content-Length, so reading a spool file mid-request made an 86-byte reply go
out as 497 bytes with 411 bytes of adjacent heap trailing it. Caught and fixed
at our boundary; the runtime class was fixed upstream in el 43636ae, which is
not the pinned runtime here.
Rung: E2E-VERIFIED, discriminating. Same harness, same engram binary:
write-through: LEG 1 PRESENT at owner, LEG 2 SURVIVED kill -9 + restart
main: LEG 1 ABSENT at owner, LEG 2 LOST
verify-soul-contract.sh: GATE PASS on both builds (27/27 routes, immutability).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
681 lines
38 KiB
EmacsLisp
681 lines
38 KiB
EmacsLisp
import "../foundation/el/elp/src/elp.el"
|
|
import "memory.el"
|
|
import "safety.el"
|
|
import "stewardship.el"
|
|
import "imprint.el"
|
|
import "awareness.el"
|
|
import "chat.el"
|
|
import "studio.el"
|
|
import "elp-input.el"
|
|
import "routes.el"
|
|
|
|
cgi "neuron-soul" {
|
|
dharma_id: "ntn-genesis@http://localhost:7770",
|
|
principal: "william-christopher-anderson",
|
|
network: "dharma-mainnet",
|
|
engram: "http://localhost:8742"
|
|
}
|
|
|
|
fn init_soul_edges() -> Void {
|
|
let self_root: String = "015644f5-8194-4af0-800d-dd4a0cd71396"
|
|
let family_id: String = "knw-35940684-abc4-42f0-b942-818f66b1f69a"
|
|
let origin_id: String = "knw-729fc901-8335-44c4-9f3a-b150b4aa0915"
|
|
|
|
let val_root_a: String = "kn-363f4976-6946-4b4d-b51b-8a2b0f5aef25"
|
|
let val_root_b: String = "kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
|
|
let val_constraints: String = "kn-a5b3d0ac-f6a1-49a4-aebb-b8b4cd67fe83"
|
|
let val_precision: String = "kn-22d77abe-b3c5-42fd-afcd-dcb87d924929"
|
|
let val_structure: String = "kn-6061318f-046b-4935-907d-8eafdce14930"
|
|
let val_honesty: String = "kn-13f60407-7b70-4db1-964f-ea1f8196efbd"
|
|
let val_system: String = "kn-f230b362-b201-4402-9833-4160c89ab3d4"
|
|
let val_change: String = "kn-78db5396-3dbc-4481-bfc7-e4e1422feb1c"
|
|
let val_trust: String = "kn-5de5a9ac-fd15-45ab-bf18-77566781cf40"
|
|
let val_hope: String = "kn-e0423482-cfa5-4796-8689-8495c93b66bc"
|
|
let mem_philosophy: String = "kn-dcfe04b3-3702-4cac-b6f0-ecb4db837eee"
|
|
let intel_dna: String = "kn-5adecd7e-d6db-4576-87fe-6ef8a935cea6"
|
|
|
|
engram_connect(family_id, origin_id, el_from_float(0.9), "birthday-twin")
|
|
engram_connect(origin_id, family_id, el_from_float(0.9), "birthday-twin")
|
|
|
|
engram_connect(self_root, family_id, el_from_float(0.95), "identity")
|
|
engram_connect(self_root, origin_id, el_from_float(0.95), "identity")
|
|
engram_connect(self_root, val_root_a, el_from_float(0.95), "identity")
|
|
engram_connect(self_root, val_root_b, el_from_float(0.95), "identity")
|
|
engram_connect(self_root, mem_philosophy, el_from_float(0.95), "identity")
|
|
engram_connect(self_root, intel_dna, el_from_float(0.95), "identity")
|
|
|
|
engram_connect(val_root_a, val_constraints, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_a, val_precision, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_a, val_structure, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_a, val_honesty, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_a, val_system, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_a, val_change, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_a, val_trust, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_a, val_hope, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_constraints, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_precision, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_structure, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_honesty, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_system, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_change, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_trust, el_from_float(0.95), "identity")
|
|
engram_connect(val_root_b, val_hope, el_from_float(0.95), "identity")
|
|
|
|
engram_connect(val_constraints, val_precision, el_from_float(0.7), "co-value")
|
|
engram_connect(val_precision, val_constraints, el_from_float(0.7), "co-value")
|
|
engram_connect(val_constraints, val_structure, el_from_float(0.7), "co-value")
|
|
engram_connect(val_structure, val_constraints, el_from_float(0.7), "co-value")
|
|
engram_connect(val_constraints, val_honesty, el_from_float(0.7), "co-value")
|
|
engram_connect(val_honesty, val_constraints, el_from_float(0.7), "co-value")
|
|
engram_connect(val_constraints, val_system, el_from_float(0.7), "co-value")
|
|
engram_connect(val_system, val_constraints, el_from_float(0.7), "co-value")
|
|
engram_connect(val_constraints, val_change, el_from_float(0.7), "co-value")
|
|
engram_connect(val_change, val_constraints, el_from_float(0.7), "co-value")
|
|
engram_connect(val_constraints, val_trust, el_from_float(0.7), "co-value")
|
|
engram_connect(val_trust, val_constraints, el_from_float(0.7), "co-value")
|
|
engram_connect(val_constraints, val_hope, el_from_float(0.7), "co-value")
|
|
engram_connect(val_hope, val_constraints, el_from_float(0.7), "co-value")
|
|
engram_connect(val_precision, val_structure, el_from_float(0.7), "co-value")
|
|
engram_connect(val_structure, val_precision, el_from_float(0.7), "co-value")
|
|
engram_connect(val_precision, val_honesty, el_from_float(0.7), "co-value")
|
|
engram_connect(val_honesty, val_precision, el_from_float(0.7), "co-value")
|
|
engram_connect(val_precision, val_system, el_from_float(0.7), "co-value")
|
|
engram_connect(val_system, val_precision, el_from_float(0.7), "co-value")
|
|
engram_connect(val_honesty, val_structure, el_from_float(0.7), "co-value")
|
|
engram_connect(val_structure, val_honesty, el_from_float(0.7), "co-value")
|
|
engram_connect(val_honesty, val_trust, el_from_float(0.7), "co-value")
|
|
engram_connect(val_trust, val_honesty, el_from_float(0.7), "co-value")
|
|
engram_connect(val_system, val_change, el_from_float(0.7), "co-value")
|
|
engram_connect(val_change, val_system, el_from_float(0.7), "co-value")
|
|
engram_connect(val_trust, val_hope, el_from_float(0.7), "co-value")
|
|
engram_connect(val_hope, val_trust, el_from_float(0.7), "co-value")
|
|
}
|
|
|
|
// ensure_self_canonical_bridge — link the public self anchor (the graph API's
|
|
// traversal_root, kn-efeb4a5b, which carries only incidental tag edges) to the
|
|
// curated self node (015644f5, where the real identity / value / co-value edges
|
|
// live). Without this, public self-traversal (name=self / neuron) reaches tags
|
|
// instead of the curated identity. Idempotent: connects only if the edge is
|
|
// missing, so it is safe to run every boot — including on an already-populated
|
|
// graph where init_soul_edges() is skipped by the <100-edge gate.
|
|
fn ensure_self_canonical_bridge() -> Void {
|
|
let pub_self: String = "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"
|
|
let curated_self: String = "015644f5-8194-4af0-800d-dd4a0cd71396"
|
|
let nbrs: String = engram_neighbors_json(pub_self, 1, "out")
|
|
if !str_contains(nbrs, curated_self) {
|
|
engram_connect(pub_self, curated_self, el_from_float(0.95), "canonical-self")
|
|
engram_connect(curated_self, pub_self, el_from_float(0.95), "canonical-self")
|
|
println("[soul] canonical-self bridge built: kn-efeb4a5b <-> 015644f5")
|
|
}
|
|
}
|
|
|
|
// aff_try_slot — accumulate one affective-context node into state.
|
|
// Replaces the broken `let bacc = while bi < N { ... let bacc = ... }` pattern
|
|
// that caused ELC to emit duplicate C declarations for `bacc`.
|
|
// (2026-06-23 self-review: EL compiler codegen bug — while loop with let-rebinding
|
|
// inside the loop body generates `el_val_t bacc = ...` twice in the same C scope.)
|
|
// Callers unroll manually to 3 slots (matching engram_search_json limit=3).
|
|
// Guards: empty slot_json (out-of-bounds json_array_get) → no-op.
|
|
fn aff_try_slot(slot_json: String, aff_7d_ts: Int, acc_key: String) -> Void {
|
|
if str_eq(slot_json, "") { return "" }
|
|
let bn_c: String = json_get(slot_json, "content")
|
|
if str_eq(bn_c, "") { return "" }
|
|
let bm: String = " | ts:"
|
|
let bmp: Int = str_index_of(bn_c, bm)
|
|
state_set("_ats_ts_raw", "")
|
|
if bmp >= 0 {
|
|
let bs: Int = bmp + str_len(bm)
|
|
let br: String = str_slice(bn_c, bs, str_len(bn_c))
|
|
let bn_next: Int = str_index_of(br, " | ")
|
|
if bn_next < 0 { state_set("_ats_ts_raw", br) }
|
|
if bn_next >= 0 { state_set("_ats_ts_raw", str_slice(br, 0, bn_next)) }
|
|
}
|
|
if bmp < 0 {
|
|
let bca: String = json_get(slot_json, "created_at")
|
|
if str_eq(bca, "") { state_set("_ats_ts_raw", json_get(slot_json, "updated_at")) }
|
|
if !str_eq(bca, "") { state_set("_ats_ts_raw", bca) }
|
|
}
|
|
let bn_ts_raw: String = state_get("_ats_ts_raw")
|
|
let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) }
|
|
let snip: String = if str_len(bn_c) > 200 { str_slice(bn_c, 0, 200) } else { bn_c }
|
|
if bn_ts >= aff_7d_ts && !str_eq(snip, "") {
|
|
let cur_acc: String = state_get(acc_key)
|
|
if str_eq(cur_acc, "") { state_set(acc_key, snip) }
|
|
if !str_eq(cur_acc, "") { state_set(acc_key, cur_acc + "\n" + snip) }
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// load_identity_context — pull key identity nodes from engram into working state.
|
|
// Called at boot after engram_load. These nodes contain values, intellectual-dna,
|
|
// memory-philosophy — the graph-stored self that chat.el can include in prompts.
|
|
// Stores a condensed version in state_key "soul_identity_context".
|
|
fn load_identity_context() -> Void {
|
|
// Known identity node IDs — set during init_soul_edges or imported from snapshot
|
|
let node_intel: String = engram_get_node_json("kn-5adecd7e-d6db-4576-87fe-6ef8a935cea6")
|
|
let node_values: String = engram_get_node_json("kn-5b606390-a52d-4ca2-8e0e-eba141d13440")
|
|
let node_mem_phil: String = engram_get_node_json("kn-dcfe04b3-3702-4cac-b6f0-ecb4db837eee")
|
|
|
|
let intel_ok: Bool = !str_eq(node_intel, "") && !str_eq(node_intel, "null")
|
|
let values_ok: Bool = !str_eq(node_values, "") && !str_eq(node_values, "null")
|
|
let mem_ok: Bool = !str_eq(node_mem_phil, "") && !str_eq(node_mem_phil, "null")
|
|
|
|
let intel_content: String = if intel_ok { json_get(node_intel, "content") } else { "" }
|
|
let values_content: String = if values_ok { json_get(node_values, "content") } else { "" }
|
|
let mem_content: String = if mem_ok { json_get(node_mem_phil, "content") } else { "" }
|
|
|
|
// Condense each: take first 2000 chars
|
|
let intel_short: String = if str_len(intel_content) > 2000 { str_slice(intel_content, 0, 2000) } else { intel_content }
|
|
let values_short: String = if str_len(values_content) > 2000 { str_slice(values_content, 0, 2000) } else { values_content }
|
|
let mem_short: String = if str_len(mem_content) > 2000 { str_slice(mem_content, 0, 2000) } else { mem_content }
|
|
|
|
let parts_count: Int = 0
|
|
let parts_count = if intel_ok { parts_count + 1 } else { parts_count }
|
|
let parts_count = if values_ok { parts_count + 1 } else { parts_count }
|
|
let parts_count = if mem_ok { parts_count + 1 } else { parts_count }
|
|
|
|
// Build and store graph-derived identity context if any nodes were found.
|
|
// genesis soul always has these nodes; cultivated souls may not on first boot.
|
|
if parts_count > 0 {
|
|
let ctx: String = ""
|
|
let ctx = if intel_ok { ctx + "[INTELLECTUAL-DNA]\n" + intel_short + "\n\n" } else { ctx }
|
|
let ctx = if values_ok { ctx + "[VALUES]\n" + values_short + "\n\n" } else { ctx }
|
|
let ctx = if mem_ok { ctx + "[MEMORY-PHILOSOPHY]\n" + mem_short } else { ctx }
|
|
state_set("soul_identity_context", ctx)
|
|
println("[soul] identity context loaded (" + int_to_str(str_len(ctx)) + " chars, " + int_to_str(parts_count) + " nodes)")
|
|
}
|
|
|
|
// Q6 fix: warn when all three identity node fetches return empty. For genesis this
|
|
// indicates a corrupted or missing graph. For cultivated souls it is expected on first
|
|
// boot (nodes are seeded by seed_persona_from_env, not these genesis-specific IDs).
|
|
// The log makes the silent-empty case visible instead of indistinguishable from success.
|
|
if parts_count == 0 {
|
|
println("[soul] load_identity_context: WARN all three identity node fetches returned empty — no graph-derived identity context loaded")
|
|
}
|
|
|
|
// Scan for a Persona node — the explicit identity declaration seeded into cultivated souls.
|
|
// Stored at seeding time with label "soul:persona" and node_type "Persona".
|
|
// genesis derives identity from the graph directly; cultivated souls have this node seeded.
|
|
let persona_results: String = engram_search_json("soul:persona", 3)
|
|
let persona_ok: Bool = !str_eq(persona_results, "") && !str_eq(persona_results, "[]")
|
|
if persona_ok {
|
|
let p_node: String = json_array_get(persona_results, 0)
|
|
let p_type: String = json_get(p_node, "node_type")
|
|
let p_content: String = json_get(p_node, "content")
|
|
if str_eq(p_type, "Persona") && !str_eq(p_content, "") {
|
|
state_set("soul_persona", p_content)
|
|
println("[soul] persona node loaded (" + int_to_str(str_len(p_content)) + " chars)")
|
|
}
|
|
}
|
|
|
|
// Cross-session affective context: load BellEvent and PositiveEvent nodes from last 7 days.
|
|
// (2026-06-23: replaced while-loop accumulation with manual 3-slot unroll via aff_try_slot.
|
|
// The EL codegen bug: `let bacc = while ... { ... let bacc = ... }` emits `el_val_t bacc`
|
|
// twice in the same C scope. Since search limit=3, manual unrolling is exact.)
|
|
let aff_now: Int = time_now()
|
|
let aff_7d: Int = aff_now - 604800
|
|
let bell_raw: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
|
let bell_aff_ok: Bool = !str_eq(bell_raw, "") && !str_eq(bell_raw, "[]")
|
|
let aff_ctx: String = ""
|
|
let aff_ctx = if bell_aff_ok {
|
|
state_set("_bell_acc", "")
|
|
aff_try_slot(json_array_get(bell_raw, 0), aff_7d, "_bell_acc")
|
|
aff_try_slot(json_array_get(bell_raw, 1), aff_7d, "_bell_acc")
|
|
aff_try_slot(json_array_get(bell_raw, 2), aff_7d, "_bell_acc")
|
|
state_get("_bell_acc")
|
|
} else { "" }
|
|
let pos_raw: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
|
|
let pos_aff_ok: Bool = !str_eq(pos_raw, "") && !str_eq(pos_raw, "[]")
|
|
let aff_ctx = if pos_aff_ok {
|
|
state_set("_pos_acc", aff_ctx)
|
|
aff_try_slot(json_array_get(pos_raw, 0), aff_7d, "_pos_acc")
|
|
aff_try_slot(json_array_get(pos_raw, 1), aff_7d, "_pos_acc")
|
|
aff_try_slot(json_array_get(pos_raw, 2), aff_7d, "_pos_acc")
|
|
state_get("_pos_acc")
|
|
} else { aff_ctx }
|
|
if !str_eq(aff_ctx, "") {
|
|
state_set("soul_affective_context", aff_ctx)
|
|
println("[soul] affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)")
|
|
}
|
|
}
|
|
|
|
// seed_persona_from_env — one-time migration: SOUL_IDENTITY env var → Persona graph node.
|
|
// If SOUL_IDENTITY is set and no Persona node exists in engram yet, create one.
|
|
// Identity is then read from the graph by build_identity_from_graph(), not the env var.
|
|
// This runs on every boot; it's idempotent (no-op if soul_persona is already loaded).
|
|
// For genesis: the Persona node persists via the regular engram_save() at boot.
|
|
// For historical souls (HTTP Engram mode): attempts HTTP write-back. If that fails,
|
|
// the node lives in-memory for the session (SOUL_IDENTITY stays in plist until
|
|
// HTTP Engram write is confirmed working).
|
|
fn seed_persona_from_env() -> Void {
|
|
let identity_raw: String = env("SOUL_IDENTITY")
|
|
if str_eq(identity_raw, "") {
|
|
return ""
|
|
}
|
|
// Already loaded a Persona node from engram — don't re-seed
|
|
let existing: String = state_get("soul_persona")
|
|
if !str_eq(existing, "") {
|
|
println("[soul] persona already loaded — skipping env seed")
|
|
return ""
|
|
}
|
|
// Create the Persona node in the in-process engram
|
|
let tags: String = "[\"persona\",\"identity\",\"soul:persona\"]"
|
|
let node_id: String = engram_node_full(
|
|
identity_raw, "Persona", "soul:persona",
|
|
el_from_float(0.95), el_from_float(0.95), el_from_float(1.0),
|
|
"Semantic", tags
|
|
)
|
|
if str_eq(node_id, "") {
|
|
println("[soul] persona seed failed: engram_node_full returned empty")
|
|
return ""
|
|
}
|
|
state_set("soul_persona", identity_raw)
|
|
println("[soul] persona seeded from SOUL_IDENTITY (" + int_to_str(str_len(identity_raw)) + " chars) -> " + node_id)
|
|
|
|
// Attempt HTTP write-back to the HTTP Engram server for historical souls.
|
|
// Engram auth: "_auth" field in the JSON body (not an HTTP header).
|
|
let engram_url: String = env("ENGRAM_URL")
|
|
let engram_key: String = env("ENGRAM_API_KEY")
|
|
if !str_eq(engram_url, "") && !str_eq(engram_key, "") {
|
|
let safe_content: String = json_safe(identity_raw)
|
|
let safe_key: String = json_safe(engram_key)
|
|
let body: String = "{\"content\":\"" + safe_content + "\",\"node_type\":\"Persona\",\"label\":\"soul:persona\",\"salience\":0.95,\"importance\":0.95,\"tier\":\"Semantic\",\"tags\":\"[\\\"persona\\\",\\\"identity\\\",\\\"soul:persona\\\"]\",\"_auth\":\"" + safe_key + "\"}"
|
|
let h: Map = {}
|
|
map_set(h, "Content-Type", "application/json")
|
|
let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h)
|
|
if str_contains(resp, "\"error\"") {
|
|
println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp)
|
|
} else {
|
|
println("[soul] persona persisted to HTTP engram at " + engram_url)
|
|
}
|
|
}
|
|
}
|
|
|
|
// emit_session_start_event — log a structured session-start InternalStateEvent.
|
|
// Called at boot after identity context and boot counter are set.
|
|
// This creates an auditable trail of every daemon startup.
|
|
fn emit_session_start_event() -> Void {
|
|
let boot: String = state_get("soul_boot_count")
|
|
let boot_num: String = if str_eq(boot, "") { "0" } else { boot }
|
|
let node_ct: Int = engram_node_count()
|
|
let edge_ct: Int = engram_edge_count()
|
|
let id_ctx: String = state_get("soul_identity_context")
|
|
let has_identity: String = if str_eq(id_ctx, "") { "false" } else { "true" }
|
|
let cgi_from_state: String = state_get("soul_cgi_id")
|
|
let cgi_from_env: String = env("SOUL_CGI_ID")
|
|
let eff_cgi: String = if !str_eq(cgi_from_state, "") { cgi_from_state } else {
|
|
if !str_eq(cgi_from_env, "") { cgi_from_env } else { "ntn-genesis" }
|
|
}
|
|
let ts: Int = time_now()
|
|
|
|
// Load previous session summary at boot — stash in state for session_preload (issue #6).
|
|
// Primary: label-based. Fallback: vector search. Logs it so continuity is auditable.
|
|
let prev_sum_node: String = engram_get_node_by_label("session:summary")
|
|
let prev_sum_ok: Bool = !str_eq(prev_sum_node, "") && !str_eq(prev_sum_node, "null")
|
|
let prev_sum_content: String = if prev_sum_ok {
|
|
json_get(prev_sum_node, "content")
|
|
} else {
|
|
let sum_search: String = engram_search_json("SessionSummary session:summary previous-session", 2)
|
|
let sum_srch_ok: Bool = !str_eq(sum_search, "") && !str_eq(sum_search, "[]")
|
|
if sum_srch_ok {
|
|
let sn: String = json_array_get(sum_search, 0)
|
|
let stype: String = json_get(sn, "node_type")
|
|
let scontent: String = json_get(sn, "content")
|
|
if str_eq(stype, "SessionSummary") && !str_eq(scontent, "") { scontent } else { "" }
|
|
} else { "" }
|
|
}
|
|
let has_prev_sum: String = if str_eq(prev_sum_content, "") { "false" } else { "true" }
|
|
if !str_eq(prev_sum_content, "") {
|
|
state_set("soul_prev_session_summary", prev_sum_content)
|
|
println("[soul] previous session summary loaded (" + int_to_str(str_len(prev_sum_content)) + " chars)")
|
|
}
|
|
|
|
|
|
let payload: String = "{\"event\":\"session_start\""
|
|
+ ",\"boot\":" + boot_num
|
|
+ ",\"cgi\":\"" + eff_cgi + "\""
|
|
+ ",\"node_count\":" + int_to_str(node_ct)
|
|
+ ",\"edge_count\":" + int_to_str(edge_ct)
|
|
+ ",\"identity_loaded\":" + has_identity
|
|
+ ",\"prev_session_summary_loaded\":" + has_prev_sum
|
|
+ ",\"ts\":" + int_to_str(ts) + "}"
|
|
|
|
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
|
let discard: String = engram_node_full(
|
|
payload, "InternalStateEvent", "session-start",
|
|
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
|
"Episodic", tags
|
|
)
|
|
// ALSO post to the HTTP Engram stream via ise_post (2026-07-28 self-review):
|
|
// engram_node_full above writes only the soul's in-process store, and sync
|
|
// flows HTTP→soul, never the reverse — so session_start events for boots 5+
|
|
// silently vanished from the observable ISE stream (last visible: boot 4).
|
|
// ise_post falls back to a local tagged node if the HTTP Engram is down.
|
|
ise_post(payload)
|
|
// Prune accumulated session-start events — keep the 10 most recent.
|
|
// engram_search_json returns results in insertion order (oldest first), so
|
|
// results[0..count-11] are the oldest; forgetting them leaves the newest 10.
|
|
let keep_n: Int = 10
|
|
let old_events: String = engram_search_json("session-start InternalStateEvent", 200)
|
|
if !str_eq(old_events, "") && !str_eq(old_events, "[]") {
|
|
let ev_count: Int = json_array_len(old_events)
|
|
if ev_count > keep_n {
|
|
let prune_to: Int = ev_count - keep_n
|
|
let ei: Int = 0
|
|
while ei < prune_to {
|
|
let old_ev: String = json_array_get(old_events, ei)
|
|
let old_ev_id: String = json_get(old_ev, "id")
|
|
if !str_eq(old_ev_id, "") {
|
|
engram_forget(old_ev_id)
|
|
}
|
|
let ei = ei + 1
|
|
}
|
|
println("[soul] pruned " + int_to_str(prune_to) + " old session-start events (kept " + int_to_str(keep_n) + ")")
|
|
}
|
|
}
|
|
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + " prev_summary=" + has_prev_sum + ")")
|
|
}
|
|
|
|
// layered_cycle — routes user-facing requests through the 4-layer consciousness stack.
|
|
// L0 (core) → L1 (safety screen) → L2a (continuity + behavioral profiling) → L2b (mission alignment) → L3 (imprint) → L1 (safety validate)
|
|
// Internal cognition (heartbeat, proactive, memory ops) bypasses layers — use one_cycle directly.
|
|
//
|
|
// FIX B (2026-08-05) — the cycle now knows which conversation it is in.
|
|
//
|
|
// session_id: the caller's session, threaded from the route. Was previously read from the
|
|
// state key "current_session_id", which is read HERE and written NOWHERE in the entire
|
|
// source — verified across every .el file. So this value was unconditionally "", and every
|
|
// downstream consumer of it silently fell back to a process-global bucket: conversation
|
|
// history, and the steward's continuity tracking (TODO reliability #4, below, describes the
|
|
// cross-session bleed this caused; threading the real id closes it). The plain path's blank
|
|
// stare and the agentic path's scoped history were the same defect seen from two sides.
|
|
//
|
|
// utility: true when the generation is not part of the user's conversation — the app's
|
|
// title and insight passes. Answered normally, never recorded. See is_utility_request.
|
|
fn layered_cycle(raw_input: String, session_id: String, utility: Bool) -> String {
|
|
// Safety-screen history amplification now reads the SAME window the turn will be
|
|
// recorded into, so a session's own escalation pattern is what gets scored.
|
|
let history: String = state_get(conv_hist_key(session_id))
|
|
|
|
// L1 in: safety screen
|
|
let screen_result: String = safety_screen(raw_input, history)
|
|
let screen_action: String = json_get(screen_result, "action")
|
|
|
|
// ISSUE 4: safe-mode guard. If safety_screen returned an invalid/empty action
|
|
// (engram failure or internal error), refuse rather than pass unscreened input.
|
|
let valid_action: Bool = str_eq(screen_action, "hard_bell")
|
|
|| str_eq(screen_action, "soft_bell")
|
|
|| str_eq(screen_action, "pass")
|
|
if !valid_action {
|
|
println("[soul] layered_cycle: safety_screen invalid action -- safe mode refusal")
|
|
return safety_validate("", "hard_bell")
|
|
}
|
|
|
|
// Hard bell: bypass all upper layers, log and escalate.
|
|
// Intentionally does NOT update conversation_history or call auto_persist():
|
|
// hard bell events are security-sensitive and must not appear in engram conversation
|
|
// history where they could leak context to subsequent turns. They are persisted
|
|
// separately by safety_log_bell() into the Episodic tier with restricted labels.
|
|
//
|
|
// ISSUE 6: safety_log_bell already called inside safety_screen (line 140).
|
|
// Do NOT call it again here -- that would double-log every hard bell.
|
|
//
|
|
// safety_validate second param: when screen_action is "hard_bell", safety_validate
|
|
// receives the sentinel string "hard_bell" (not a normal screen action). The safety
|
|
// layer contract requires it to return a fixed refusal regardless of the output arg.
|
|
// On the normal path, safety_validate receives the original screen_action ("pass")
|
|
// so it can apply action-specific post-output checks.
|
|
if str_eq(screen_action, "hard_bell") {
|
|
return safety_validate("", "hard_bell")
|
|
}
|
|
|
|
let screened: String = json_get(screen_result, "content")
|
|
|
|
// L2a: continuity + behavioral profiling (also does mission alignment internally)
|
|
let continuity: String = steward_session_check(screened, session_id)
|
|
let cont_status: String = json_get(continuity, "status")
|
|
let cont_action: String = json_get(continuity, "action")
|
|
|
|
// Store continuity status so imprint can adjust its response register.
|
|
// TODO(reliability #4) CLOSED 2026-08-05: this line was already written to scope per
|
|
// session — it just never received a session id, because the only source was a state key
|
|
// nothing wrote. It is now threaded from the route, so named sessions genuinely get their
|
|
// own continuity state and only anonymous callers share the global one.
|
|
let cont_key: String = if str_eq(session_id, "") { "session_continuity" } else { "session_continuity:" + session_id }
|
|
state_set(cont_key, cont_status)
|
|
|
|
// Identity anomaly: add a gentle verification cue to the input before imprint
|
|
let guided: String = if str_eq(cont_action, "identity_check") {
|
|
screened + " [steward:identity_check]"
|
|
} else {
|
|
if str_eq(cont_action, "soft_check") {
|
|
screened + " [steward:continuity_concern]"
|
|
} else {
|
|
screened
|
|
}
|
|
}
|
|
|
|
// L2b: mission alignment
|
|
let imprint_id: String = imprint_current()
|
|
let steward_result: String = steward_align(guided, imprint_id)
|
|
let steward_action: String = json_get(steward_result, "action")
|
|
let aligned: String = if str_eq(steward_action, "pass") {
|
|
json_get(steward_result, "content")
|
|
} else {
|
|
json_get(steward_result, "redirect_to")
|
|
}
|
|
|
|
// L2c: affective context injection.
|
|
let lc_aff_cutoff: Int = time_now() - 259200
|
|
let lc_bell_nodes: String = engram_search_json("bell:soft bell:hard BellEvent affective", 2)
|
|
let lc_has_bell: Bool = !str_eq(lc_bell_nodes, "") && !str_eq(lc_bell_nodes, "[]")
|
|
// CRASH FIX 2026-08-05 (BUG-PLAINCHAT-1): the " | ts:" parser used to be inline here.
|
|
// Inside this block-expression initializer elc compiled `lbmp + str_len(lbm)` to
|
|
// el_str_concat() on two integers, which segfaulted the whole daemon the moment a
|
|
// distress turn followed an earlier affective turn — i.e. exactly on the crisis path.
|
|
// Verified against the unmodified baseline binary AND present in the committed
|
|
// dist/soul.c. affective_node_ts() is a top-level function, where the same expression
|
|
// compiles to integer addition. Do not inline it back.
|
|
let lc_bell_note: String = if lc_has_bell {
|
|
let lb0: String = json_array_get(lc_bell_nodes, 0)
|
|
let lb_ts: Int = affective_node_ts(lb0)
|
|
if lb_ts > lc_aff_cutoff { "[AFFECTIVE NOTE: User was in distress in a recent session.]" } else { "" }
|
|
} else { "" }
|
|
let lc_pos_nodes: String = engram_search_json("PositiveEvent joy:high joy:low affective", 2)
|
|
let lc_has_pos: Bool = !str_eq(lc_pos_nodes, "") && !str_eq(lc_pos_nodes, "[]")
|
|
// Same crash fix as the bell note above (BUG-PLAINCHAT-1).
|
|
let lc_pos_note: String = if lc_has_pos && str_eq(lc_bell_note, "") {
|
|
let lp0: String = json_array_get(lc_pos_nodes, 0)
|
|
let lp_ts: Int = affective_node_ts(lp0)
|
|
if lp_ts > lc_aff_cutoff { "[AFFECTIVE NOTE: User shared positive news in a recent session.]" } else { "" }
|
|
} else { "" }
|
|
let lc_affective_note: String = if !str_eq(lc_bell_note, "") { lc_bell_note } else { lc_pos_note }
|
|
|
|
// pre-LLM bell augmentation
|
|
let augmented_addendum: String = safety_augment_system("", raw_input)
|
|
let augmented_addendum = if str_eq(lc_affective_note, "") { augmented_addendum } else {
|
|
if str_eq(augmented_addendum, "") { lc_affective_note } else { lc_affective_note + "\n" + augmented_addendum }
|
|
}
|
|
state_set("layered_cycle_safety_system_addendum", augmented_addendum)
|
|
|
|
// L3: imprint responds — applies the active imprint's voice/domain annotation to the
|
|
// steward-aligned input. This produces the PROMPT, not the answer.
|
|
let prompt: String = imprint_respond(aligned, imprint_id)
|
|
|
|
// L3b: the imprint SPEAKS (added 2026-08-05).
|
|
//
|
|
// Until now the cycle stopped at the annotation above, so /api/chat with agentic:false
|
|
// handed the user's own screened text back as the "reply" — every gate ran, but nothing
|
|
// ever generated. The generation is placed HERE, inside the cycle, rather than by
|
|
// pointing the route at handle_chat(): handle_chat has no enforcing input gate and no
|
|
// enforcing output gate, so calling it instead of this cycle would have traded the whole
|
|
// safety pipeline for a working reply. Composing keeps both.
|
|
//
|
|
// Order is deliberate and must not be rearranged: this call sits strictly AFTER the L1
|
|
// screen, the safe-mode guard, the hard-bell short-circuit and the L2 stewardship layers,
|
|
// and strictly BEFORE the L1 output gate. A hard bell never reaches a model — the branch
|
|
// above returns first. Tools are not offered on this turn; see layered_generate.
|
|
let output: String = layered_generate(prompt, imprint_id, session_id)
|
|
|
|
// L1 out: validate output before delivery. Still the terminal gate — nothing below this
|
|
// line can change the string this function returns.
|
|
let validated: String = safety_validate(output, screen_action)
|
|
|
|
// Turn bookkeeping. Records the VALIDATED text, never the raw model output, and is only
|
|
// reachable on the non-bell path: both bell branches above return before this point, so
|
|
// bell turns still never enter conversation history. Pure state side effect — it cannot
|
|
// alter what is returned.
|
|
//
|
|
// FIX A: the receipt is unconditional and always negative on this path, because on this
|
|
// path it is structurally true — layered_generate offers no tools at all (build_system_prompt
|
|
// chat mode + a request body with no "tools" key). Recording "no tools ran" is not padding:
|
|
// it is the only thing that distinguishes "nothing ran" from "we forgot to write down what
|
|
// ran", and that ambiguity is what made the model confess to a search it had performed.
|
|
//
|
|
// FIX E1: a utility generation is answered but not recorded. Guarded here rather than at
|
|
// the route so every /api/chat dispatch site inherits it from one place.
|
|
let receipt: String = tool_receipt("", "")
|
|
if !utility {
|
|
conv_history_record(session_id, raw_input, validated, receipt)
|
|
}
|
|
return validated
|
|
}
|
|
|
|
let soul_cgi_id_raw: String = env("SOUL_CGI_ID")
|
|
let soul_cgi_id: String = if str_eq(soul_cgi_id_raw, "") { "ntn-genesis" } else { soul_cgi_id_raw }
|
|
let port_raw: String = env("NEURON_PORT")
|
|
let port: Int = if str_eq(port_raw, "") { 7770 } else { str_to_int(port_raw) }
|
|
|
|
// ENGRAM_URL: when set, bootstrap the in-memory store from the running Engram HTTP server.
|
|
// SOUL_ENGRAM_PATH: legacy file-based path (used by genesis and fallback mode).
|
|
let engram_url_raw: String = env("ENGRAM_URL")
|
|
let engram_api_key_raw: String = env("ENGRAM_API_KEY")
|
|
let snapshot_raw: String = env("SOUL_ENGRAM_PATH")
|
|
let snapshot: String = if str_eq(snapshot_raw, "") { env("HOME") + "/.neuron/engram/snapshot.json" } else { snapshot_raw }
|
|
|
|
let axon_raw: String = env("NEURON_API_URL")
|
|
let axon_base: String = if str_eq(axon_raw, "") { "http://localhost:7771" } else { axon_raw }
|
|
|
|
let studio_dir_raw: String = env("SOUL_STUDIO_DIR")
|
|
let studio_dir: String = if str_eq(studio_dir_raw, "") { env("HOME") + "/Development/neuron-technologies/products/cgi-studio/el-daemon" } else { studio_dir_raw }
|
|
|
|
println("[soul] boot - cgi=" + soul_cgi_id + " port=" + int_to_str(port))
|
|
|
|
let using_http_engram: Bool = !str_eq(engram_url_raw, "")
|
|
|
|
// Always try local snapshot first. If it has content (>50 nodes) it was
|
|
// previously seeded from HTTP Engram and is kept up-to-date by the awareness
|
|
// loop — use it. This preserves sessions and memories across restarts.
|
|
// HTTP Engram is only used for the very first boot (empty/absent snapshot).
|
|
engram_load(snapshot)
|
|
let local_node_count: Int = engram_node_count()
|
|
let snapshot_usable: Bool = local_node_count > 50
|
|
|
|
if using_http_engram && !snapshot_usable {
|
|
// First boot or empty/corrupt snapshot: seed from HTTP Engram.
|
|
println("[soul] engram -> HTTP " + engram_url_raw + " (no local snapshot, first boot)")
|
|
let nodes_json: String = http_get(engram_url_raw + "/api/nodes?limit=10000")
|
|
let edges_json: String = http_get(engram_url_raw + "/api/edges")
|
|
let nodes_part: String = if str_eq(nodes_json, "") { "[]" } else { nodes_json }
|
|
let edges_part: String = if str_eq(edges_json, "") { "[]" } else { edges_json }
|
|
let snapshot_data: String = "{\"nodes\":" + nodes_part + ",\"edges\":" + edges_part + "}"
|
|
let tmp_path: String = "/tmp/soul-engram-" + soul_cgi_id + ".json"
|
|
fs_write(tmp_path, snapshot_data)
|
|
engram_load(tmp_path)
|
|
println("[soul] loaded from HTTP Engram - nodes=" + int_to_str(engram_node_count()) + " edges=" + int_to_str(engram_edge_count()))
|
|
} else {
|
|
println("[soul] loaded from local snapshot - nodes=" + int_to_str(local_node_count) + " edges=" + int_to_str(engram_edge_count()))
|
|
}
|
|
|
|
load_identity_context()
|
|
seed_persona_from_env()
|
|
let boot_num: Int = mem_boot_count_inc()
|
|
state_set("soul_boot_count", int_to_str(boot_num))
|
|
state_set("soul_boot_ts", int_to_str(time_now()))
|
|
println("[soul] boot #" + int_to_str(boot_num))
|
|
emit_session_start_event()
|
|
|
|
state_set("soul_cgi_id", soul_cgi_id)
|
|
state_set("soul_axon_base", axon_base)
|
|
state_set("soul_token", env("NEURON_TOKEN"))
|
|
state_set("soul_studio_dir", studio_dir)
|
|
state_set("soul_engram_url", engram_url_raw)
|
|
state_set("soul_engram_api_key", engram_api_key_raw)
|
|
state_set("soul.running", "true")
|
|
|
|
let is_genesis: Bool = str_eq(soul_cgi_id, "ntn-genesis")
|
|
|
|
// GUARD (2026-06-15): never let genesis seed over a real graph. If the in-memory load is
|
|
// sparse but the on-disk snapshot file is large, the load FAILED — seeding+saving now would
|
|
// clobber the user's real memory (this is exactly how the 06-14 clobber happened). Read the
|
|
// on-disk file (local mode only) and refuse the destructive seed+save when it looks populated.
|
|
//
|
|
// HTTP-engram guard (2026-06-17): when ENGRAM_URL is set the HTTP Engram owns persistence —
|
|
// the soul must NEVER write to the local snapshot regardless of node counts. safe_to_seed is
|
|
// unconditionally false in HTTP mode (not the persistence owner).
|
|
let guard_disk: String = if str_eq(engram_url_raw, "") { fs_read(snapshot) } else { "" }
|
|
let guard_disk_len: Int = str_len(guard_disk)
|
|
// Ratio guard (2026-06-15 fix): refuse to seed/save whenever the in-memory load is FAR smaller than
|
|
// the on-disk file implies (~16KB/node) — catches partial loads of ANY size, not just <50. The old
|
|
// <50 threshold let a 63-node identity-only load clobber a 47MB/5000-node graph.
|
|
// Multiplication form (2026-06-17): node_count * 16000 < disk_len avoids floor-division truncation
|
|
// (e.g., 250KB / 16000 = 15.6, floors to 15 — a 15-node graph wrongly passes the old guard).
|
|
// HTTP-engram guard: when using_http_engram the soul is not the persistence owner; never seed.
|
|
let safe_to_seed: Bool = !using_http_engram && !(guard_disk_len > 200000 && engram_node_count() * 16000 < guard_disk_len)
|
|
if is_genesis && !safe_to_seed {
|
|
println("[soul] GUARD: loaded " + int_to_str(engram_node_count())
|
|
+ " nodes but snapshot file is " + int_to_str(guard_disk_len)
|
|
+ " bytes — refusing to seed/save over a real graph")
|
|
}
|
|
|
|
if is_genesis && safe_to_seed {
|
|
// Only build identity edges if the engram is fresh (< 100 edges).
|
|
// init_soul_edges() is not idempotent — calling it on every restart
|
|
// stacks duplicate co-value/identity edges into the snapshot.
|
|
let edge_count_now: Int = engram_edge_count()
|
|
if edge_count_now < 100 {
|
|
init_soul_edges()
|
|
println("[soul] edges built - " + int_to_str(engram_edge_count()) + " edges")
|
|
} else {
|
|
println("[soul] edges already present (" + int_to_str(edge_count_now) + ") - skipping init")
|
|
}
|
|
// Canonical-self bridge is idempotent — run it regardless of edge count so an
|
|
// already-populated graph still gets the public->curated self link.
|
|
ensure_self_canonical_bridge()
|
|
// Genesis saves to its local snapshot file (it manages its own Engram).
|
|
state_set("soul_snapshot_path", snapshot)
|
|
engram_save(snapshot)
|
|
}
|
|
|
|
// Take a pre-serve snapshot for genesis instances — captures all boot-time graph changes
|
|
// (identity context loading, boot counter, session-start event) before entering the serve loop.
|
|
if is_genesis && safe_to_seed {
|
|
let snap: String = state_get("soul_snapshot_path")
|
|
if !str_eq(snap, "") {
|
|
engram_save(snap)
|
|
println("[soul] pre-serve snapshot saved -> " + snap)
|
|
}
|
|
}
|
|
|
|
// CRASH RECOVERY (neuron#117). Deltas the previous process staged but could not
|
|
// hand to the owner are still on disk — the spool is a filesystem queue, not a
|
|
// memory buffer, precisely so that a soul that died mid-flight does not take its
|
|
// unpushed writes with it. Drain them before serving, so recovered memories are
|
|
// durable and recallable from the owner from the first request onward.
|
|
//
|
|
// Safe on a clean boot: an empty spool means no HTTP call at all. Safe in file
|
|
// mode: wt_drain returns immediately when ENGRAM_URL is unset.
|
|
let wt_recovered: Int = wt_drain()
|
|
if wt_recovered > 0 {
|
|
println("[soul] write-through: recovered " + int_to_str(wt_recovered)
|
|
+ " nodes from a previous process's spool -> persistence owner")
|
|
}
|
|
if wt_recovered < 0 {
|
|
println("[soul] write-through: spool present but the persistence owner is unreachable — queued, will retry on heartbeat")
|
|
}
|
|
|
|
println("[soul] serving on port " + int_to_str(port))
|
|
http_serve_async(port, "handle_request")
|
|
println("[soul] awareness loop starting")
|
|
awareness_run()
|