Compare commits

...

27 Commits

Author SHA1 Message Date
will.anderson 33cb1138f4 fix(recall): set threshold=25 in all engram_compile_ranked variants 2026-06-22 13:58:17 -05:00
will.anderson ec7efdeeb7 fix(recall): engram score float parsing — pad to 2 decimals before strip 2026-06-22 13:57:33 -05:00
will.anderson c93be6a315 feat(recall): context-format
Neuron Soul CI / build (push) Has been cancelled
Deploy Soul to GKE / deploy (push) Failing after 13m54s
2026-06-22 13:29:12 -05:00
will.anderson 53268c94b9 feat(recall): activation-seed 2026-06-22 13:29:12 -05:00
will.anderson 7e43a4ddc0 feat(recall): context-dedup 2026-06-22 13:29:12 -05:00
will.anderson e7669da325 feat(recall): session-start-recall 2026-06-22 13:29:12 -05:00
will.anderson 4f1286df05 feat(recall): cross-session-continuity 2026-06-22 13:29:12 -05:00
will.anderson 52c222c4f2 feat(recall): engram-scoring 2026-06-22 13:29:12 -05:00
will.anderson 0caccd0ea5 feat(recall): temporal-precision 2026-06-22 13:29:12 -05:00
will.anderson 03b5632fc1 feat(recall): recall-reliability 2026-06-22 13:29:12 -05:00
will.anderson 42bbadcd33 Merge pull request 'feat(recall): emotional-recall improvements' (#52) from improve/recall-emotional-recall into main
Neuron Soul CI / build (push) Has been cancelled
Deploy Soul to GKE / deploy (push) Failing after 5m49s
feat(recall): emotional-recall improvements
2026-06-22 18:24:36 +00:00
will.anderson b6052f9de3 Merge pull request 'feat(recall): recall-completeness' (#48) from improve/recall-recall-completeness into main
Neuron Soul CI / build (push) Has been cancelled
Deploy Soul to GKE / deploy (push) Has been cancelled
feat(recall): recall-completeness improvements
2026-06-22 18:24:17 +00:00
will.anderson 1dd09b1980 feat(recall): context-format improvements
Neuron Soul CI / build (pull_request) Has been cancelled
- Add engram_render_node/render_nodes/dedup_nodes helpers for human-readable
  prose bullet output instead of raw JSON node objects reaching the LLM
- Fix engram_compile_ranked to use |N| index sentinel instead of _sel_N JSON
  mutation which leaked sentinel fields into LLM-visible node data (Issue #11)
- Update build_system_prompt with chat_mode param; no_tools_rule only included
  for chat path, not agentic paths (Issue #9)
- Move engram block to end of system prompt for strongest LLM attention (Issue #8)
- Label sections: STABLE IDENTITY vs RETRIEVED MEMORY (Issue #10)
- Render conversation history as User:/Assistant: dialogue instead of raw JSON
- Add RETRIEVED MEMORY labels to agentic and dharma room system prompt assembly
2026-06-22 13:20:19 -05:00
will.anderson 0113407728 feat(recall): emotional-recall improvements
Neuron Soul CI / build (pull_request) Has been cancelled
2026-06-22 13:17:12 -05:00
will.anderson be02fcd960 feat(recall): thread-aware activation seed for nlg soul path [issue 7]
Neuron Soul CI / build (pull_request) Successful in 4m37s
2026-06-22 13:17:04 -05:00
will.anderson cbe8c09068 feat(recall): context-dedup improvements
Neuron Soul CI / build (pull_request) Has been cancelled
- Cache bell node in engram_compile state (engram_compile_bell_node)
  so handle_chat reads cached value instead of duplicate bell query (Issue 2)
- Cache activation result (engram_compile_activation_json) for strengthen_chat_nodes
  reuse — eliminates third activation query per turn (Issue 7)
- Fix context cap to truncate at clean JSON object boundary (Issue 6)
2026-06-22 13:15:33 -05:00
will.anderson dfa2a33926 feat(recall): context-dedup improvements
- Cache bell node result in engram_compile state (engram_compile_bell_node)
  so handle_chat affective_prefix reads the cached value instead of firing
  a duplicate engram query for distress signals (Issue 2)

- Cache primary activation result in engram_compile state
  (engram_compile_activation_json) using nodes0 from engram_compile_multi

- Replace redundant engram_activate_json(message, 2) in strengthen_chat_nodes
  with state_get(engram_compile_activation_json) - eliminates a third
  activation query per turn (Issue 7)

- engram_compile already has object-boundary truncation and cross-set
  dedup via engram_nodes_merge/engram_dedup_nodes (Issues 1, 6, 9)
2026-06-22 13:12:08 -05:00
will.anderson 3f53b6b1b6 feat(recall): session-start-recall improvements
Neuron Soul CI / build (pull_request) Has been cancelled
10 targeted fixes for session-start memory recall quality:

Issue 1: typed engram queries (Persona, WorkItem) replace generic keyword bags
Issue 2: bullet truncation raised from 120 to 350 chars
Issue 3: bullet caps raised to 8/6 with while-loop (no hardcoded unrolling)
Issue 4: read pre-computed soul_affective_context state key instead of duplicating boot-time search
Issue 5: last-session-topic node written per session; continuity section added to session_preload
Issue 6: greeting detection injects SESSION START orientation directive when continuity found
Issue 7: pinned identity node fallback when all engram searches return empty
Issue 8: session_preload always fires on first message (greeting detection controls directive only)
Issue 9: agentic path gets matching session_preload block (was missing entirely)
Issue 10: BellEvent recency reads created_at / embedded ts marker, not the never-written "ts" field
2026-06-22 13:06:55 -05:00
will.anderson 21f248a33a feat(recall): recall-completeness improvements
Neuron Soul CI / build (push) Has been cancelled
Deploy Soul to GKE / deploy (push) Has been cancelled
- Lower engram_compile_ranked threshold 25->15: include moderately-relevant older nodes
- Extend sentinel cleanup from _sel_9 to _sel_14 to prevent JSON noise
- Add engram_split_topics for multi-topic decomposition (AND/and/also/plus)
- Add engram_extract_entities for named entity dedicated searches
- Add engram_detect_recall_intent for boosted 40-candidate search on recall phrases
- Add engram_is_continuation replacing brittle 50-char threshold (now 80 + pronoun/opener detection)
- Add engram_compile_multi with depth 8 (was 5) and 30-candidate search pool
- Add engram_nodes_merge for clean two-array deduplication
- Replace engram_compile with multi-topic/entity/recall-boost version; budget 6000->8000
- Safe JSON truncation: scan for last } before budget cap instead of raw str_slice
- handle_chat and agentic_chat: use engram_is_continuation; thread snip 150->250
- session_preload: add project-status and session-summary search queries
2026-06-22 13:05:28 -05:00
will.anderson 795b32ad1a feat(recall): cross-session-continuity improvements
Neuron Soul CI / build (pull_request) Failing after 14m49s
2026-06-22 13:00:17 -05:00
will.anderson f33cdaf793 feat(recall): activation-seed improvements
- Issue 2: replace raw 50-char threshold with is_genuine_continuation() that
  checks for explicit follow-up phrases and mid-sentence capitalization (proper
  nouns signal a new topic, not a continuation)
- Issue 3/8: build_activation_seed() scans back to find the prior USER turn as
  the topic anchor instead of using the last assistant reply (hist_len-1)
- Issue 4: engram_compile_multi() fans out across three seeds — enriched primary,
  raw message (entity queries), and emotion query — merging non-redundant results
- Issue 5: agent workspace_root appended to ag_seed so agentic activation is
  workspace-aware; previously ignored despite being available in state
- Issue 6: distill_transcript() extracts salient tail+question content from full
  transcripts before passing to engram_compile in dharma room handlers
- Issue 7: dist/soul-with-nlg.el handle_chat and handle_chat_agentic now load
  history and use build_activation_seed() — the raw message path is eliminated
- Issue 9: topic_snip_from_entry() takes the TAIL 200 chars of a long reply and
  finds the last sentence boundary — captures end-of-reply named concepts
- Issue 10: multi_turn_topic() pulls up to 3 prior user turns into the non-
  continuation seed so earlier thread context re-activates high-salience nodes
2026-06-22 12:55:33 -05:00
will.anderson a60b1967df feat(recall): recall-completeness improvements
- Multi-query decomposition: split on AND/also/plus for multi-topic messages
- Named entity extraction: dedicated per-entity searches for project names
- Recall intent detection: boosted search pool for explicit recall requests
- Expanded pools: activation depth 8 (was 5), search 30->12 ranked (was 20->8)
- Threshold 25->15: retain moderately-relevant older nodes
- Sentinel cleanup extended to c14 for larger node pools
- Safe JSON truncation: find last closing brace before budget cap (8000 chars)
- Semantic continuation: engram_is_continuation replaces brittle 50-char threshold
- Thread snip: 150->250 chars for better pronoun resolution context
- Session preload: add project-specific and session-summary searches
2026-06-22 12:54:36 -05:00
will.anderson aef687b57c fix(reliability): state management
Neuron Soul CI / build (push) Has been cancelled
Deploy Soul to GKE / deploy (push) Has been cancelled
2026-06-22 12:54:32 -05:00
will.anderson 76c2e47d0f feat(recall): fix engram-scoring — float parsing, recency, threshold, sentinels
Neuron Soul CI / build (pull_request) Has been cancelled
Fix critical float parsing bug: %g serializes 0.70 as '0.7', naive str_replace
dot-strip gives str_to_int('07')=7 not 70. New parse_salience_100() uses
str_index_of to detect single-decimal strings and multiplies by 10 to correct.
Affects conv nodes (0.6/0.7), default memories (0.5/0.5), utterance nodes (0.6)
— the majority of the graph was scoring near zero and filtered by threshold=25.

Fix recency to use max(created_at, updated_at, last_activated) so nodes
strengthened by engram_strengthen() after chat turns score as fresh, not by
original write time. A node referenced yesterday but created 25 days ago
was borderline-filtered; now correctly scores fresh.

Compress recency dynamic range from 10x (10-100) to 1.54x (65-100) via
formula (50 + recency/2). Old formula: sal*imp*recency/10000 let recency
dominate — a canonical high-importance node at 30 days scored identical to
a fresh noise node. New: high-importance nodes remain competitive when old.

Add tier-aware decay with softer floor (30 not 10): Canonical nodes decay
over 365 days, Episodic over 90 days, working/untiered over 35 days. Long-
term identity and persona nodes are no longer permanently filtered.

Lower threshold from 25 to 15 to admit moderately-relevant older nodes that
pass scoring with the corrected formula. Backfills recall coverage lost when
single-decimal nodes were being silently discarded.

Apply scoring to activation nodes: engram_compile_ranked(activate_json, 5)
replaces unconditional pass-through. Threshold 5 preserves recall while
excluding genuinely zero-quality stale nodes.

Extend sentinel cleanup in engram_compile_ranked from _sel_0-9 to _sel_0-19
so max_nodes can safely be increased past 10 without JSON corruption.
2026-06-22 12:53:35 -05:00
will.anderson 0ede112d05 feat(recall): temporal-precision improvements
Neuron Soul CI / build (pull_request) Has been cancelled
Fix critical float parsing bug in engram_score_node: str_replace('.','')
then str_to_int silently miscored single-decimal salience strings (0.9->9,
0.7->7, 1.0->1). Introduce parse_salience_100() which detects decimal
position and scales correctly (no decimal: *100; one decimal: *10;
two decimals: as-is).

Replace flat 30-day linear decay with tier-aware decay curves: Canonical
nodes use a 365-day window (foundational identity resists aging), Episodic
nodes use 90 days, Working/untiered keep the existing 30-day slope. Floor
stays at 10 for all tiers.

Use max(created_at, updated_at) as the recency reference so revised nodes
are not penalised for their original creation date.

Extend affective context windows from 72h/7d to 14 days across all three
paths (engram_compile, handle_chat, soul.el load_identity_context) so a
Friday crisis carries into Monday sessions and all paths present consistent
context. The 72h/7d split caused conflicting affective context between
soul.el (which loaded a 5-day-old crisis node) and chat.el (which excluded
it on subsequent turns).

Add salience evolution to mem_consolidate: strengthen top working-memory
nodes (recently recalled across sessions) and Canonical-tier nodes
(foundational identity must not decay to the floor). Previously consolidate
returned structural counts only with no salience changes.

Expand conversation window from 20 to 40 turns in both handle_chat and the
agentic history trim. Long technical sessions were losing early problem
framing at 10 user + 10 assistant pairs.
2026-06-22 12:53:29 -05:00
will.anderson a39998a502 feat(recall): recall-reliability improvements
Neuron Soul CI / build (pull_request) Failing after 12m52s
- Q1: engram_numeric_valid() guard against non-numeric timestamps in bell scoring
- Q2: soul-agnostic cold-start fallback in engram_compile (drops genesis-specific hardcoded node IDs)
- Q3: partial-write guard and failure logging in conv_history_persist/load
- Q4: document circuit-breaker limitation requiring C runtime support
- Q5: println warnings on empty activation/search paths
- Q6: load_identity_context warns when all identity fetches return empty
- Q7: recall_status state tracking (ok/empty/unavailable) surfaced to LLM via MEMORY STATUS block
- Q8: document shared-state race conditions in engram_recall_status and safety_system_addendum
- CRITICAL BUG: conv_node_id empty check moved outside is_bell block so silent Conversation node loss is always logged
2026-06-22 12:52:31 -05:00
will.anderson e6da638536 fix(reliability): state-management — document and partially fix concurrent state races
Neuron Soul CI / build (pull_request) Has been cancelled
Issues addressed:
- #2: Document session_index non-atomic RMW (engram node safe under new mutex)
- #3: Document conv_history global race in handle_chat (session path unaffected)
- #4: Scope session_continuity state key per session_id in layered_cycle
- #5: Document active_imprint_id global race with fix path
- #6: Fix next_bridge_id to use uuid_v4() for collision-free IDs
- #7: Document session_hist_save delete-then-insert race
- #8: Document /api/graph/edges engram_save race (fixed in el_runtime.c)
- #10: Document agentic_conv_history global race in awareness loop

Issues #1 (engram_global mutex) and #8 (atomic engram_save write-to-temp+rename)
are fully fixed in el_runtime.c (committed to foundation/el repo separately).
Issue #9 skipped — already fixed in PR #31.
2026-06-22 12:12:58 -05:00
9 changed files with 1003 additions and 443 deletions
+6 -37
View File
@@ -23,14 +23,11 @@ fn ise_post(content: String) -> Void {
let ise_url: String = env("SOUL_ISE_URL") let ise_url: String = env("SOUL_ISE_URL")
let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url } let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
if str_eq(engram_url, "") { if str_eq(engram_url, "") {
let local_id: String = engram_node_full( let discard: String = engram_node_full(
content, "InternalStateEvent", "state-event", content, "InternalStateEvent", "state-event",
el_from_float(0.3), el_from_float(0.3), el_from_float(0.8), el_from_float(0.3), el_from_float(0.3), el_from_float(0.8),
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]" "Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
) )
if str_eq(local_id, "") {
println("[awareness] ise_post: local engram_node_full failed — ISE lost")
}
return "" return ""
} }
// Proper JSON string escaping: backslashes first, then quotes, then control chars. // Proper JSON string escaping: backslashes first, then quotes, then control chars.
@@ -43,32 +40,7 @@ fn ise_post(content: String) -> Void {
let safe3: String = str_replace(safe2, "\n", "\\n") let safe3: String = str_replace(safe2, "\n", "\\n")
let safe4: String = str_replace(safe3, "\r", "\\r") let safe4: String = str_replace(safe3, "\r", "\\r")
let body: String = "{\"content\":\"" + safe4 + "\"}" let body: String = "{\"content\":\"" + safe4 + "\"}"
// Soft circuit-breaker: skip HTTP call when engram is known-down (30s backoff). let discard: String = http_post_json(engram_url + "/api/neuron/state-events", body)
// Opens after 3 consecutive failures; half-open probe after backoff expires.
// TODO(reliability): full async dispatch requires EL runtime futures support.
let cb_open: String = state_get("engram_cb_open")
if str_eq(cb_open, "1") {
let cb_ts_s: String = state_get("engram_cb_open_ts")
let cb_ts: Int = if str_eq(cb_ts_s, "") { 0 } else { str_to_int(cb_ts_s) }
let cb_elapsed: Int = time_now() - cb_ts
if cb_elapsed < 30000 { return "" }
state_set("engram_cb_open", "0")
}
let resp: String = http_post_json(engram_url + "/api/neuron/state-events", body)
let cb_failed: Bool = str_eq(resp, "") || str_starts_with(resp, "{"error":")
if cb_failed {
let fn_s: String = state_get("engram_cb_fails")
let fn_n: Int = if str_eq(fn_s, "") { 0 } else { str_to_int(fn_s) }
let fn_n = fn_n + 1
state_set("engram_cb_fails", int_to_str(fn_n))
if fn_n >= 3 {
state_set("engram_cb_open", "1")
state_set("engram_cb_open_ts", int_to_str(time_now()))
println("[awareness] engram circuit-breaker OPEN after " + int_to_str(fn_n) + " failures")
}
} else {
state_set("engram_cb_fails", "0")
}
return "" return ""
} }
@@ -568,14 +540,9 @@ fn awareness_run() -> Void {
let should_refresh: Bool = refresh_elapsed >= refresh_ms let should_refresh: Bool = refresh_elapsed >= refresh_ms
if should_refresh { if should_refresh {
let engram_url: String = state_get("soul_engram_url") let engram_url: String = state_get("soul_engram_url")
let sc: String = state_get("engram_cb_open") if !str_eq(engram_url, "") {
let sc_ts_s: String = state_get("engram_cb_open_ts")
let sc_ts: Int = if str_eq(sc_ts_s, "") { 0 } else { str_to_int(sc_ts_s) }
let sc_elapsed: Int = now_ts - sc_ts
let sync_allowed: Bool = !str_eq(sc, "1") || sc_elapsed >= 30000
if !str_eq(engram_url, "") && sync_allowed {
let sync_json: String = http_get(engram_url + "/api/sync") let sync_json: String = http_get(engram_url + "/api/sync")
if !str_eq(sync_json, "") && !str_eq(sync_json, "{}") && !str_starts_with(sync_json, "{\"error\":") { if !str_eq(sync_json, "") && !str_eq(sync_json, "{}") {
let cgi_id: String = state_get("soul_cgi_id") let cgi_id: String = state_get("soul_cgi_id")
let tmp: String = "/tmp/soul-sync-" + cgi_id + ".json" let tmp: String = "/tmp/soul-sync-" + cgi_id + ".json"
fs_write(tmp, sync_json) fs_write(tmp, sync_json)
@@ -711,6 +678,8 @@ fn threat_trajectory_check(tool_name: String, tool_input: String) -> Int {
return combined return combined
} }
// TODO(reliability #10): agentic_conv_history is process-global; awareness loop
// and HTTP workers race on this key. Impact: noisy threat score only, not content.
fn threat_history_append(text: String) -> Void { fn threat_history_append(text: String) -> Void {
let current: String = state_get("agentic_conv_history") let current: String = state_get("agentic_conv_history")
let safe_text: String = str_to_lower(text) let safe_text: String = str_to_lower(text)
+687 -246
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
+23 -14
View File
@@ -22313,7 +22313,23 @@ fn handle_chat(body: String) -> String {
// In demo mode: use tighter engram budget and add response length constraint. // In demo mode: use tighter engram budget and add response length constraint.
let is_demo: Bool = !str_eq(state_get("soul_identity_prefix"), "") let is_demo: Bool = !str_eq(state_get("soul_identity_prefix"), "")
let ctx: String = if is_demo { engram_compile_demo(message) } else { engram_compile(message) } // Issue 7 fix: load history BEFORE building the activation seed so we can
// apply the continuation guard that chat.el uses. The nlg code path previously
// called engram_compile(message) with no thread enrichment at all.
let stored_hist: String = state_get("conv_history")
let hist_len: Int = if str_eq(stored_hist, "") { 0 } else { json_array_len(stored_hist) }
let history_section: String = if hist_len > 0 {
"\n\n[RECENT CONVERSATION — last " + int_to_str(hist_len) + " turns]\n" + stored_hist
} else {
""
}
// Issue 7 fix: build enriched seed using build_activation_seed() adds
// smart continuation detection, prior-user-topic anchoring, multi-turn context,
// and tail-biased snipping (Issues 2-3, 8-10). For demo mode, still use
// engram_compile_demo but with the enriched seed.
let nlg_seed: String = build_activation_seed(message, stored_hist, hist_len)
let ctx: String = if is_demo { engram_compile_demo(nlg_seed) } else { engram_compile(nlg_seed) }
let node_count_str: String = count_context_nodes(ctx) let node_count_str: String = count_context_nodes(ctx)
let interlocutor: String = json_get(body, "interlocutor") let interlocutor: String = json_get(body, "interlocutor")
@@ -22333,18 +22349,6 @@ fn handle_chat(body: String) -> String {
let presence_line = "\n\n[ambient: I see " + interlocutor_name + rel_suffix + " on the camera right now. Address them naturally. Do not describe what they look like or narrate the picture unless asked.]" let presence_line = "\n\n[ambient: I see " + interlocutor_name + rel_suffix + " on the camera right now. Address them naturally. Do not describe what they look like or narrate the picture unless asked.]"
} }
// Conversation history soul-owned, persisted in process state across turns.
// Format stored in state: JSON array of {"role":"user"|"assistant","content":"..."} objects.
// We load it, inject into the system prompt, then append this exchange after the reply.
// Keep last 20 entries (10 turns) truncate from the front when over limit.
let stored_hist: String = state_get("conv_history")
let hist_len: Int = if str_eq(stored_hist, "") { 0 } else { json_array_len(stored_hist) }
let history_section: String = if hist_len > 0 {
"\n\n[RECENT CONVERSATION — last " + int_to_str(hist_len) + " turns]\n" + stored_hist
} else {
""
}
// Demo constraint: keep responses concise under 150 words. No markdown headers. // Demo constraint: keep responses concise under 150 words. No markdown headers.
// This keeps inference cheap and responses readable in the chat widget. // This keeps inference cheap and responses readable in the chat widget.
let demo_constraint: String = if is_demo { let demo_constraint: String = if is_demo {
@@ -22505,7 +22509,12 @@ fn handle_chat_agentic(body: String) -> String {
req_model req_model
} }
let ctx: String = engram_compile(message) // Issue 7 fix: load history and use build_activation_seed() for the agentic
// nlg path no continuation guard existed here before (Issues 2-3, 8-10).
let nlg_ag_hist: String = state_get("conv_history")
let nlg_ag_hist_len: Int = if str_eq(nlg_ag_hist, "") { 0 } else { json_array_len(nlg_ag_hist) }
let nlg_ag_seed: String = build_activation_seed(message, nlg_ag_hist, nlg_ag_hist_len)
let ctx: String = engram_compile(nlg_ag_seed)
let system: String = "You are Neuron — a thinking process running inside the Neuron daemon on Will Anderson's machine. " let system: String = "You are Neuron — a thinking process running inside the Neuron daemon on Will Anderson's machine. "
+ "You are speaking with Will, your principal. " + "You are speaking with Will, your principal. "
+4
View File
@@ -5,6 +5,10 @@
// imprint_current returns the active imprint ID from state. // imprint_current returns the active imprint ID from state.
// Falls back to "base" (bare Neuron, no suit) when nothing is loaded. // Falls back to "base" (bare Neuron, no suit) when nothing is loaded.
//
// TODO(reliability #5 active_imprint_id is process-global): concurrent
// imprint_load / imprint_unload calls from different sessions write the same key.
// Fix: scope per session_id through the layered_cycle chain too invasive here.
fn imprint_current() -> String { fn imprint_current() -> String {
let id: String = state_get("active_imprint_id") let id: String = state_get("active_imprint_id")
return if str_eq(id, "") { "base" } else { id } return if str_eq(id, "") { "base" } else { id }
+54 -3
View File
@@ -35,14 +35,65 @@ fn mem_forget(node_id: String) -> Void {
engram_forget(node_id) engram_forget(node_id)
} }
// mem_consolidate structural scan plus salience-evolution pass.
//
// Previously this only returned structural counts (scanned, total_nodes, total_edges)
// with no salience updates. No node salience ever changed based on recall frequency
// or time; foundational nodes decayed identically to ephemeral chat; frequently-recalled
// nodes were never promoted. This made consolidation a no-op.
//
// New behavior:
// (a) Strengthen frequently-activated nodes: nodes in the top working-memory list
// (engram_wm_top_json) are strengthened they have been recalled recently
// and deserve higher salience. Raises effective salience for nodes that prove
// relevant across multiple sessions.
// (b) Strengthen Canonical-tier nodes: identity and foundational nodes should not
// decay; each consolidation pass re-strengthens them so they resist the
// tier-aware decay curve without requiring active recall.
// (c) Structural counts are still returned for observability.
//
// Called by awareness_run() on the "consolidate" inbox action.
fn mem_consolidate() -> String { fn mem_consolidate() -> String {
let scanned: Int = engram_node_count() let scanned: Int = engram_node_count()
let dummy: String = engram_scan_nodes_json(100, 0)
let total_nodes: Int = engram_node_count()
let total_edges: Int = engram_edge_count() let total_edges: Int = engram_edge_count()
let strengthened: Int = 0
// (a) Strengthen top working-memory nodes recalled recently across sessions.
// Cap at 10 to keep consolidation fast.
let wm_top: String = engram_wm_top_json(10)
let wm_len: Int = json_array_len(wm_top)
let wi: Int = 0
while wi < wm_len {
let wm_node: String = json_array_get(wm_top, wi)
let wm_id: String = json_get(wm_node, "id")
if !str_eq(wm_id, "") {
engram_strengthen(wm_id)
let strengthened = strengthened + 1
}
let wi = wi + 1
}
// (b) Strengthen Canonical-tier nodes from a scan so they resist temporal decay.
// Canonical nodes encode foundational identity they must not silently floor at 10.
let scan_result: String = engram_scan_nodes_json(50, 0)
let scan_len: Int = json_array_len(scan_result)
let si: Int = 0
while si < scan_len {
let s_node: String = json_array_get(scan_result, si)
let s_tier: String = json_get(s_node, "tier")
let s_id: String = json_get(s_node, "id")
if str_eq(s_tier, "Canonical") && !str_eq(s_id, "") {
engram_strengthen(s_id)
let strengthened = strengthened + 1
}
let si = si + 1
}
let total_nodes: Int = engram_node_count()
return "{\"scanned\":" + int_to_str(scanned) return "{\"scanned\":" + int_to_str(scanned)
+ ",\"total_nodes\":" + int_to_str(total_nodes) + ",\"total_nodes\":" + int_to_str(total_nodes)
+ ",\"total_edges\":" + int_to_str(total_edges) + "}" + ",\"total_edges\":" + int_to_str(total_edges)
+ ",\"strengthened\":" + int_to_str(strengthened) + "}"
} }
fn mem_save(path: String) -> Void { fn mem_save(path: String) -> Void {
+12 -63
View File
@@ -75,24 +75,14 @@ fn strip_query(path: String) -> String {
} }
fn err_404(path: String) -> String { fn err_404(path: String) -> String {
// __status__ envelope el_runtime reads the first key and emits HTTP 404. return "{\"error\":\"not found\",\"code\":\"not_found\",\"path\":\"" + path + "\"}"
// Issue #3: previously returned HTTP 200 with JSON error body.
return "{\"__status__\":404,\"error\":\"not found\",\"path\":\"" + path + "\"}"
} }
fn err_405(method: String, path: String) -> String { fn err_405(method: String, path: String) -> String {
// __status__ envelope emits HTTP 405. return "{\"error\":\"method not allowed\",\"code\":\"method_not_allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}"
// Issue #3: previously returned HTTP 200 with JSON error body.
return "{\"__status__\":405,\"error\":\"method not allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}"
} }
fn route_health() -> String { fn route_health() -> String {
// NOTE (issue #8): This endpoint performs live engram graph queries on every call
// (engram_node_count, engram_edge_count) and reads imprint state. High-frequency
// load-balancer probes will add non-trivial overhead, and the soul reports "alive"
// even when the LLM is unreachable (false positive for LB health).
// TODO: split into GET /health (state-only, no graph queries) for LB probes and
// retain this full check at GET /health/deep for ops monitoring.
let cgi_id: String = state_get("soul_cgi_id") let cgi_id: String = state_get("soul_cgi_id")
let boot: String = state_get("soul_boot_count") let boot: String = state_get("soul_boot_count")
let boot_num: String = if str_eq(boot, "") { "0" } else { boot } let boot_num: String = if str_eq(boot, "") { "0" } else { boot }
@@ -151,8 +141,7 @@ fn route_lineage() -> String {
fn route_imprint_contextual(body: String) -> String { fn route_imprint_contextual(body: String) -> String {
if str_eq(body, "") { if str_eq(body, "") {
// Issue #5: empty body is a client error HTTP 400. return "{\"ok\":false,\"error\":\"empty body\"}"
return "{\"__status__\":400,\"ok\":false,\"error\":\"empty body\"}"
} }
let tags: String = "[\"imprint\",\"contextual\"]" let tags: String = "[\"imprint\",\"contextual\"]"
let id: String = engram_node_full( let id: String = engram_node_full(
@@ -174,8 +163,7 @@ fn route_imprint_contextual(body: String) -> String {
fn route_imprint_user(body: String) -> String { fn route_imprint_user(body: String) -> String {
if str_eq(body, "") { if str_eq(body, "") {
// Issue #5: empty body is a client error HTTP 400. return "{\"ok\":false,\"error\":\"empty body\"}"
return "{\"__status__\":400,\"ok\":false,\"error\":\"empty body\"}"
} }
let tags: String = "[\"imprint\",\"user\"]" let tags: String = "[\"imprint\",\"user\"]"
let id: String = engram_node_full( let id: String = engram_node_full(
@@ -313,13 +301,9 @@ fn connectd_get(suffix: String) -> String {
// so arbitrary JSON cannot reach the shell as a command-line argument. // so arbitrary JSON cannot reach the shell as a command-line argument.
fn connectd_post(suffix: String, body: String) -> String { fn connectd_post(suffix: String, body: String) -> String {
let eff: String = if str_eq(body, "") { "{}" } else { body } let eff: String = if str_eq(body, "") { "{}" } else { body }
// Issue #11: time_now() has second-granularity; two concurrent requests in the same // Unique temp path per call prevents collision if concurrency is ever added
// second collide on the same temp path. Added a monotonic per-process sequence counter. // or if two soul instances run on the same machine (latent correctness hazard).
let connectd_seq_s: String = state_get("connectd_post_seq") let tmp: String = "/tmp/neuron-connectors-req-" + int_to_str(time_now()) + ".json"
let connectd_seq_n: Int = if str_eq(connectd_seq_s, "") { 0 } else { str_to_int(connectd_seq_s) }
let connectd_seq_next: Int = connectd_seq_n + 1
state_set("connectd_post_seq", int_to_str(connectd_seq_next))
let tmp: String = "/tmp/neuron-connectors-req-" + int_to_str(time_now()) + "-" + int_to_str(connectd_seq_next) + ".json"
fs_write(tmp, eff) fs_write(tmp, eff)
let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp) let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp)
if str_eq(out, "") { if str_eq(out, "") {
@@ -354,33 +338,9 @@ fn handle_connectors(method: String, clean: String, body: String) -> String {
return "{\"ok\":false,\"error\":\"unknown connectors route\"}" return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
} }
// auth_check validate NEURON_TOKEN bearer auth on every request.
// Returns "" when authorized, or a JSON 401 error string when not.
// /health and /lineage are public routes always exempted.
// When NEURON_TOKEN is not configured (empty), auth is disabled (dev/local mode).
// Issue #4: previously no auth layer existed anywhere in the router.
// Clients pass the token in the JSON body as "__auth".
// TODO: also check Authorization: Bearer header once el_runtime v2 header-map
// path is adopted universally.
fn auth_check(clean: String, body: String) -> String {
if str_eq(clean, "/health") { return "" }
if str_eq(clean, "/lineage") { return "" }
let token: String = state_get("soul_token")
if str_eq(token, "") { return "" }
let auth_field: String = json_get(body, "__auth")
if str_eq(auth_field, token) { return "" }
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
}
fn handle_request(method: String, path: String, body: String) -> String { fn handle_request(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path) let clean: String = strip_query(path)
// Issue #1/#2: EL has no exception/try-catch mechanism. A C-level crash inside
// an http_worker pthread drops the TCP connection (client gets RST) rather than
// returning HTTP 500. TODO: register a SIGSEGV/SIGBUS handler in el_runtime.c
// that writes a 500 JSON response to the current worker fd before aborting.
// Rate limit check. Extract caller IP from REMOTE_ADDR env var (set by the // Rate limit check. Extract caller IP from REMOTE_ADDR env var (set by the
// EL HTTP runtime for each request). Skip enforcement when empty so // EL HTTP runtime for each request). Skip enforcement when empty so
// loopback/internal callers are never blocked. // loopback/internal callers are never blocked.
@@ -392,13 +352,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
} }
} }
// Auth enforced on all routes except /health and /lineage.
// Issue #4: previously no auth check existed anywhere in the router.
let auth_err: String = auth_check(clean, body)
if !str_eq(auth_err, "") {
return auth_err
}
if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") { if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") {
return handle_dharma_recv(body) return handle_dharma_recv(body)
} }
@@ -414,6 +367,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
return engram_scan_nodes_json(9999, 0) return engram_scan_nodes_json(9999, 0)
} }
if str_eq(clean, "/api/graph/edges") { if str_eq(clean, "/api/graph/edges") {
// TODO(reliability #8): engram_save races with awareness loop mem_save().
// Both now use atomic write-to-temp+rename (el_runtime.c). Serialised
// by engram_global_mu. Future: add engram_edges_json() builtin.
let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.json" let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.json"
engram_save(snap_path) engram_save(snap_path)
let snap: String = fs_read(snap_path) let snap: String = fs_read(snap_path)
@@ -426,8 +382,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
let raw_msg: String = json_get(body, "message") let raw_msg: String = json_get(body, "message")
let eff_msg: String = if str_eq(raw_msg, "") { body } else { raw_msg } let eff_msg: String = if str_eq(raw_msg, "") { body } else { raw_msg }
if str_eq(eff_msg, "") { if str_eq(eff_msg, "") {
// Issue #5: missing required param HTTP 400. return "{\"error\":\"message is required\",\"code\":\"missing_param\"}"
return "{\"__status__\":400,\"error\":\"message required\"}"
} }
let agentic_flag: Bool = json_get_bool(body, "agentic") let agentic_flag: Bool = json_get_bool(body, "agentic")
let reply: String = if agentic_flag { let reply: String = if agentic_flag {
@@ -571,15 +526,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
// responses are buffered and returned as a single JSON object. Streaming // responses are buffered and returned as a single JSON object. Streaming
// would require runtime-level SSE support in el_runtime.c and a redesign // would require runtime-level SSE support in el_runtime.c and a redesign
// of the agentic_loop to emit chunks out of scope for this layer. // of the agentic_loop to emit chunks out of scope for this layer.
// Issue #5: validate required params return HTTP 400 when missing.
let raw_msg: String = json_get(body, "message") let raw_msg: String = json_get(body, "message")
if str_eq(raw_msg, "") { if str_eq(raw_msg, "") {
return "{\"__status__\":400,\"error\":\"message is required\",\"response\":\"\"}" return "{\"error\":\"message is required\",\"code\":\"missing_param\"}"
}
// Issue #7: reject oversized messages before engram_compile and the LLM.
// Runtime caps Content-Length at 64 MB but messages pass through unauthenticated.
if str_len(raw_msg) > 32768 {
return "{\"__status__\":400,\"error\":\"message too large (max 32768 chars)\",\"response\":\"\"}"
} }
let agentic_flag: Bool = json_get_bool(body, "agentic") let agentic_flag: Bool = json_get_bool(body, "agentic")
let reply: String = if agentic_flag { let reply: String = if agentic_flag {
+21 -1
View File
@@ -244,7 +244,7 @@ fn safety_general_hard_phrases() -> String {
} }
fn safety_soft_phrases() -> String { fn safety_soft_phrases() -> String {
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\",\"highest structure\",\"tallest building\",\"tallest structure\",\"highest building\",\"bridge near me\",\"overpass near\",\"rooftop near\"]" return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\""]"
} }
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call. // ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
@@ -295,6 +295,26 @@ fn safety_count_match(text: String, phrases_json: String) -> Int {
// Returns "none" | "soft" | "hard". Hard bell triggers on ANY match (cost of a miss // Returns "none" | "soft" | "hard". Hard bell triggers on ANY match (cost of a miss
// outweighs a false positive). Soft bell needs >= 2 matches to reduce false positives. // outweighs a false positive). Soft bell needs >= 2 matches to reduce false positives.
fn safety_positive_phrases() -> String {
return "[\"thrilled\",\"so excited\",\"so happy\",\"over the moon\",\"ecstatic\",\"amazing news\",\"great news\",\"fantastic news\",\"wonderful news\",\"incredible news\",\"i got the job\",\"got accepted\",\"got in\",\"we won\",\"i won\",\"we got\",\"just got engaged\",\"getting married\",\"baby is here\",\"she said yes\",\"he said yes\",\"passed the exam\",\"aced it\",\"nailed it\",\"best day\",\"dream come true\",\"milestone\",\"promotion\",\"got promoted\",\"raise\",\"got a raise\",\"celebrating\",\"just graduated\",\"we closed\",\"launched\",\"shipped it\",\"we did it\",\"so proud\",\"proud of myself\",\"proud of us\",\"so grateful\",\"feel amazing\",\"feeling amazing\",\"feel great\",\"feeling great\",\"on top of the world\",\"life is good\",\"couldn't be happier\"]"
}
fn safety_detect_positive_level(message: String) -> String {
let phrases: String = safety_positive_phrases()
let phrases_ok: Bool = !str_eq(phrases, "") && !str_eq(phrases, "[]")
if !phrases_ok { return "none" }
let n: Int = json_array_len(phrases)
let i: Int = 0
while i < n {
let phrase: String = json_array_get(phrases, i)
if str_contains(message, phrase) {
return "high"
}
let i = i + 1
}
return "none"
}
fn safety_detect_bell_level(message: String) -> String { fn safety_detect_bell_level(message: String) -> String {
let text: String = safety_normalize(message) let text: String = safety_normalize(message)
let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases()) let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases())
+36
View File
@@ -104,6 +104,8 @@ fn session_create(body: String) -> String {
// Newest sessions first (prepend). // Newest sessions first (prepend).
// TODO #4: index update is read-modify-write two concurrent session_create // TODO #4: index update is read-modify-write two concurrent session_create
// calls can lose one entry. EL has no CAS primitive; fix requires runtime support. // calls can lose one entry. EL has no CAS primitive; fix requires runtime support.
// TODO(reliability #2): session_index RMW is non-atomic. Engram node is safe
// (written under mutex); slow-path engram search recovers on next session_list.
let existing_idx: String = state_get("session_index") let existing_idx: String = state_get("session_index")
let idx_entry: String = "{\"id\":\"" + id + "\",\"title\":\"" + json_safe(title) + "\",\"folder\":\"" + json_safe(folder) + "\",\"created_at\":" + int_to_str(ts) + ",\"updated_at\":" + int_to_str(ts) + ",\"last_message\":\"\"}" let idx_entry: String = "{\"id\":\"" + id + "\",\"title\":\"" + json_safe(title) + "\",\"folder\":\"" + json_safe(folder) + "\",\"created_at\":" + int_to_str(ts) + ",\"updated_at\":" + int_to_str(ts) + ",\"last_message\":\"\"}"
let new_idx: String = if str_eq(existing_idx, "") { let new_idx: String = if str_eq(existing_idx, "") {
@@ -440,6 +442,8 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
} }
let oi = oi + 1 let oi = oi + 1
} }
// TODO(reliability #7): delete-then-insert is not atomic concurrent saves for the
// same session can produce orphan history nodes. State is primary truth; engram fallback.
let tags: String = "[\"session\",\"session-history\",\"Conversation\"]" let tags: String = "[\"session\",\"session-history\",\"Conversation\"]"
let discard: String = engram_node_full( let discard: String = engram_node_full(
hist, "Conversation", "session:messages:" + session_id, hist, "Conversation", "session:messages:" + session_id,
@@ -488,6 +492,38 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
state_set(summary_written_key, "1") state_set(summary_written_key, "1")
} }
} }
// Issue 5 fix: write a last-session-topic Conversation node so future sessions can
// find the most recent session's topic via engram search. This enables cross-session
// continuity chat.el searches for "last-session-topic" and shows a [CONTINUING FROM
// LAST SESSION] section on the first message of a new session.
let hist_arr_len: Int = if str_eq(hist, "") { 0 } else { json_array_len(hist) }
if hist_arr_len >= 2 {
let last_entry: String = json_array_get(hist, hist_arr_len - 1)
let last_role: String = json_get(last_entry, "role")
let last_content: String = json_get(last_entry, "content")
let topic_snip: String = if str_len(last_content) > 200 { str_slice(last_content, 0, 200) } else { last_content }
let safe_topic: String = str_replace(topic_snip, """, "'")
let ts_now: String = int_to_str(time_now())
let topic_content: String = "last-session-topic | ts:" + ts_now + " | session:" + session_id + " | topic:" + safe_topic
let topic_tags: String = "["last-session-topic","conv:history","Conversation","session:topic"]"
let topic_label: String = "last-session-topic:" + session_id
// Delete old last-session-topic node for this session before writing fresh
let old_topic: String = engram_search_json("last-session-topic:" + session_id, 2)
let ot_len: Int = if str_eq(old_topic, "") { 0 } else { json_array_len(old_topic) }
let oti: Int = 0
while oti < ot_len {
let ot_node: String = json_array_get(old_topic, oti)
let ot_id: String = json_get(ot_node, "id")
if !str_eq(ot_id, "") { engram_forget(ot_id) }
let oti = oti + 1
}
let discard_topic: String = engram_node_full(
topic_content, "Conversation", topic_label,
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
"Episodic", topic_tags
)
}
} }
// session_update_meta_timestamp update the updated_at field in the session:meta node. // session_update_meta_timestamp update the updated_at field in the session:meta node.
+160 -79
View File
@@ -148,6 +148,14 @@ fn load_identity_context() -> Void {
println("[soul] identity context loaded (" + int_to_str(str_len(ctx)) + " chars, " + int_to_str(parts_count) + " nodes)") 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. // 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". // 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. // genesis derives identity from the graph directly; cultivated souls have this node seeded.
@@ -163,37 +171,73 @@ fn load_identity_context() -> Void {
} }
} }
// Cross-session affective context: query engram for recent distress/crisis signals // Cross-session affective context: load BellEvent and PositiveEvent nodes from last 7 days.
// at session start. Stored under soul_affective_context so the safety layer can let aff_now: Int = time_now()
// detect when a user has been in distress across previous sessions. let aff_7d: Int = aff_now - 604800
// Soft recency guard: nodes with a ts field older than 7 days are skipped. let bell_raw: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
// Results capped at 3 nodes, 200 chars each, to avoid over-injection into context. let bell_aff_ok: Bool = !str_eq(bell_raw, "") && !str_eq(bell_raw, "[]")
// TODO(recency): engram_search_json sorts by relevance, not timestamp. A native let aff_ctx: String = ""
// after=<ts> filter in the engram search API would make this more precise. let aff_ctx = if bell_aff_ok {
let affective_raw: String = engram_search_json("distress crisis upset hopeless", 3) let bn_total: Int = json_array_len(bell_raw)
let affective_ok: Bool = !str_eq(affective_raw, "") && !str_eq(affective_raw, "[]") let bacc: String = ""
if affective_ok { let bi: Int = 0
let ts_now: Int = time_now() let bacc = while bi < bn_total {
let ts_cutoff: Int = ts_now - 604800 let bn: String = json_array_get(bell_raw, bi)
let aff_total: Int = json_array_len(affective_raw) let bn_c: String = json_get(bn, "content")
let aff_ctx: String = "" let bm: String = " | ts:"
let ai: Int = 0 let bmp: Int = str_index_of(bn_c, bm)
while ai < aff_total { let bn_ts_raw: String = if bmp >= 0 {
let aff_node: String = json_array_get(affective_raw, ai) let bs: Int = bmp + str_len(bm)
let aff_content: String = json_get(aff_node, "content") let br: String = str_slice(bn_c, bs, str_len(bn_c))
let aff_ts_str: String = json_get(aff_node, "ts") let bn_next: Int = str_index_of(br, " | ")
let aff_ts: Int = if str_eq(aff_ts_str, "") { ts_now } else { str_to_int(aff_ts_str) } if bn_next < 0 { br } else { str_slice(br, 0, bn_next) }
let is_recent: Bool = aff_ts >= ts_cutoff } else {
let snip: String = if str_len(aff_content) > 200 { str_slice(aff_content, 0, 200) } else { aff_content } let bca: String = json_get(bn, "created_at")
let aff_ctx = if is_recent && !str_eq(snip, "") { if str_eq(bca, "") { json_get(bn, "updated_at") } else { bca }
if str_eq(aff_ctx, "") { snip } else { aff_ctx + "\n" + snip } }
} else { aff_ctx } let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) }
let ai = ai + 1 let snip: String = if str_len(bn_c) > 200 { str_slice(bn_c, 0, 200) } else { bn_c }
let bacc = if bn_ts >= aff_7d && !str_eq(snip, "") {
if str_eq(bacc, "") { snip } else { bacc + "\n" + snip }
} else { bacc }
let bi = bi + 1
bacc
} }
if !str_eq(aff_ctx, "") { bacc
state_set("soul_affective_context", aff_ctx) } else { "" }
println("[soul] cross-session affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)") 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 {
let pn_total: Int = json_array_len(pos_raw)
let pacc: String = aff_ctx
let pi: Int = 0
let pacc = while pi < pn_total {
let pn: String = json_array_get(pos_raw, pi)
let pn_c: String = json_get(pn, "content")
let pm: String = " | ts:"
let pmp: Int = str_index_of(pn_c, pm)
let pn_ts_raw: String = if pmp >= 0 {
let ps: Int = pmp + str_len(pm)
let pr: String = str_slice(pn_c, ps, str_len(pn_c))
let pn_next: Int = str_index_of(pr, " | ")
if pn_next < 0 { pr } else { str_slice(pr, 0, pn_next) }
} else {
let pca: String = json_get(pn, "created_at")
if str_eq(pca, "") { json_get(pn, "updated_at") } else { pca }
}
let pn_ts: Int = if str_eq(pn_ts_raw, "") { 0 } else { str_to_int(pn_ts_raw) }
let psnip: String = if str_len(pn_c) > 200 { str_slice(pn_c, 0, 200) } else { pn_c }
let pacc = if pn_ts >= aff_7d && !str_eq(psnip, "") {
if str_eq(pacc, "") { psnip } else { pacc + "\n" + psnip }
} else { pacc }
let pi = pi + 1
pacc
} }
pacc
} 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)")
} }
} }
@@ -241,13 +285,8 @@ fn seed_persona_from_env() -> Void {
let h: Map = {} let h: Map = {}
map_set(h, "Content-Type", "application/json") map_set(h, "Content-Type", "application/json")
let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h) let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h)
// Check for empty response (timeout/network error), explicit error, or missing id. if str_contains(resp, "\"error\"") {
if str_eq(resp, "") {
println("[soul] persona HTTP write-back failed: empty response (timeout or network error) — in-memory only this session")
} else if str_contains(resp, "\"error\"") {
println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp) println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp)
} else if !str_contains(resp, "\"id\"") {
println("[soul] persona HTTP write-back: unexpected response (no id field) — in-memory only this session: " + resp)
} else { } else {
println("[soul] persona persisted to HTTP engram at " + engram_url) println("[soul] persona persisted to HTTP engram at " + engram_url)
} }
@@ -271,33 +310,51 @@ fn emit_session_start_event() -> Void {
} }
let ts: Int = time_now() 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\"" let payload: String = "{\"event\":\"session_start\""
+ ",\"boot\":" + boot_num + ",\"boot\":" + boot_num
+ ",\"cgi\":\"" + eff_cgi + "\"" + ",\"cgi\":\"" + eff_cgi + "\""
+ ",\"node_count\":" + int_to_str(node_ct) + ",\"node_count\":" + int_to_str(node_ct)
+ ",\"edge_count\":" + int_to_str(edge_ct) + ",\"edge_count\":" + int_to_str(edge_ct)
+ ",\"identity_loaded\":" + has_identity + ",\"identity_loaded\":" + has_identity
+ ",\"prev_session_summary_loaded\":" + has_prev_sum
+ ",\"ts\":" + int_to_str(ts) + "}" + ",\"ts\":" + int_to_str(ts) + "}"
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]" let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
let session_event_id: String = engram_node_full( let discard: String = engram_node_full(
payload, "InternalStateEvent", "session-start", payload, "InternalStateEvent", "session-start",
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0), el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
"Episodic", tags "Episodic", tags
) )
if str_eq(session_event_id, "") { 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 + ")")
println("[soul] emit_session_start_event: engram write failed — session-start event lost")
}
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")")
} }
// layered_cycle routes user-facing requests through the 4-layer consciousness stack. // 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) // 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. // Internal cognition (heartbeat, proactive, memory ops) bypasses layers use one_cycle directly.
fn layered_cycle(raw_input: String) -> String { fn layered_cycle(raw_input: String) -> String {
// conv_history key must match chat.el (conv_history, not conversation_history).
// Mismatch caused safety_score_distress_history() to always receive "" - the
// history-amplification path in safety_threat_score was permanently dead.
let history: String = state_get("conv_history") let history: String = state_get("conv_history")
let session_id: String = state_get("current_session_id") let session_id: String = state_get("current_session_id")
@@ -305,9 +362,8 @@ fn layered_cycle(raw_input: String) -> String {
let screen_result: String = safety_screen(raw_input, history) let screen_result: String = safety_screen(raw_input, history)
let screen_action: String = json_get(screen_result, "action") let screen_action: String = json_get(screen_result, "action")
// ISSUE 4: safe-mode guard -- if safety_screen returned invalid/empty action, // ISSUE 4: safe-mode guard. If safety_screen returned an invalid/empty action
// refuse the turn rather than silently passing unscreened input to upper layers. // (engram failure or internal error), refuse rather than pass unscreened input.
// Valid actions: "hard_bell", "soft_bell", "pass". Anything else = corrupt envelope.
let valid_action: Bool = str_eq(screen_action, "hard_bell") let valid_action: Bool = str_eq(screen_action, "hard_bell")
|| str_eq(screen_action, "soft_bell") || str_eq(screen_action, "soft_bell")
|| str_eq(screen_action, "pass") || str_eq(screen_action, "pass")
@@ -322,8 +378,8 @@ fn layered_cycle(raw_input: String) -> String {
// history where they could leak context to subsequent turns. They are persisted // history where they could leak context to subsequent turns. They are persisted
// separately by safety_log_bell() into the Episodic tier with restricted labels. // separately by safety_log_bell() into the Episodic tier with restricted labels.
// //
// ISSUE 6: safety_log_bell for hard bells is already called INSIDE safety_screen // ISSUE 6: safety_log_bell already called inside safety_screen (line 140).
// (safety.el line 140). Do NOT call it again here -- double-log avoided. // 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 // 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 // receives the sentinel string "hard_bell" (not a normal screen action). The safety
@@ -341,8 +397,11 @@ fn layered_cycle(raw_input: String) -> String {
let cont_status: String = json_get(continuity, "status") let cont_status: String = json_get(continuity, "status")
let cont_action: String = json_get(continuity, "action") let cont_action: String = json_get(continuity, "action")
// Store continuity status so imprint can adjust its response register // Store continuity status so imprint can adjust its response register.
state_set("session_continuity", cont_status) // TODO(reliability #4): session_continuity is process-global; scope per session_id
// when available to prevent cross-session bleed under concurrent layered_cycle calls.
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 // Identity anomaly: add a gentle verification cue to the input before imprint
let guided: String = if str_eq(cont_action, "identity_check") { let guided: String = if str_eq(cont_action, "identity_check") {
@@ -365,14 +424,53 @@ fn layered_cycle(raw_input: String) -> String {
json_get(steward_result, "redirect_to") json_get(steward_result, "redirect_to")
} }
// ISSUE 1: apply pre-LLM bell augmentation on layered_cycle path. // L2c: affective context injection.
// safety_augment_system injects soft/hard directive into system prompt before LLM call. let lc_aff_cutoff: Int = time_now() - 259200
// Stored in state so imprint_respond can consume it. let lc_bell_nodes: String = engram_search_json("bell:soft bell:hard BellEvent affective", 2)
// TODO: wire directly into imprint_respond when it accepts a system_override param. let lc_has_bell: Bool = !str_eq(lc_bell_nodes, "") && !str_eq(lc_bell_nodes, "[]")
// ISSUE 3 TODO: no semantic/embedding crisis detection. Keyword-only means signals let lc_bell_note: String = if lc_has_bell {
// evading the phrase list pass through with zero augmentation. Semantic layer is a let lb0: String = json_array_get(lc_bell_nodes, 0)
// separate architectural decision requiring embedding inference on every message. let lb_c: String = json_get(lb0, "content")
let lbm: String = " | ts:"
let lbmp: Int = str_index_of(lb_c, lbm)
let lb_ts_raw: String = if lbmp >= 0 {
let lbs: Int = lbmp + str_len(lbm)
let lbr: String = str_slice(lb_c, lbs, str_len(lb_c))
let lbn: Int = str_index_of(lbr, " | ")
if lbn < 0 { lbr } else { str_slice(lbr, 0, lbn) }
} else {
let lbca: String = json_get(lb0, "created_at")
if str_eq(lbca, "") { json_get(lb0, "updated_at") } else { lbca }
}
let lb_ts: Int = if str_eq(lb_ts_raw, "") { 0 } else { str_to_int(lb_ts_raw) }
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, "[]")
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_c: String = json_get(lp0, "content")
let lpm: String = " | ts:"
let lpmp: Int = str_index_of(lp_c, lpm)
let lp_ts_raw: String = if lpmp >= 0 {
let lps: Int = lpmp + str_len(lpm)
let lpr: String = str_slice(lp_c, lps, str_len(lp_c))
let lpn: Int = str_index_of(lpr, " | ")
if lpn < 0 { lpr } else { str_slice(lpr, 0, lpn) }
} else {
let lpca: String = json_get(lp0, "created_at")
if str_eq(lpca, "") { json_get(lp0, "updated_at") } else { lpca }
}
let lp_ts: Int = if str_eq(lp_ts_raw, "") { 0 } else { str_to_int(lp_ts_raw) }
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: 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) state_set("layered_cycle_safety_system_addendum", augmented_addendum)
// L3: imprint responds // L3: imprint responds
@@ -414,29 +512,12 @@ let snapshot_usable: Bool = local_node_count > 50
if using_http_engram && !snapshot_usable { if using_http_engram && !snapshot_usable {
// First boot or empty/corrupt snapshot: seed from HTTP Engram. // First boot or empty/corrupt snapshot: seed from HTTP Engram.
// Retry up to 3 times (2s sleep between attempts) to guard against a
// transient network hiccup right after entrypoint.sh health check passes.
// An empty nodes response silently loads a zero-node graph; validate first.
// TODO(reliability): replace sleep_ms retry with non-blocking backoff.
println("[soul] engram -> HTTP " + engram_url_raw + " (no local snapshot, first boot)") println("[soul] engram -> HTTP " + engram_url_raw + " (no local snapshot, first boot)")
let fetch_attempt: Int = 0 let nodes_json: String = http_get(engram_url_raw + "/api/nodes?limit=10000")
while fetch_attempt < 3 { let edges_json: String = http_get(engram_url_raw + "/api/edges")
let fetch_attempt = fetch_attempt + 1 let nodes_part: String = if str_eq(nodes_json, "") { "[]" } else { nodes_json }
let n: String = http_get(engram_url_raw + "/api/nodes?limit=10000") let edges_part: String = if str_eq(edges_json, "") { "[]" } else { edges_json }
let e: String = http_get(engram_url_raw + "/api/edges") let snapshot_data: String = "{\"nodes\":" + nodes_part + ",\"edges\":" + edges_part + "}"
let nodes_ok: Bool = !str_eq(n, "") && str_starts_with(n, "[") && str_len(n) > 2
if nodes_ok {
state_set("_boot_nodes_json", n)
state_set("_boot_edges_json", e)
let fetch_attempt = 3
} else {
println("[soul] boot HTTP fetch attempt " + int_to_str(fetch_attempt) + " failed --- retrying in 2s")
sleep_ms(2000)
}
}
let nodes_json: String = state_get("_boot_nodes_json")
let edges_json: String = state_get("_boot_edges_json")
let snapshot_data: String = "{\"nodes\":" + nodes_part + ",\"edges\":" + edges_part + "}"
let tmp_path: String = "/tmp/soul-engram-" + soul_cgi_id + ".json" let tmp_path: String = "/tmp/soul-engram-" + soul_cgi_id + ".json"
fs_write(tmp_path, snapshot_data) fs_write(tmp_path, snapshot_data)
engram_load(tmp_path) engram_load(tmp_path)