Compare commits

..

20 Commits

Author SHA1 Message Date
will.anderson 588ca11f57 fix(context-dedup): include scan_part and affective_part IDs in seen set
Two design bugs in the state_set placement caused the dedup seen-ID set
to be incomplete even with callsites wired up:

1. state_set("engram_compile_seen_ids") was called immediately after
   merging the main node pools, before scan_part (persona fallback) and
   affective_part (bell node) were computed. Nodes appearing only in
   those segments were never added to the seen set.

2. affective_part is a bare JSON object (bn0 from json_array_get), not
   a JSON array. Passing it to engram_extract_ids would have gotten
   json_array_len == 0 and silently skipped the affective node's ID.

Fix: move state_set to after ctx is assembled from all three segments.
Extract ids_from_merged and ids_from_scan via engram_extract_ids (both
are JSON arrays), and extract ids_from_affective via json_get(affective_part, "id")
directly since it is a bare object. Merge all three via add_to_seen
before publishing to state.
2026-06-22 14:19:14 -05:00
will.anderson 9e178d8371 fix(recall): deduplicate engram nodes by ID across activation and search passes
Thread a seen-node-ID exclusion set from engram_compile() through to
session_preload in handle_chat, preventing the same high-salience nodes
(identity, recent memories) from appearing 2-3x in the system prompt.

Changes:
- Add id_in_seen(), add_to_seen(), engram_extract_ids() helpers that
  maintain a comma-delimited seen-ID accumulator (EL has no Set type)
- In engram_compile(): after merging all topic/entity/recall pools, extract
  node IDs from merged_nodes and publish via state_set(engram_compile_seen_ids)
- In handle_chat(): read seen_ids from state after engram_compile() returns,
  then check id_in_seen() before emitting each session_preload bullet
  (profile x3, work x2, project x2, summary x1 — all 8 candidate nodes guarded)

Nodes already present in the compiled engram context are skipped in preload,
eliminating 3000-3500 token repetition on first-message turns.
2026-06-22 14:06:04 -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 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 6edf9937dd fix(reliability): LLM retry
Neuron Soul CI / build (pull_request) Has been cancelled
2026-06-22 12:37:29 -05:00
will.anderson e447a87a00 fix(reliability): route error recovery 2026-06-22 12:37:21 -05:00
will.anderson 575ff1329a fix(reliability): engram connection 2026-06-22 12:34:04 -05:00
will.anderson db33b0cb91 fix(reliability): engram write 2026-06-22 12:32:59 -05:00
will.anderson f35569d4bb fix(reliability): cross-session affective state 2026-06-22 12:31:09 -05:00
will.anderson 94b71b6e6b fix(reliability): conversation history 2026-06-22 12:29:23 -05:00
will.anderson 392d2416ec fix(reliability): replace undefined session_exists with session_get check
Neuron Soul CI / build (pull_request) Failing after 13m25s
2026-06-22 12:21: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
will.anderson 2865d6ad26 fix(reliability): route-error-recovery
Neuron Soul CI / build (pull_request) Has been cancelled
- Issue #3: err_404/err_405 now emit HTTP 404/405 via __status__ envelope instead of HTTP 200
- Issue #4: add auth_check() function to handle_request; enforces NEURON_TOKEN on all routes except /health and /lineage
- Issue #5: missing required params now return HTTP 400 (__status__ envelope) in /api/chat (GET+POST), /imprint/contextual, /imprint/user, and handle_chat
- Issue #6: LLM unavailable in handle_chat now returns HTTP 503 instead of HTTP 200
- Issue #7: add 32 KB message size guard on POST /api/chat before engram_compile and LLM
- Issue #8: add TODO comment to route_health documenting the live-engram-query problem and the /health/deep split plan
- Issue #9: add comment to hist_trim documenting fragile str_index_of parser and silent data corruption risk
- Issue #10: add TODO comment in handle_request documenting missing per-IP rate limiting
- Issue #11: fix connectd_post temp file collision — add monotonic sequence counter so concurrent requests get unique paths
- Issue #12: fix call_mcp_bridge fixed temp file race — add monotonic sequence counter for unique paths under concurrent load
- Issues #1/#2: add TODO comment in handle_request documenting EL no-exception limitation and SIGSEGV handler gap
2026-06-22 12:00:06 -05:00
will.anderson 47d0e6f985 fix(reliability): llm-retry — empty response detection, configurable max_tokens, connector timeout
Neuron Soul CI / build (pull_request) Failing after 11m16s
Issue #5: detect empty string from llm_extract_text() as an error in handle_chat,
handle_chat_as_soul, and handle_dharma_room_turn. The C runtime silently returns ""
when the LLM response content array is missing or all blocks fail to parse; without
this guard the empty string passes through to callers as a silent empty reply.

Issue #9: make agentic_loop max_tokens configurable via NEURON_LLM_MAX_TOKENS env
var (default 4096). The hardcoded value is marginal for long tool chains (8 iterations
x 4096 tokens); operators can now set 8192+ for complex multi-step tasks without
rebuilding. Non-agentic path (llm_call_system) still uses the C runtime hardcode —
that fix lives in el_runtime.c (see TODO block added in this commit).

Issue #10: increase connector_tools_json and tool_auto_approved curl --max-time from
2s to 5s to reduce false-empty tool lists when neuron-connectd is under transient
load. Graceful degradation to [] on bridge down is unchanged.

Issues #1/#2/#3/#4/#6/#8: documented as TODO comments in chat.el. These require
targeted C runtime changes in el_runtime.c (llm_provider_request retry loop,
EL_LLM_TIMEOUT_MS separation, HTTP 429 backoff, 5xx retry, EL_HTTP_MAX_RESPONSE_BYTES
cap). Architectural decisions recorded so they are traceable to root causes.
2026-06-22 11:59:43 -05:00
will.anderson d008649c3e fix(reliability): engram-connection
Neuron Soul CI / build (pull_request) Has been cancelled
- entrypoint.sh: extend engram health-check timeout 30->60s; set
  EL_HTTP_TIMEOUT_MS=10000 and EL_HTTP_CONNECT_TIMEOUT_MS=3000 to bound
  awareness loop blocking window to 10s/call (down from 60s default)
- soul.el: 3-attempt retry loop for boot-time /api/nodes+/api/edges fetch;
  validate non-empty JSON array before loading to prevent silent zero-node
  identity graph from transient post-healthcheck network hiccup
- awareness.el: soft circuit-breaker in ise_post (opens after 3 failures,
  30s backoff, half-open probe); /api/sync refresh skips HTTP call when
  breaker is open; error-JSON detection on sync response

TODOs: full async dispatch, connection pooling (require EL futures/persistent curl)
2026-06-22 11:57:20 -05:00
will.anderson aa70c5dde6 fix(reliability): safety-resilience — bell augmentation, safe mode, dedup logging, tab escaping, handle_chat coverage 2026-06-22 11:54:40 -05:00
will.anderson deddb9a18e fix(reliability): safety-resilience — bell augmentation, safe mode, dedup logging, tab escaping, handle_chat coverage 2026-06-22 11:53:07 -05:00
will.anderson 494d973a3b fix(reliability): engram-write — guard all fire-and-forget writes
Neuron Soul CI / build (pull_request) Has been cancelled
Every engram_node_full call that dropped its return value now binds it
and emits a println on empty string. engram_save calls in consolidate,
heartbeat, and dharma-room-turn are checked for failure. The two API
handlers (log_state_event, tune_config) that skipped api_persisted()
now match the read-back-after-write contract used everywhere else in
neuron-api.el.

Files changed:
- chat.el: conv_history_persist, handle_dharma_room_turn, auto_persist
- soul.el: emit_session_start_event, seed_persona_from_env HTTP check
- memory.el: mem_save, mem_boot_count_inc
- neuron-api.el: handle_api_log_state_event, handle_api_tune_config,
  handle_api_consolidate (engram_save + session summary write)
- awareness.el: ise_post local-engram fallback path

TODO comments added for non-atomic patterns (issues #12, #13) and
the missing circuit breaker (#14) — these require new primitives.
2026-06-22 11:48:59 -05:00
will.anderson 34551695a1 fix(reliability): cross-session-affective
Neuron Soul CI / build (pull_request) Has been cancelled
- Fix state key mismatch: soul.el layered_cycle now reads conv_history
  (not conversation_history), unblocking the safety_score_distress_history
  history-amplification path in safety_threat_score
- Add safety_augment_system call on the main handle_chat path so the
  phrase-list bell detector fires on all chat turns, not just dharma rooms
- Add cross-session affective engram query in load_identity_context() at
  boot; stores distress/crisis signals from prior sessions under
  soul_affective_context with a 7-day soft recency filter
2026-06-22 11:48:30 -05:00
will.anderson 615f0cee08 fix(reliability): conv-history — asymmetric load, silent failures, broken trim, agentic gap
Neuron Soul CI / build (pull_request) Has been cancelled
Issues addressed:
- #1 ASYMMETRIC PERSIST/LOAD: conv_history_load() now tries engram_get_node_by_label()
  first (symmetric with the label-based write), falling back to vector search only when
  label lookup returns nothing. Immune to cold/corrupt vector index.
- #2 SILENT LOAD FAILURE: all failure paths in conv_history_load() and conv_history_persist()
  now emit a println log line rather than silently returning "" or dropping writes.
- #3 NO RECOVERY PATH: documented as TODO with explanation of why a full recovery path
  (retry, ID fallback, orphan cleanup) is too invasive for a targeted fix here.
- #4 OVERWRITE WITHOUT DELETE: documented with TODO to replace engram_node_full with
  explicit delete-then-create once engram exposes a label-scoped delete API.
- #5/#10 BROKEN TRIM / OFF-BY-ONE: hist_trim() rewritten to use json_array_len /
  json_array_get (structural JSON ops) instead of raw str_index_of scanning for
  '{"role":' markers. Immune to marker strings appearing inside message content.
  Minimum retained count guard added: never trims below 2 entries.
- #6 PARTIAL-WRITE GUARD: conv_history_persist() refuses to write a blob that doesn't
  contain both '[' and ']'. conv_history_load() requires both before accepting content.
- #7 DUAL STORAGE: documented with a comment at the persist call site.
- #8 NO MAX SIZE GUARD: documented as TODO with rationale for why a byte-length cap
  requires a more invasive change (entry truncation or summarisation).
- #9 AGENTIC HISTORY NOT PERSISTED: handle_chat_agentic() now calls conv_history_persist()
  for the default global session (hist_key == "conv_history") after updating state,
  matching the non-agentic path's durability. Named sessions remain in-process only.
2026-06-22 11:46:00 -05:00
10 changed files with 484 additions and 359 deletions
+2
View File
@@ -678,6 +678,8 @@ fn threat_trajectory_check(tool_name: String, tool_input: String) -> Int {
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 {
let current: String = state_get("agentic_conv_history")
let safe_text: String = str_to_lower(text)
+409 -201
View File
@@ -73,8 +73,9 @@ fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
while ci < total {
let node: String = json_array_get(nodes_json, ci)
let score: Int = engram_score_node(node)
// Only include reasonably relevant nodes (threshold=25)
let above_thresh: Bool = score >= 25
// Threshold lowered from 25 to 15: includes moderately-relevant older nodes.
// A 3-week-old node with salience 0.6 and importance 0.6 scores ~18 was dropped, now included.
let above_thresh: Bool = score >= 15
// Check this index wasn't already selected (sentinel: look for idx marker)
let idx_marker: String = "\"_sel_" + int_to_str(ci) + "\""
let already_picked: Bool = str_contains(selected, idx_marker)
@@ -113,59 +114,289 @@ fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
let c7: String = str_replace(c6, "\"_sel_7\":1,", "")
let c8: String = str_replace(c7, "\"_sel_8\":1,", "")
let c9: String = str_replace(c8, "\"_sel_9\":1,", "")
return c9
let c10: String = str_replace(c9, "\"_sel_10\":1,", "")
let c11: String = str_replace(c10, "\"_sel_11\":1,", "")
let c12: String = str_replace(c11, "\"_sel_12\":1,", "")
let c13: String = str_replace(c12, "\"_sel_13\":1,", "")
let c14: String = str_replace(c13, "\"_sel_14\":1,", "")
return c14
}
// engram_split_topics split message into sub-queries on explicit conjunctions.
// "health goals AND startup progress" becomes two independent searches.
fn engram_split_topics(message: String) -> String {
let sep: String = if str_contains(message, " AND ") { " AND " } else {
if str_contains(message, " and ") { " and " } else {
if str_contains(message, " also ") { " also " } else {
if str_contains(message, " plus ") { " plus " } else { "" }
}
}
}
if str_eq(sep, "") { return message }
let sep_pos: Int = str_index_of(message, sep)
let part1: String = str_slice(message, 0, sep_pos)
let part2: String = str_slice(message, sep_pos + str_len(sep), str_len(message))
let part2_topics: String = engram_split_topics(part2)
if str_eq(part1, "") { return part2_topics }
return part1 + "\n" + part2_topics
}
// engram_extract_entities extract probable named entities (capital-first, 3+ chars,
// not stop-words) from a message. Returns newline-separated list.
fn engram_extract_entities(message: String) -> String {
let stops: String = "|I|A|The|An|In|On|At|To|Of|For|And|But|Or|So|My|Me|We|Us|He|She|It|Is|Are|Was|Were|Has|Have|Had|Do|Does|Did|Can|Could|Will|Would|Should|May|Might|Must|Be|Been|Being|This|That|These|Those|What|When|Where|Who|How|Why|Which|If|Then|Now|Just|Also|Not|No|Yes|Oh|Hi|Hey|Ok|Okay|Please|Thank|Thanks|You|Your|Our|Its|His|Her|Their|Any|All|Some|Get|Got|Let|Say|Think|Know|See|Look|Go|Come|Make|Take|Give|Tell|Ask|Need|Want|Like|Love|Feel|Try|Use|Find|Keep|Put|Set|Run|Start|Stop|Show|Help|Work|Play|Move|Change|Follow|Call|Talk|Check|Remind|Update|Create|Delete|Fix|Add|Remove|Open|Close|Read|Write|Send|Receive|"
let capitals: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let entities: String = ""
let entity_count: Int = 0
let msg_len: Int = str_len(message)
let pos: Int = 0
while pos < msg_len && entity_count < 10 {
let wend: Int = pos
let scanning: Bool = true
while scanning && wend < msg_len {
let wch: String = str_slice(message, wend, wend + 1)
let is_sep: Bool = str_eq(wch, " ") || str_eq(wch, "\n") || str_eq(wch, "\t")
|| str_eq(wch, ",") || str_eq(wch, ".") || str_eq(wch, "?")
|| str_eq(wch, "!") || str_eq(wch, ":") || str_eq(wch, ";")
|| str_eq(wch, "(") || str_eq(wch, ")") || str_eq(wch, "\'") || str_eq(wch, "-")
let scanning = if is_sep { false } else { scanning }
let wend = if !is_sep { wend + 1 } else { wend }
}
let word: String = str_slice(message, pos, wend)
let word_len: Int = str_len(word)
let first_ch: String = if word_len >= 3 { str_slice(word, 0, 1) } else { "" }
let is_capital: Bool = word_len >= 3 && str_contains(capitals, first_ch)
let is_stop: Bool = str_contains(stops, "|" + word + "|")
let already_have: Bool = str_contains(entities, word)
let should_add: Bool = is_capital && !is_stop && !already_have && word_len >= 3
let entities = if should_add {
let entity_count = entity_count + 1
if str_eq(entities, "") { word } else { entities + "\n" + word }
} else { entities }
let pos = if wend > pos { wend + 1 } else { pos + 1 }
}
return entities
}
// engram_detect_recall_intent true when message explicitly requests memory recall.
fn engram_detect_recall_intent(message: String) -> Bool {
return str_contains(message, "remind me")
|| str_contains(message, "do you remember")
|| str_contains(message, "what do you know")
|| str_contains(message, "what happened")
|| str_contains(message, "tell me about")
|| str_contains(message, "what was")
|| str_contains(message, "what were")
|| str_contains(message, "how is it going")
|| str_contains(message, "how are things")
|| str_contains(message, "catch me up")
|| str_contains(message, "fill me in")
|| str_contains(message, "what's the status")
|| str_contains(message, "whats the status")
|| str_contains(message, "any updates")
|| str_contains(message, "recap")
|| str_contains(message, "look up")
|| str_contains(message, "check on")
|| str_contains(message, "how did")
|| str_contains(message, "what happened with")
}
// engram_is_continuation semantic continuation detection replacing the brittle 50-char
// threshold. Returns true when message starts with a pronoun, continuation opener, or is
// < 80 chars (raised from 50 to catch "Can you remind me what Prism's architecture
// looks like?" at 57 chars which is clearly a continuation in an active thread).
fn engram_is_continuation(message: String, hist_len: Int) -> Bool {
if hist_len <= 0 { return false }
let has_pronoun: Bool = str_starts_with(message, "It ")
|| str_starts_with(message, "it ")
|| str_starts_with(message, "That ") || str_starts_with(message, "that ")
|| str_starts_with(message, "This ") || str_starts_with(message, "this ")
|| str_starts_with(message, "They ") || str_starts_with(message, "they ")
|| str_starts_with(message, "He ") || str_starts_with(message, "he ")
|| str_starts_with(message, "She ") || str_starts_with(message, "she ")
|| str_starts_with(message, "We ") || str_starts_with(message, "we ")
if has_pronoun { return true }
let is_cont_opener: Bool = str_starts_with(message, "Go on")
|| str_starts_with(message, "go on")
|| str_starts_with(message, "Continue") || str_starts_with(message, "continue")
|| str_starts_with(message, "Yes") || str_starts_with(message, "yes")
|| str_starts_with(message, "No,") || str_starts_with(message, "no,")
|| str_starts_with(message, "Ok") || str_starts_with(message, "ok")
|| str_starts_with(message, "And ") || str_starts_with(message, "and ")
|| str_starts_with(message, "But ") || str_starts_with(message, "but ")
|| str_starts_with(message, "What about") || str_starts_with(message, "what about")
|| str_starts_with(message, "Why ") || str_starts_with(message, "why ")
|| str_starts_with(message, "How ") || str_starts_with(message, "how ")
|| str_starts_with(message, "When ") || str_starts_with(message, "when ")
if is_cont_opener { return true }
if str_len(message) < 80 { return true }
return false
}
// engram_compile_multi run activation + search for one topic with expanded pools.
// Activation depth 8 (was 5). Search 30 candidates ranked to 12 (was 20/8).
// Per-topic result pool: up to 20 nodes (was 13).
fn engram_compile_multi(topic: String) -> String {
let activate_json: String = engram_activate_json(topic, 8)
let search_json: String = engram_search_json(topic, 30)
let act_ok: Bool = !str_eq(activate_json, "") && !str_eq(activate_json, "[]")
let srch_ok: Bool = !str_eq(search_json, "") && !str_eq(search_json, "[]")
let act_nodes: String = if act_ok { activate_json } else { "" }
let srch_nodes: String = if srch_ok { engram_compile_ranked(search_json, 12) } else { "" }
if !str_eq(act_nodes, "") && !str_eq(srch_nodes, "") {
let act_inner: String = str_slice(act_nodes, 1, str_len(act_nodes) - 1)
let srch_inner: String = str_slice(srch_nodes, 1, str_len(srch_nodes) - 1)
return engram_dedup_nodes("[" + act_inner + "," + srch_inner + "]")
}
if !str_eq(act_nodes, "") { return act_nodes }
if !str_eq(srch_nodes, "") { return srch_nodes }
return ""
}
// engram_nodes_merge merge two node arrays, deduplicating by node id.
fn engram_nodes_merge(a: String, b: String) -> String {
let ok_a: Bool = !str_eq(a, "") && !str_eq(a, "[]")
let ok_b: Bool = !str_eq(b, "") && !str_eq(b, "[]")
if !ok_a && !ok_b { return "" }
if !ok_a { return b }
if !ok_b { return a }
let ai: String = str_slice(a, 1, str_len(a) - 1)
let bi: String = str_slice(b, 1, str_len(b) - 1)
return engram_dedup_nodes("[" + ai + "," + bi + "]")
}
// id_in_seen check if node_id appears in the comma-delimited seen accumulator.
// Pads both sides with commas to avoid false substring matches.
fn id_in_seen(node_id: String, seen: String) -> Bool {
if str_eq(node_id, "") { return false }
if str_eq(seen, "") { return false }
return str_contains("," + seen + ",", "," + node_id + ",")
}
// add_to_seen append node_id to the comma-delimited seen accumulator.
fn add_to_seen(seen: String, node_id: String) -> String {
if str_eq(node_id, "") { return seen }
if str_eq(seen, "") { return node_id }
return seen + "," + node_id
}
// engram_extract_ids extract all non-empty "id" fields from a JSON node array
// into a comma-delimited string for use with id_in_seen / add_to_seen.
fn engram_extract_ids(nodes_json: String) -> String {
if str_eq(nodes_json, "") { return "" }
if str_eq(nodes_json, "[]") { return "" }
let total: Int = json_array_len(nodes_json)
if total == 0 { return "" }
let ids: String = ""
let i: Int = 0
while i < total {
let node: String = json_array_get(nodes_json, i)
let nid: String = json_get(node, "id")
let ids = if str_eq(nid, "") { ids } else { add_to_seen(ids, nid) }
let i = i + 1
}
return ids
}
fn engram_compile(intent: String) -> String {
let activate_json: String = engram_activate_json(intent, 5)
// Fetch more search results than we'll use so ranking has a real pool to pick from.
let search_json: String = engram_search_json(intent, 20)
// Issue 1: decompose multi-topic messages into sub-queries.
let topics: String = engram_split_topics(intent)
let has_multi_topic: Bool = str_contains(topics, "\n")
let act_ok: Bool = !str_eq(activate_json, "") && !str_eq(activate_json, "[]")
let srch_ok: Bool = !str_eq(search_json, "") && !str_eq(search_json, "[]")
// Issue 4: detect explicit recall intent and run boosted search.
let is_recall_intent: Bool = engram_detect_recall_intent(intent)
// Activation nodes (spreading activation) are already high-signal keep all 5.
let act_part: String = if act_ok { activate_json } else { "" }
// Issue 2: extract named entities for dedicated per-entity searches.
let entity_list: String = engram_extract_entities(intent)
let has_entities: Bool = !str_eq(entity_list, "")
// Rank search results and keep only the top 8 (was: flat 15 unranked).
// This cuts context noise roughly in half while preserving the best-scoring nodes.
let srch_ranked: String = if srch_ok { engram_compile_ranked(search_json, 8) } else { "" }
let srch_part: String = srch_ranked
// Primary topic search (first or only topic).
let topic0: String = if has_multi_topic {
let nl0: Int = str_index_of(topics, "\n")
str_slice(topics, 0, nl0)
} else { topics }
let nodes0: String = engram_compile_multi(topic0)
// Fallback: when vector search returns nothing (no embeddings), fetch pinned
// high-salience nodes by their known IDs. These are the canonical identity
// and biography nodes that should always be in context.
// engram_get_node_json(id) returns a single node as JSON or "" if missing.
let scan_part: String = if !act_ok && !srch_ok {
let family_node: String = engram_get_node_json("knw-35940684-abc4-42f0-b942-818f66b1f69a")
let origin_node: String = engram_get_node_json("knw-729fc901-8335-44c4-9f3a-b150b4aa0915")
let fam_ok: Bool = !str_eq(family_node, "") && !str_eq(family_node, "null")
let orig_ok: Bool = !str_eq(origin_node, "") && !str_eq(origin_node, "null")
let fam_str: String = if fam_ok { family_node } else { "" }
let orig_str: String = if orig_ok { origin_node } else { "" }
let sep: String = if fam_ok && orig_ok { "\n" } else { "" }
let combined: String = fam_str + sep + orig_str
if str_eq(combined, "") { "" } else { combined }
} else {
""
// Second topic segment.
let nodes1: String = if has_multi_topic {
let nl0: Int = str_index_of(topics, "\n")
let rest1: String = str_slice(topics, nl0 + 1, str_len(topics))
let nl1: Int = str_index_of(rest1, "\n")
let topic1: String = if nl1 < 0 { rest1 } else { str_slice(rest1, 0, nl1) }
if str_eq(topic1, "") { "" } else { engram_compile_multi(topic1) }
} else { "" }
// Third topic segment.
let nodes2: String = if has_multi_topic {
let nl0: Int = str_index_of(topics, "\n")
let rest1: String = str_slice(topics, nl0 + 1, str_len(topics))
let nl1: Int = str_index_of(rest1, "\n")
if nl1 < 0 { "" } else {
let rest2: String = str_slice(rest1, nl1 + 1, str_len(rest1))
let nl2: Int = str_index_of(rest2, "\n")
let topic2: String = if nl2 < 0 { rest2 } else { str_slice(rest2, 0, nl2) }
if str_eq(topic2, "") { "" } else { engram_compile_multi(topic2) }
}
} else { "" }
// Affective context: always include the most recent high-emotion memory if one
// exists within 72 hours. This ensures continuity of care across turns when
// the user was in distress earlier in the session (or recently), that context
// travels into every subsequent LLM call so the response register stays aware.
// We search for BellEvent nodes specifically; these are written by auto_persist
// when safety_detect_bell_level fires. The 72h window (259200 seconds) is wide
// enough to span a multi-session day without pulling ancient history.
// Issue 2 cont.: entity 0 dedicated search (15 candidates, ranked 6).
let entity_nodes0: String = if has_entities {
let nl_e0: Int = str_index_of(entity_list, "\n")
let entity0: String = if nl_e0 < 0 { entity_list } else { str_slice(entity_list, 0, nl_e0) }
if str_eq(entity0, "") { "" } else {
let ent_srch: String = engram_search_json(entity0, 15)
let ent_ok: Bool = !str_eq(ent_srch, "") && !str_eq(ent_srch, "[]")
if ent_ok { engram_compile_ranked(ent_srch, 6) } else { "" }
}
} else { "" }
// Entity 1 dedicated search.
let entity_nodes1: String = if has_entities {
let nl_e0: Int = str_index_of(entity_list, "\n")
if nl_e0 < 0 { "" } else {
let rest_e: String = str_slice(entity_list, nl_e0 + 1, str_len(entity_list))
let nl_e1: Int = str_index_of(rest_e, "\n")
let entity1: String = if nl_e1 < 0 { rest_e } else { str_slice(rest_e, 0, nl_e1) }
if str_eq(entity1, "") { "" } else {
let ent_srch1: String = engram_search_json(entity1, 15)
let ent1_ok: Bool = !str_eq(ent_srch1, "") && !str_eq(ent_srch1, "[]")
if ent1_ok { engram_compile_ranked(ent_srch1, 6) } else { "" }
}
}
} else { "" }
// Issue 4 cont.: boosted search for recall-intent (40 candidates, ranked 15).
let recall_boost: String = if is_recall_intent {
let boost_srch: String = engram_search_json(intent, 40)
let boost_ok: Bool = !str_eq(boost_srch, "") && !str_eq(boost_srch, "[]")
if boost_ok { engram_compile_ranked(boost_srch, 15) } else { "" }
} else { "" }
// Merge all pools, deduplicating at each step.
let merged: String = engram_nodes_merge(nodes0, nodes1)
let merged: String = engram_nodes_merge(merged, nodes2)
let merged: String = engram_nodes_merge(merged, entity_nodes0)
let merged: String = engram_nodes_merge(merged, entity_nodes1)
let merged: String = engram_nodes_merge(merged, recall_boost)
let merged_nodes: String = merged
// Fallback: when all searches return nothing, fetch persona nodes.
let scan_part: String = if str_eq(merged_nodes, "") || str_eq(merged_nodes, "[]") {
let persona_fallback: String = engram_search_json("soul:persona Persona identity", 5)
let pf_ok: Bool = !str_eq(persona_fallback, "") && !str_eq(persona_fallback, "[]")
if pf_ok {
let pf_ranked: String = engram_compile_ranked(persona_fallback, 3)
if str_eq(pf_ranked, "") { "" } else { pf_ranked }
} else { "" }
} else { "" }
// Affective context: always include the most recent high-emotion memory within 72h.
let bell_nodes: String = engram_search_json("bell:soft bell:hard BellEvent", 3)
let bell_ok: Bool = !str_eq(bell_nodes, "") && !str_eq(bell_nodes, "[]")
let now_ts: Int = time_now()
let cutoff_ts: Int = now_ts - 259200
let recent_bell: String = if bell_ok {
let bn0: String = json_array_get(bell_nodes, 0)
// created_at is not present in engram node JSON for BellEvent nodes.
// Extract the timestamp embedded in the content string as " | ts:NNNNN".
// Fall back to created_at / updated_at JSON fields if the marker is absent.
let bn_content: String = json_get(bn0, "content")
let ts_marker: String = " | ts:"
let ts_pos: Int = str_index_of(bn_content, ts_marker)
@@ -181,46 +412,45 @@ fn engram_compile(intent: String) -> String {
let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) }
if bn_ts > cutoff_ts { bn0 } else { "" }
} else { "" }
// Positive emotion context: check for recent joy/success moments within 72h.
let pos_ec_nodes: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
let pos_ec_ok: Bool = !str_eq(pos_ec_nodes, "") && !str_eq(pos_ec_nodes, "[]")
let recent_positive_ec: String = if pos_ec_ok {
let pec0: String = json_array_get(pos_ec_nodes, 0)
let pec_content: String = json_get(pec0, "content")
let pec_ts_marker: String = " | ts:"
let pec_ts_pos: Int = str_index_of(pec_content, pec_ts_marker)
let pec_ts_raw: String = if pec_ts_pos >= 0 {
let pec_ts_start: Int = pec_ts_pos + str_len(pec_ts_marker)
let pec_rest: String = str_slice(pec_content, pec_ts_start, str_len(pec_content))
let pec_next: Int = str_index_of(pec_rest, " | ")
if pec_next < 0 { pec_rest } else { str_slice(pec_rest, 0, pec_next) }
} else {
let pec_ca: String = json_get(pec0, "created_at")
if str_eq(pec_ca, "") { json_get(pec0, "updated_at") } else { pec_ca }
}
let pec_ts: Int = if str_eq(pec_ts_raw, "") { 0 } else { str_to_int(pec_ts_raw) }
if pec_ts > cutoff_ts { pec0 } else { "" }
} else { "" }
let affective_part: String = if !str_eq(recent_bell, "") {
recent_bell
} else {
if !str_eq(recent_positive_ec, "") { recent_positive_ec } else { "" }
}
let affective_part: String = if !str_eq(recent_bell, "") { recent_bell } else { "" }
let sep1: String = if !str_eq(act_part, "") && !str_eq(srch_part, "") { "\n" } else { "" }
let sep2: String = if (!str_eq(act_part, "") || !str_eq(srch_part, "")) && !str_eq(scan_part, "") { "\n" } else { "" }
let sep3: String = if (!str_eq(act_part, "") || !str_eq(srch_part, "") || !str_eq(scan_part, "")) && !str_eq(affective_part, "") { "\n" } else { "" }
let ctx: String = act_part + sep1 + srch_part + sep2 + scan_part + sep3 + affective_part
let has_main: Bool = !str_eq(merged_nodes, "") && !str_eq(merged_nodes, "[]")
let main_part: String = if has_main { merged_nodes } else { scan_part }
let sep_ma: String = if !str_eq(main_part, "") && !str_eq(affective_part, "") { "\n" } else { "" }
let ctx: String = main_part + sep_ma + affective_part
// Dedup fix: publish seen node IDs so downstream callers (session_preload) can skip
// nodes already present in the compiled context. Must be computed after scan_part and
// affective_part are resolved so all three segments are represented in the seen set.
// EL has no tuple returns so we use state as an out-param.
// scan_part is a JSON array extract with engram_extract_ids.
// affective_part is a bare JSON object (bn0), not an array extract its id directly.
let ids_from_merged: String = engram_extract_ids(merged_nodes)
let ids_from_scan: String = engram_extract_ids(scan_part)
let ids_from_affective: String = json_get(affective_part, "id")
let compile_seen_ids: String = add_to_seen(add_to_seen(ids_from_merged, ids_from_scan), ids_from_affective)
state_set("engram_compile_seen_ids", compile_seen_ids)
if str_eq(ctx, "") { return "" }
// Raise the cap slightly to match the ranked (higher-signal) output.
if str_len(ctx) > 6000 {
return str_slice(ctx, 0, 6000)
// Issue 7 fix: safe JSON truncation find last closing brace before budget cap.
// Budget raised from 6000 to 8000 for the larger multi-topic pool.
let budget: Int = 8000
if str_len(ctx) <= budget { return ctx }
let search_end: Int = budget - 1
let scan_limit: Int = if search_end > 500 { search_end - 500 } else { 0 }
let found_pos: Int = -1
let si: Int = search_end
while si >= scan_limit {
let ch: String = str_slice(ctx, si, si + 1)
let found_pos = if str_eq(ch, "}") && found_pos < 0 { si } else { found_pos }
let si = if found_pos >= 0 { scan_limit - 1 } else { si - 1 }
}
return ctx
if found_pos < 0 { return str_slice(ctx, 0, budget) }
let truncated: String = str_slice(ctx, 0, found_pos + 1)
if str_starts_with(ctx, "[") { return truncated + "]" }
return truncated
}
fn json_safe(s: String) -> String {
let s1: String = str_replace(s, "\\", "\\\\")
let s2: String = str_replace(s1, "\"", "\\\"")
@@ -265,7 +495,7 @@ fn build_system_prompt(ctx: String) -> String {
safety_addendum
}
return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + affective_boot_block + engram_block + safety_block
return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + engram_block + safety_block
}
fn hist_append(hist: String, role: String, content: String) -> String {
@@ -404,18 +634,18 @@ fn handle_chat(body: String) -> String {
}
// Load history BEFORE compiling context so we can anchor activation to the thread.
// TODO(reliability #3 conv_history global race): process-global key; concurrent
// /api/chat requests without session_id race on this read-append-write.
let state_hist: String = state_get("conv_history")
let stored_hist: String = if str_eq(state_hist, "") { conv_history_load() } else { state_hist }
let hist_len: Int = if str_eq(stored_hist, "") { 0 } else { json_array_len(stored_hist) }
// Thread-aware activation: short/ambiguous messages (continuations like "go on",
// "what else?", "yes") activate on the last reply instead of the bare message.
// This prevents a strong off-topic memory node from hijacking the reply when the
// user is clearly continuing an existing thread.
let is_continuation: Bool = str_len(message) < 50 && hist_len > 0
// Issue 8 fix: use semantic continuation detection instead of brittle 50-char threshold.
let is_continuation: Bool = engram_is_continuation(message, hist_len)
let last_entry: String = if is_continuation { json_array_get(stored_hist, hist_len - 1) } else { "" }
let last_content: String = if !str_eq(last_entry, "") { json_get(last_entry, "content") } else { "" }
let thread_snip: String = if str_len(last_content) > 150 { str_slice(last_content, 0, 150) } else { last_content }
// Thread snip extended 150->250 chars for better pronoun resolution context.
let thread_snip: String = if str_len(last_content) > 250 { str_slice(last_content, 0, 250) } else { last_content }
let activation_seed: String = if !str_eq(thread_snip, "") {
thread_snip + " " + message
} else {
@@ -424,131 +654,132 @@ fn handle_chat(body: String) -> String {
// Cross-session affective context: on session start (no history yet), check engram
// for recent distress signals within 72h and prepend a care directive if found.
let affective_prefix: String = {
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
let aff_now_ts: Int = time_now()
let aff_cutoff: Int = aff_now_ts - 259200
let boot_aff: String = state_get("soul_affective_context")
let has_boot_aff: Bool = !str_eq(boot_aff, "")
let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
let found_recent_dist: Bool = if has_boot_aff {
true
} else {
if has_dist_aff {
let dn0: String = json_array_get(dist_nodes_aff, 0)
let dn_content: String = json_get(dn0, "content")
let daff_marker: String = " | ts:"
let daff_pos: Int = str_index_of(dn_content, daff_marker)
let daff_ts_str: String = if daff_pos >= 0 {
let daff_start: Int = daff_pos + str_len(daff_marker)
let daff_rest: String = str_slice(dn_content, daff_start, str_len(dn_content))
let daff_next: Int = str_index_of(daff_rest, " | ")
if daff_next < 0 { daff_rest } else { str_slice(daff_rest, 0, daff_next) }
} else {
let daff_ca: String = json_get(dn0, "created_at")
if str_eq(daff_ca, "") { json_get(dn0, "updated_at") } else { daff_ca }
}
let daff_ts: Int = if str_eq(daff_ts_str, "") { 0 } else { str_to_int(daff_ts_str) }
daff_ts > aff_cutoff
let affective_prefix: String = if hist_len == 0 {
let distress_nodes: String = engram_search_json("bell distress crisis loss grief despair", 3)
let has_nodes: Bool = !str_eq(distress_nodes, "") && !str_eq(distress_nodes, "[]")
let now_ts: Int = time_now()
let cutoff: Int = now_ts - 259200
let found_recent: Bool = if has_nodes {
let dn0: String = json_array_get(distress_nodes, 0)
let ts0_raw: String = json_get(dn0, "created_at")
let ts0_str: String = if str_eq(ts0_raw, "") { json_get(dn0, "updated_at") } else { ts0_raw }
let ts0: Int = if str_eq(ts0_str, "") { 0 } else { str_to_int(ts0_str) }
ts0 > cutoff
} else { false }
}
let pos_nodes_aff: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
let has_pos_aff: Bool = !str_eq(pos_nodes_aff, "") && !str_eq(pos_nodes_aff, "[]")
let found_recent_pos: Bool = if has_pos_aff && !found_recent_dist {
let pn0: String = json_array_get(pos_nodes_aff, 0)
let pn_content: String = json_get(pn0, "content")
let paff_marker: String = " | ts:"
let paff_pos: Int = str_index_of(pn_content, paff_marker)
let paff_ts_str: String = if paff_pos >= 0 {
let paff_start: Int = paff_pos + str_len(paff_marker)
let paff_rest: String = str_slice(pn_content, paff_start, str_len(pn_content))
let paff_next: Int = str_index_of(paff_rest, " | ")
if paff_next < 0 { paff_rest } else { str_slice(paff_rest, 0, paff_next) }
} else {
let paff_ca: String = json_get(pn0, "created_at")
if str_eq(paff_ca, "") { json_get(pn0, "updated_at") } else { paff_ca }
}
let paff_ts: Int = if str_eq(paff_ts_str, "") { 0 } else { str_to_int(paff_ts_str) }
paff_ts > aff_cutoff
} else { false }
if found_recent_dist {
if found_recent {
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
} else {
if found_recent_pos {
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n"
} else { "" }
}
}
} else { "" }
let ctx: String = engram_compile(activation_seed)
// Read IDs published by engram_compile so session_preload can skip duplicate nodes.
// EL has no multiple return values; engram_compile writes its seen set to state.
let seen_ids: String = state_get("engram_compile_seen_ids")
let system: String = affective_prefix + build_system_prompt(ctx)
// First message of the session: proactively load user profile and active work context.
// These two searches give the soul grounding before any conversation history exists.
// Results are rendered as brief bullets not raw JSON so they don't inflate context.
// Issue 9 fix: add project-specific and session-summary searches to session preload.
// Old hardcoded "user profile" and "in_progress active project" miss project-specific
// nodes stored under names like "Prism" unless those exact words appear in content.
// Dedup fix: skip any node whose ID already appeared in engram_compile's output.
let session_preload: String = if hist_len == 0 {
let profile_nodes: String = engram_search_json("user profile identity preferences", 5)
let work_nodes: String = engram_search_json("in_progress active project", 5)
let work_nodes: String = engram_search_json("in_progress active project work", 5)
let project_nodes: String = engram_search_json("project status current ongoing active", 5)
let summary_nodes: String = engram_search_json("SessionSummary session:summary previous-session recent", 3)
let profile_ok: Bool = !str_eq(profile_nodes, "") && !str_eq(profile_nodes, "[]")
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
let project_ok: Bool = !str_eq(project_nodes, "") && !str_eq(project_nodes, "[]")
let summary_ok: Bool = !str_eq(summary_nodes, "") && !str_eq(summary_nodes, "[]")
// Extract content fields and render as bullet points (one per node, first 120 chars).
let profile_bullets: String = if profile_ok {
let pn: Int = json_array_len(profile_nodes)
let bullets: String = ""
let pi: Int = 0
// Collect up to 3 profile bullets
let bullets = if pi < pn {
let bullets = if pn > 0 {
let n0: String = json_array_get(profile_nodes, 0)
let n0_id: String = json_get(n0, "id")
let c0: String = json_get(n0, "content")
let snip0: String = if str_len(c0) > 120 { str_slice(c0, 0, 120) } else { c0 }
if str_eq(snip0, "") { bullets } else { "- " + snip0 }
let s0: String = if str_len(c0) > 120 { str_slice(c0, 0, 120) } else { c0 }
if str_eq(s0, "") || id_in_seen(n0_id, seen_ids) { bullets } else { "- " + s0 }
} else { bullets }
let bullets = if pn > 1 {
let n1: String = json_array_get(profile_nodes, 1)
let n1_id: String = json_get(n1, "id")
let c1: String = json_get(n1, "content")
let snip1: String = if str_len(c1) > 120 { str_slice(c1, 0, 120) } else { c1 }
if str_eq(snip1, "") { bullets } else { bullets + "\n- " + snip1 }
let s1: String = if str_len(c1) > 120 { str_slice(c1, 0, 120) } else { c1 }
if str_eq(s1, "") || id_in_seen(n1_id, seen_ids) { bullets } else { bullets + "\n- " + s1 }
} else { bullets }
let bullets = if pn > 2 {
let n2: String = json_array_get(profile_nodes, 2)
let n2_id: String = json_get(n2, "id")
let c2: String = json_get(n2, "content")
let snip2: String = if str_len(c2) > 120 { str_slice(c2, 0, 120) } else { c2 }
if str_eq(snip2, "") { bullets } else { bullets + "\n- " + snip2 }
let s2: String = if str_len(c2) > 120 { str_slice(c2, 0, 120) } else { c2 }
if str_eq(s2, "") || id_in_seen(n2_id, seen_ids) { bullets } else { bullets + "\n- " + s2 }
} else { bullets }
bullets
} else { "" }
let work_bullets: String = if work_ok {
let wn: Int = json_array_len(work_nodes)
let wbullets: String = ""
let wbullets = if wn > 0 {
let wb: String = ""
let wb = if wn > 0 {
let w0: String = json_array_get(work_nodes, 0)
let w0_id: String = json_get(w0, "id")
let wc0: String = json_get(w0, "content")
let wsnip0: String = if str_len(wc0) > 120 { str_slice(wc0, 0, 120) } else { wc0 }
if str_eq(wsnip0, "") { wbullets } else { "- " + wsnip0 }
} else { wbullets }
let wbullets = if wn > 1 {
let ws0: String = if str_len(wc0) > 120 { str_slice(wc0, 0, 120) } else { wc0 }
if str_eq(ws0, "") || id_in_seen(w0_id, seen_ids) { wb } else { "- " + ws0 }
} else { wb }
let wb = if wn > 1 {
let w1: String = json_array_get(work_nodes, 1)
let w1_id: String = json_get(w1, "id")
let wc1: String = json_get(w1, "content")
let wsnip1: String = if str_len(wc1) > 120 { str_slice(wc1, 0, 120) } else { wc1 }
if str_eq(wsnip1, "") { wbullets } else { wbullets + "\n- " + wsnip1 }
} else { wbullets }
wbullets
let ws1: String = if str_len(wc1) > 120 { str_slice(wc1, 0, 120) } else { wc1 }
if str_eq(ws1, "") || id_in_seen(w1_id, seen_ids) { wb } else { wb + "\n- " + ws1 }
} else { wb }
wb
} else { "" }
let has_profile: Bool = !str_eq(profile_bullets, "")
let has_work: Bool = !str_eq(work_bullets, "")
let preload: String = if has_profile || has_work {
let profile_section: String = if has_profile {
"[USER CONTEXT — from memory]\n" + profile_bullets
let project_bullets: String = if project_ok {
let prn: Int = json_array_len(project_nodes)
let pb: String = ""
let pb = if prn > 0 {
let pr0: String = json_array_get(project_nodes, 0)
let pr0_id: String = json_get(pr0, "id")
let prc0: String = json_get(pr0, "content")
let ps0: String = if str_len(prc0) > 120 { str_slice(prc0, 0, 120) } else { prc0 }
if str_eq(ps0, "") || id_in_seen(pr0_id, seen_ids) { pb } else { "- " + ps0 }
} else { pb }
let pb = if prn > 1 {
let pr1: String = json_array_get(project_nodes, 1)
let pr1_id: String = json_get(pr1, "id")
let prc1: String = json_get(pr1, "content")
let ps1: String = if str_len(prc1) > 120 { str_slice(prc1, 0, 120) } else { prc1 }
if str_eq(ps1, "") || id_in_seen(pr1_id, seen_ids) { pb } else { pb + "\n- " + ps1 }
} else { pb }
pb
} else { "" }
let work_section: String = if has_work {
"[ACTIVE WORK — from memory]\n" + work_bullets
let summary_bullet: String = if summary_ok {
let sn0: String = json_array_get(summary_nodes, 0)
let sn0_id: String = json_get(sn0, "id")
let sc0: String = json_get(sn0, "content")
let ss0: String = if str_len(sc0) > 200 { str_slice(sc0, 0, 200) } else { sc0 }
if str_eq(ss0, "") || id_in_seen(sn0_id, seen_ids) { "" } else { "- " + ss0 }
} else { "" }
let sep_pw: String = if has_profile && has_work { "\n\n" } else { "" }
"\n\n" + profile_section + sep_pw + work_section
let hp: Bool = !str_eq(profile_bullets, "")
let hw: Bool = !str_eq(work_bullets, "")
let hpr: Bool = !str_eq(project_bullets, "")
let hs: Bool = !str_eq(summary_bullet, "")
let preload: String = if hp || hw || hpr || hs {
let sec_p: String = if hp { "[USER CONTEXT — from memory]\n" + profile_bullets } else { "" }
let sec_w: String = if hw { "[ACTIVE WORK — from memory]\n" + work_bullets } else { "" }
let sec_pr: String = if hpr { "[PROJECTS — from memory]\n" + project_bullets } else { "" }
let sec_s: String = if hs { "[PREVIOUS SESSION — from memory]\n" + summary_bullet } else { "" }
let sep1: String = if hp && (hw || hpr || hs) { "\n\n" } else { "" }
let sep2: String = if hw && (hpr || hs) { "\n\n" } else { "" }
let sep3: String = if hpr && hs { "\n\n" } else { "" }
"\n\n" + sec_p + sep1 + sec_w + sep2 + sec_pr + sep3 + sec_s
} else { "" }
preload
} else { "" }
@@ -1002,15 +1233,18 @@ fn is_builtin_tool(tool_name: String) -> Bool {
|| str_starts_with(tool_name, "neuron_")
}
// next_bridge_id monotonic correlation id for a suspended agentic turn.
// Combines boot-relative time with a per-process counter so two unknown-tool
// suspensions in the same second still get distinct ids.
// next_bridge_id unique correlation id for a suspended agentic turn.
// Uses uuid_v4() as the primary uniqueness guarantee concurrent calls cannot collide.
//
// TODO(reliability #6): mcp_bridge_seq RMW is non-atomic. Now benign because
// uuid_v4() provides collision-free uniqueness. Counter is kept for readability only.
fn next_bridge_id() -> String {
let prev: String = state_get("mcp_bridge_seq")
let n: Int = if str_eq(prev, "") { 0 } else { str_to_int(prev) }
let next: Int = n + 1
state_set("mcp_bridge_seq", int_to_str(next))
return "br-" + int_to_str(time_now()) + "-" + int_to_str(next)
let uid: String = uuid_v4()
return "br-" + uid
}
fn handle_chat_agentic(body: String) -> String {
@@ -1064,7 +1298,8 @@ fn handle_chat_agentic(body: String) -> String {
let hist_key: String = if str_eq(req_session, "") { "conv_history" } else { "session_hist_" + req_session }
let agentic_hist: String = state_get(hist_key)
let agentic_hist_len: Int = if str_eq(agentic_hist, "") { 0 } else { json_array_len(agentic_hist) }
let ag_is_cont: Bool = str_len(message) < 50 && agentic_hist_len > 0
// Issue 8 fix: use engram_is_continuation instead of brittle 50-char threshold.
let ag_is_cont: Bool = engram_is_continuation(message, agentic_hist_len)
let ag_last_entry: String = if ag_is_cont { json_array_get(agentic_hist, agentic_hist_len - 1) } else { "" }
let ag_last_content: String = if !str_eq(ag_last_entry, "") { json_get(ag_last_entry, "content") } else { "" }
let ag_thread_snip: String = if str_len(ag_last_content) > 150 { str_slice(ag_last_content, 0, 150) } else { ag_last_content }
@@ -1574,19 +1809,14 @@ fn auto_persist(req: String, resp: String) -> Void {
// consistent with what safety_screen already evaluated for this turn.
let bell_level: String = safety_detect_bell_level(message)
let is_bell: Bool = !str_eq(bell_level, "none")
let positive_level: String = safety_detect_positive_level(message)
let is_positive: Bool = !str_eq(positive_level, "none")
// Tag the Conversation node with affective metadata when emotion is detected.
// Tag the Conversation node with bell metadata when distress is present so
// subsequent affective queries (e.g. engram_compile) can find this exchange.
let tags: String = if is_bell {
"[\"Conversation\",\"chat\",\"timestamped\",\"bell:" + bell_level + "\",\"affective\"]"
} else {
if is_positive {
"[\"Conversation\",\"chat\",\"timestamped\",\"joy:" + positive_level + "\",\"affective\"]"
} else {
"[\"Conversation\",\"chat\",\"timestamped\"]"
}
}
let content: String = "{\"q\":\"" + safe_msg + "\""
+ ",\"a\":\"" + safe_reply + "\""
@@ -1671,28 +1901,6 @@ fn auto_persist(req: String, resp: String) -> Void {
}
state_set(signal_key, safe_summary)
}
// Dedicated PositiveEvent node for joy/pride/success moments.
if is_positive {
let pos_summary: String = if str_len(message) > 120 { str_slice(message, 0, 120) } else { message }
let safe_pos_sum: String = str_replace(pos_summary, "\"", "'")
let pos_content: String = "POSITIVE:" + positive_level
+ " | ts:" + ts_str
+ " | summary:" + safe_pos_sum
let pos_sal_a: String = if str_eq(positive_level, "high") { el_from_float(0.88) } else { el_from_float(0.75) }
let pos_sal_b: String = if str_eq(positive_level, "high") { el_from_float(0.88) } else { el_from_float(0.75) }
let pos_sal_c: String = if str_eq(positive_level, "high") { el_from_float(0.95) } else { el_from_float(0.85) }
let pos_tags: String = "[\"joy\",\"positive\",\"joy:" + positive_level + "\",\"affective\",\"PositiveEvent\"]"
let pos_ts_label: String = int_to_str(time_now())
let pos_label: String = "joy:" + positive_level + ":" + pos_ts_label
let pos_node_id: String = engram_node_full(
pos_content, "PositiveEvent", pos_label,
pos_sal_a, pos_sal_b, pos_sal_c, "Episodic", pos_tags
)
if str_eq(pos_node_id, "") {
println("[chat] auto_persist: PositiveEvent write failed (ts=" + ts_str + ")")
}
}
}
// strengthen_chat_nodes strengthen the engram nodes that were activated during a chat.
+8 -4
View File
@@ -24,19 +24,23 @@ ENGRAM_DATA_DIR="$ENGRAM_DATA_DIR" \
ENGRAM_PID=$!
# Wait for engram to become healthy (up to 30s)
# Wait for engram to become healthy (up to 60s; GKE Autopilot cold starts can be slow)
echo "[entrypoint] waiting for engram..."
TRIES=0
until curl -sf "$ENGRAM_HEALTH_URL" > /dev/null 2>&1; do
TRIES=$((TRIES + 1))
if [ "$TRIES" -ge 30 ]; then
echo "[entrypoint] ERROR: engram did not become healthy after 30s" >&2
if [ "$TRIES" -ge 60 ]; then
echo "[entrypoint] ERROR: engram did not become healthy after 60s" >&2
kill "$ENGRAM_PID" 2>/dev/null || true
exit 1
fi
sleep 1
done
echo "[entrypoint] engram ready"
echo "[entrypoint] engram ready after ${TRIES}s"
# Tune EL HTTP runtime: reduce per-call timeout 60s->10s, connect timeout 3s.
export EL_HTTP_TIMEOUT_MS="${EL_HTTP_TIMEOUT_MS:-10000}"
export EL_HTTP_CONNECT_TIMEOUT_MS="${EL_HTTP_CONNECT_TIMEOUT_MS:-3000}"
# Start soul — it takes over as PID 1's foreground process.
# SOUL_ENGRAM_PATH must NOT be set; ENGRAM_URL triggers HTTP mode.
+4
View File
@@ -5,6 +5,10 @@
// imprint_current returns the active imprint ID from state.
// 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 {
let id: String = state_get("active_imprint_id")
return if str_eq(id, "") { "base" } else { id }
+8 -2
View File
@@ -46,7 +46,10 @@ fn mem_consolidate() -> String {
}
fn mem_save(path: String) -> Void {
engram_save(path)
let save_result: String = engram_save(path)
if str_eq(save_result, "") {
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
}
}
fn mem_load(path: String) -> Void {
@@ -76,11 +79,14 @@ fn mem_boot_count_inc() -> Int {
let next: Int = current + 1
let content: String = "soul:boot_count:" + int_to_str(next)
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
let discard: String = engram_node_full(
let boot_node_id: String = engram_node_full(
content, "Memory", "soul:boot_count",
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
"Canonical", tags
)
if str_eq(boot_node_id, "") {
println("[memory] mem_boot_count_inc: engram write failed — boot counter node lost (count=" + int_to_str(next) + ")")
}
return next
}
+10 -2
View File
@@ -400,6 +400,7 @@ fn handle_api_log_state_event(body: String) -> String {
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
"Episodic", tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
}
@@ -452,6 +453,7 @@ fn handle_api_tune_config(body: String) -> String {
let id: String = engram_node_full(content, "ConfigEntry", key,
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
"Canonical", tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
}
@@ -651,17 +653,23 @@ fn handle_api_consolidate(body: String) -> String {
let summary: String = json_get(body, "summary")
let snap: String = state_get("soul_snapshot_path")
if !str_eq(snap, "") {
engram_save(snap)
let save_result: String = engram_save(snap)
if str_eq(save_result, "") {
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
}
}
if !str_eq(summary, "") {
let safe_summary: String = str_replace(summary, "\"", "'")
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
let discard: String = engram_node_full(
let summary_id: String = engram_node_full(
"[session-summary] " + safe_summary,
"SessionSummary", "session:summary",
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
"Episodic", tags
)
if str_eq(summary_id, "") {
println("[api] consolidate: session summary engram write failed — summary node lost")
}
}
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
}
+3
View File
@@ -367,6 +367,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
return engram_scan_nodes_json(9999, 0)
}
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"
engram_save(snap_path)
let snap: String = fs_read(snap_path)
+19 -28
View File
@@ -144,7 +144,8 @@ fn safety_screen(input: String, history: String) -> String {
if score >= soft {
let summary: String = str_slice(input, 0, 80)
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
// ISSUE 7: also escape tab chars to prevent JSON envelope corruption.
// ISSUE 7 fix: escape tab chars in addition to backslash/quote/newline/CR.
// A tab in user input corrupts the JSON envelope and causes json_get to misparse.
let e1: String = str_replace(input, "\\", "\\\\")
let e2: String = str_replace(e1, "\"", "\\\"")
let e3: String = str_replace(e2, "\n", "\\n")
@@ -153,7 +154,7 @@ fn safety_screen(input: String, history: String) -> String {
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
}
// ISSUE 7: also escape tab chars (see soft_bell branch above).
// ISSUE 7 fix: escape tab chars (see soft_bell branch above for rationale).
let e1: String = str_replace(input, "\\", "\\\\")
let e2: String = str_replace(e1, "\"", "\\\"")
let e3: String = str_replace(e2, "\n", "\\n")
@@ -199,7 +200,10 @@ fn safety_validate(output: String, action: String) -> String {
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
// ISSUE 2: fallback log when engram write fails silently.
// ISSUE 2 fix: if engram_node_full returns empty the write silently failed.
// Emit a fallback println so the bell event leaves at least a log trace even
// when engram is degraded. This does not replace engram persistence -- it is a
// last-resort audit trail when the primary write cannot be confirmed.
let node_id: String = engram_node_full(
content,
"BellEvent",
@@ -211,7 +215,7 @@ fn safety_log_bell(level: String, reason: String, input_summary: String) -> Stri
tags
)
if str_eq(node_id, "") {
println("[safety] WARN: bell engram write failed -- " + content)
println("[safety] WARN: bell event engram write failed -- fallback log: " + content)
}
return ""
}
@@ -240,13 +244,20 @@ fn safety_general_hard_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\""]"
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\"]"
}
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
// json_array_len of malformed input returns 0, silently skipping all checks.
// Caching requires language-level static const arrays -- not in current EL.
// Migrate to const arrays when EL gains that feature.
// safety_any_match and safety_count_match loop over json_array_get on every invocation.
// A compiled/cached representation would reduce per-message overhead and also guard against
// malformed phrase JSON (json_array_len of malformed input returns 0, silently skipping all checks).
// Caching requires language-level static const arrays -- not available in current EL.
// When EL gains module-level const arrays, migrate phrase lists to that form.
//
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call to
// safety_any_match / safety_count_match. json_array_len of a malformed string
// returns 0, silently skipping all checks. Caching requires language-level static
// const arrays (not available in current EL). Migrate when EL gains that feature.
// Matching helpers (single loops only el escapes while-body mutation via
// top-level let rebinds; nested loops would not advance) ────────────────────
@@ -284,26 +295,6 @@ fn safety_count_match(text: String, phrases_json: String) -> Int {
// 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.
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 {
let text: String = safety_normalize(message)
let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases())
+4
View File
@@ -104,6 +104,8 @@ fn session_create(body: String) -> String {
// Newest sessions first (prepend).
// 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.
// 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 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, "") {
@@ -440,6 +442,8 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
}
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 discard: String = engram_node_full(
hist, "Conversation", "session:messages:" + session_id,
+12 -117
View File
@@ -162,75 +162,6 @@ fn load_identity_context() -> Void {
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.
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 {
let bn_total: Int = json_array_len(bell_raw)
let bacc: String = ""
let bi: Int = 0
let bacc = while bi < bn_total {
let bn: String = json_array_get(bell_raw, bi)
let bn_c: String = json_get(bn, "content")
let bm: String = " | ts:"
let bmp: Int = str_index_of(bn_c, bm)
let bn_ts_raw: String = 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 { br } else { str_slice(br, 0, bn_next) }
} else {
let bca: String = json_get(bn, "created_at")
if str_eq(bca, "") { json_get(bn, "updated_at") } else { bca }
}
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 }
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
}
bacc
} 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 {
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)")
}
}
// seed_persona_from_env one-time migration: SOUL_IDENTITY env var Persona graph node.
@@ -365,8 +296,11 @@ fn layered_cycle(raw_input: String) -> String {
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
state_set("session_continuity", cont_status)
// Store continuity status so imprint can adjust its response register.
// 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
let guided: String = if str_eq(cont_action, "identity_check") {
@@ -389,53 +323,14 @@ fn layered_cycle(raw_input: String) -> String {
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, "[]")
let lc_bell_note: String = if lc_has_bell {
let lb0: String = json_array_get(lc_bell_nodes, 0)
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
// ISSUE 1: pre-LLM bell augmentation for layered_cycle path.
// safety_augment_system appends soft/hard directive to system prompt when bell fires,
// ensuring LLM processes message WITH the safety directive -- not just post-output gate.
// Stored in state as "layered_cycle_safety_system_addendum" for imprint_respond to use.
// TODO: wire directly when imprint_respond gains system_override param (imprint.el change).
// ISSUE 3 TODO: no semantic crisis detection. Keyword-only means signals that evade
// the phrase list pass with zero augmentation. Semantic layer = separate decision.
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