Compare commits

..

1 Commits

Author SHA1 Message Date
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
4 changed files with 336 additions and 564 deletions
+186 -533
View File
@@ -48,474 +48,72 @@ fn engram_score_node(node_json: String) -> Int {
return salience_100 * importance_100 * recency_100 / 10000 return salience_100 * importance_100 * recency_100 / 10000
} }
// engram_compile_ranked build a ranked list of nodes, best-first by score. // engram_compile_ranked build a context string from a JSON array of node objects,
// Fix (Issue #11): uses "|N|" index tracking instead of _sel_N JSON mutation, // ordered best-first by score. Only nodes above a minimum score (25 = salience 0.5 *
// which leaked sentinel fields into the node objects passed to the LLM. // importance 0.5 * recency 1.0) are included; the rest are noise. Returns at most
// Threshold lowered to 15 to include moderately-relevant older nodes. // max_nodes entries concatenated as JSON array text. Because el has no sort primitive,
// we do a single selection pass picking the top N by linear scan (N=10 cap).
fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String { fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
if str_eq(nodes_json, "") { return "" } if str_eq(nodes_json, "") { return "" }
if str_eq(nodes_json, "[]") { return "" } if str_eq(nodes_json, "[]") { return "" }
let total: Int = json_array_len(nodes_json) let total: Int = json_array_len(nodes_json)
if total == 0 { return "" } if total == 0 { return "" }
// selected_indices is a pipe-delimited string of chosen integer indices, e.g. "|2|7|". // Two-pass: first pass finds the top `max_nodes` by score via selection.
// No sentinel fields are injected into the node JSON the nodes stay clean. // We track selected node indices and their scores to avoid duplicate picks.
let selected_indices: String = "" let selected: String = "" // comma-sep JSON snippets for chosen nodes
let selected_nodes: String = "" let selected_count: Int = 0
let pass: Int = 0 let pass: Int = 0
while pass < max_nodes && pass < total { while pass < max_nodes && pass < total {
// Find the unselected node with the highest score
let best_idx: Int = -1 let best_idx: Int = -1
let best_score: Int = -1 let best_score: Int = -1
let ci: Int = 0 let ci: Int = 0
while ci < total { while ci < total {
let node: String = json_array_get(nodes_json, ci) let node: String = json_array_get(nodes_json, ci)
let score: Int = engram_score_node(node) let score: Int = engram_score_node(node)
// Threshold lowered from 25 to 15: includes moderately-relevant older nodes. // Only include reasonably relevant nodes (threshold=25)
// A 3-week-old node with salience 0.6 and importance 0.6 scores ~18. let above_thresh: Bool = score >= 25
let above_thresh: Bool = score >= 15 // Check this index wasn't already selected (sentinel: look for idx marker)
// Check this index wasn't already selected using the index string. let idx_marker: String = "\"_sel_" + int_to_str(ci) + "\""
let idx_marker: String = "|" + int_to_str(ci) + "|" let already_picked: Bool = str_contains(selected, idx_marker)
let already_picked: Bool = str_contains(selected_indices, idx_marker)
let is_better: Bool = score > best_score && above_thresh && !already_picked let is_better: Bool = score > best_score && above_thresh && !already_picked
let best_score = if is_better { score } else { best_score } let best_score = if is_better { score } else { best_score }
let best_idx = if is_better { ci } else { best_idx } let best_idx = if is_better { ci } else { best_idx }
let ci = ci + 1 let ci = ci + 1
} }
// No more qualifying nodes
if best_idx < 0 { if best_idx < 0 {
let pass = total // break let pass = total // break
} else { } else {
let chosen: String = json_array_get(nodes_json, best_idx) let chosen: String = json_array_get(nodes_json, best_idx)
let sep: String = if str_eq(selected_nodes, "") { "" } else { "," } let sep: String = if str_eq(selected, "") { "" } else { "," }
let selected_nodes = selected_nodes + sep + chosen // Append the index sentinel inline so already_picked checks work
let selected_indices = selected_indices + "|" + int_to_str(best_idx) + "|" let selected = selected + sep + "{\"_sel_" + int_to_str(best_idx) + "\":1," + str_slice(chosen, 1, str_len(chosen) - 1) + "}"
let selected_count = selected_count + 1
} }
let pass = pass + 1 let pass = pass + 1
} }
if str_eq(selected_nodes, "") { return "" } if str_eq(selected, "") { return "" }
return "[" + selected_nodes + "]" // Strip the _sel_N sentinel fields that were used for duplicate-detection bookkeeping.
} // The sentinels have the form "\"_sel_N\":1," (trailing comma, space before next key).
// We injected them as the first field in each object, so the pattern is predictable.
// engram_render_node render a single engram node JSON object as a human-readable // Because el has no regex, remove up to 10 possible sentinel variants by literal replace.
// bullet line for inclusion in the system prompt. Format: - [TYPE age sal] content let clean: String = "[" + selected + "]"
// Fix (Issue #3, #4): passes context as prose bullets instead of raw JSON objects, let c0: String = str_replace(clean, "\"_sel_0\":1,", "")
// which are opaque to the LLM and waste token budget on field names. let c1: String = str_replace(c0, "\"_sel_1\":1,", "")
fn engram_render_node(node_json: String) -> String { let c2: String = str_replace(c1, "\"_sel_2\":1,", "")
if str_eq(node_json, "") { return "" } let c3: String = str_replace(c2, "\"_sel_3\":1,", "")
let content: String = json_get(node_json, "content") let c4: String = str_replace(c3, "\"_sel_4\":1,", "")
if str_eq(content, "") { return "" } let c5: String = str_replace(c4, "\"_sel_5\":1,", "")
let node_type: String = json_get(node_json, "node_type") let c6: String = str_replace(c5, "\"_sel_6\":1,", "")
let type_label: String = if str_eq(node_type, "") { "mem" } else { node_type } let c7: String = str_replace(c6, "\"_sel_7\":1,", "")
let now_ts: Int = time_now() let c8: String = str_replace(c7, "\"_sel_8\":1,", "")
let created_str: String = json_get(node_json, "created_at") let c9: String = str_replace(c8, "\"_sel_9\":1,", "")
let updated_str: String = json_get(node_json, "updated_at") return c9
let ts_raw: String = if str_eq(created_str, "") { updated_str } else { created_str }
let age_label: String = if str_eq(ts_raw, "") { "" } else {
let node_ts: Int = str_to_int(ts_raw)
let age_secs: Int = now_ts - node_ts
let age_days: Int = if age_secs < 0 { 0 } else { age_secs / 86400 }
if age_days == 0 { "today" } else {
if age_days > 30 { "old" } else { int_to_str(age_days) + "d" }
}
}
let salience_str: String = json_get(node_json, "salience")
let sal_100: Int = if str_eq(salience_str, "") { 0 } else {
let s: Int = str_to_int(str_replace(salience_str, ".", ""))
if s > 100 { 100 } else { if s < 0 { 0 } else { s } }
}
let salience_hint: String = if str_eq(salience_str, "") { "" } else {
if sal_100 >= 80 { "high" } else { if sal_100 >= 50 { "med" } else { "low" } }
}
let ann_inner: String = type_label
let ann_inner = if str_eq(age_label, "") { ann_inner } else { ann_inner + " " + age_label }
let ann_inner = if str_eq(salience_hint, "") { ann_inner } else { ann_inner + " " + salience_hint }
let ann: String = "[" + ann_inner + "]"
let snip: String = if str_len(content) > 200 { str_slice(content, 0, 200) } else { content }
return "- " + ann + " " + snip
}
// engram_render_nodes render a JSON array of engram nodes as newline-joined
// prose bullet lines. Returns "" when input is empty.
// Fix (Issue #3): called by build_system_prompt to convert raw JSON ctx to
// human-readable bullets before injecting into the LLM system prompt.
fn engram_render_nodes(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 result: String = ""
let i: Int = 0
while i < total {
let node: String = json_array_get(nodes_json, i)
let line: String = engram_render_node(node)
let result = if str_eq(line, "") { result } else {
if str_eq(result, "") { line } else { result + "\n" + line }
}
let i = i + 1
}
return result
}
// engram_render_ctx render the ctx string returned by engram_compile as prose bullets.
// ctx may be a JSON array "[...]", a single object "{...}", or up to two such segments
// joined by "\n". We handle the three common shapes produced by engram_compile:
// 1. single JSON array -> engram_render_nodes
// 2. single JSON object -> engram_render_node
// 3. two segments sep by "\n" -> render each half individually and join
// Fix (Issue #3): called by build_system_prompt so the LLM receives human-readable
// prose bullets instead of raw JSON field blobs.
fn engram_render_ctx(ctx: String) -> String {
if str_eq(ctx, "") { return "" }
// Single JSON array.
if str_starts_with(ctx, "[") {
let nl: Int = str_index_of(ctx, "\n")
if nl < 0 {
// Whole ctx is one array.
let r: String = engram_render_nodes(ctx)
if !str_eq(r, "") { return r }
return ""
}
// First segment is an array; try to render it and the rest separately.
let part1: String = str_slice(ctx, 0, nl)
let part2: String = str_slice(ctx, nl + 1, str_len(ctx))
let r1: String = engram_render_nodes(part1)
let r2: String = if str_starts_with(part2, "[") {
engram_render_nodes(part2)
} else {
if str_starts_with(part2, "{") { engram_render_node(part2) } else { "" }
}
if str_eq(r1, "") { return r2 }
if str_eq(r2, "") { return r1 }
return r1 + "\n" + r2
}
// Single JSON object (e.g. affective_part node when it's the only result).
if str_starts_with(ctx, "{") {
let nl: Int = str_index_of(ctx, "\n")
if nl < 0 {
let r: String = engram_render_node(ctx)
if !str_eq(r, "") { return r }
return ""
}
let part1: String = str_slice(ctx, 0, nl)
let part2: String = str_slice(ctx, nl + 1, str_len(ctx))
let r1: String = engram_render_node(part1)
let r2: String = if str_starts_with(part2, "[") {
engram_render_nodes(part2)
} else {
if str_starts_with(part2, "{") { engram_render_node(part2) } else { "" }
}
if str_eq(r1, "") { return r2 }
if str_eq(r2, "") { return r1 }
return r1 + "\n" + r2
}
// Fallback: ctx is in an unexpected format; return as-is.
return ctx
}
// is_followup_phrase returns true when the message is a recognized follow-up
// reference that should anchor recall to the prior user topic rather than stand alone.
// Used by build_activation_seed to choose the right enrichment strategy.
fn is_followup_phrase(msg: String) -> Bool {
if str_contains(msg, "tell me more") { return true }
if str_contains(msg, "elaborate") { return true }
if str_contains(msg, "go on") { return true }
if str_contains(msg, "what about that") { return true }
if str_contains(msg, "what else") { return true }
if str_contains(msg, "keep going") { return true }
if str_contains(msg, "continue") { return true }
if str_contains(msg, "more detail") { return true }
if str_contains(msg, "last part") { return true }
if str_contains(msg, "say more") { return true }
if str_eq(msg, "ok") { return true }
if str_eq(msg, "yes") { return true }
if str_eq(msg, "yeah") { return true }
if str_eq(msg, "and?") { return true }
if str_eq(msg, "so?") { return true }
return false
}
// is_genuine_continuation returns true when a short message is a contextual
// follow-up rather than a new topic.
// Issue 4 fix: the prior heuristic only checked for mid-string capitals, which
// fails for all-lowercase new-topic queries like "what is rust?" (14 chars) or
// "explain quantum computing" (26 chars). Added question-word prefix detection
// that fires BEFORE the length check: any message starting with a question word
// (what/how/why/when/where/who/which/is/can/could/does/do) introduces a new
// topic and is never a continuation, regardless of length.
fn is_genuine_continuation(msg: String, hist_len: Int) -> Bool {
if hist_len == 0 { return false }
if str_len(msg) == 0 { return false }
if is_followup_phrase(msg) { return true }
// Question-word prefix: messages starting with these introduce new topics.
// Check before the length heuristic so short new-topic questions escape.
let is_question_start: Bool = str_starts_with(msg, "what ")
|| str_starts_with(msg, "What ")
|| str_starts_with(msg, "how ") || str_starts_with(msg, "How ")
|| str_starts_with(msg, "why ") || str_starts_with(msg, "Why ")
|| str_starts_with(msg, "when ") || str_starts_with(msg, "When ")
|| str_starts_with(msg, "where ") || str_starts_with(msg, "Where ")
|| str_starts_with(msg, "who ") || str_starts_with(msg, "Who ")
|| str_starts_with(msg, "which ") || str_starts_with(msg, "Which ")
|| str_starts_with(msg, "is ") || str_starts_with(msg, "Is ")
|| str_starts_with(msg, "can ") || str_starts_with(msg, "Can ")
|| str_starts_with(msg, "could ") || str_starts_with(msg, "Could ")
|| str_starts_with(msg, "does ") || str_starts_with(msg, "Does ")
|| str_starts_with(msg, "do ") || str_starts_with(msg, "Do ")
|| str_starts_with(msg, "explain ") || str_starts_with(msg, "Explain ")
|| str_starts_with(msg, "describe ") || str_starts_with(msg, "Describe ")
|| str_starts_with(msg, "define ") || str_starts_with(msg, "Define ")
if is_question_start { return false }
// Long messages (50+ chars) typically introduce new topics.
if str_len(msg) >= 50 { return false }
// Short messages with a mid-string capital are likely named-concept queries
// (e.g. "tell me about Rust", "what about AWS") treat as new topic.
let rest: String = str_slice(msg, 1, str_len(msg))
let has_mid_capital: Bool = false
let has_mid_capital = has_mid_capital || str_contains(rest, " A")
let has_mid_capital = has_mid_capital || str_contains(rest, " B")
let has_mid_capital = has_mid_capital || str_contains(rest, " C")
let has_mid_capital = has_mid_capital || str_contains(rest, " D")
let has_mid_capital = has_mid_capital || str_contains(rest, " E")
let has_mid_capital = has_mid_capital || str_contains(rest, " F")
let has_mid_capital = has_mid_capital || str_contains(rest, " G")
let has_mid_capital = has_mid_capital || str_contains(rest, " H")
let has_mid_capital = has_mid_capital || str_contains(rest, " I")
let has_mid_capital = has_mid_capital || str_contains(rest, " J")
let has_mid_capital = has_mid_capital || str_contains(rest, " K")
let has_mid_capital = has_mid_capital || str_contains(rest, " L")
let has_mid_capital = has_mid_capital || str_contains(rest, " M")
let has_mid_capital = has_mid_capital || str_contains(rest, " N")
let has_mid_capital = has_mid_capital || str_contains(rest, " O")
let has_mid_capital = has_mid_capital || str_contains(rest, " P")
let has_mid_capital = has_mid_capital || str_contains(rest, " Q")
let has_mid_capital = has_mid_capital || str_contains(rest, " R")
let has_mid_capital = has_mid_capital || str_contains(rest, " S")
let has_mid_capital = has_mid_capital || str_contains(rest, " T")
let has_mid_capital = has_mid_capital || str_contains(rest, " U")
let has_mid_capital = has_mid_capital || str_contains(rest, " V")
let has_mid_capital = has_mid_capital || str_contains(rest, " W")
let has_mid_capital = has_mid_capital || str_contains(rest, " X")
let has_mid_capital = has_mid_capital || str_contains(rest, " Y")
let has_mid_capital = has_mid_capital || str_contains(rest, " Z")
if has_mid_capital { return false }
return true
}
// topic_snip_from_entry extract the most salient snippet from a history entry's
// content. Fixes Issue 9: takes the TAIL (last 200 chars) then trims to the last
// sentence boundary, so named concepts introduced near the end are captured.
fn topic_snip_from_entry(content: String) -> String {
let clen: Int = str_len(content)
if clen <= 200 { return content }
let tail: String = str_slice(content, clen - 200, clen)
let last_boundary: Int = -1
let si: Int = 0
let tail_len: Int = str_len(tail)
while si < tail_len - 1 {
let ch2: String = str_slice(tail, si, si + 2)
let is_boundary: Bool = str_eq(ch2, ". ") || str_eq(ch2, ".\n")
let last_boundary = if is_boundary { si } else { last_boundary }
let si = si + 1
}
let clean_tail: String = if last_boundary >= 0 {
str_slice(tail, last_boundary + 2, tail_len)
} else { tail }
if str_len(clean_tail) > 150 { return str_slice(clean_tail, 0, 150) }
return clean_tail
}
// multi_turn_topic build a combined topic string from recent user turns in history.
// Fixes Issue 10: pulls up to 3 prior user turns into the seed so earlier
// high-salience nodes from the thread are re-queried.
fn multi_turn_topic(hist: String, hist_len: Int) -> String {
if hist_len == 0 { return "" }
let topic: String = ""
let collected: Int = 0
let idx: Int = hist_len - 1
while idx >= 0 && collected < 3 {
let entry: String = json_array_get(hist, idx)
let role: String = json_get(entry, "role")
let content: String = json_get(entry, "content")
let is_user: Bool = str_eq(role, "user")
let snip: String = if str_len(content) > 100 { str_slice(content, 0, 100) } else { content }
let topic = if is_user && !str_eq(snip, "") {
if str_eq(topic, "") { snip } else { snip + " " + topic }
} else { topic }
let collected = if is_user { collected + 1 } else { collected }
let idx = idx - 1
}
if str_len(topic) > 300 { return str_slice(topic, 0, 300) }
return topic
}
// distill_transcript extract salient content from a multi-turn transcript.
// Fixes Issue 6: a full transcript produces a diffuse embedding query.
// Strategy: last 150 chars (recency) + any question in last 500 chars. Cap 250.
fn distill_transcript(transcript: String) -> String {
if str_len(transcript) <= 250 { return transcript }
let tlen: Int = str_len(transcript)
let tail_start: Int = if tlen > 500 { tlen - 500 } else { 0 }
let tail: String = str_slice(transcript, tail_start, tlen)
let tail_len: Int = str_len(tail)
let q_pos: Int = -1
let qi: Int = 0
while qi < tail_len {
let qch: String = str_slice(tail, qi, qi + 1)
let q_pos = if str_eq(qch, "?") { qi } else { q_pos }
let qi = qi + 1
}
let q_context: String = if q_pos > 0 {
let q_start: Int = if q_pos > 100 { q_pos - 100 } else { 0 }
str_slice(tail, q_start, q_pos + 1)
} else { "" }
let recency_seed: String = if tail_len > 150 {
str_slice(tail, tail_len - 150, tail_len)
} else { tail }
let combined: String = if str_eq(q_context, "") {
recency_seed
} else {
if str_contains(recency_seed, q_context) { recency_seed }
else { q_context + " " + recency_seed }
}
if str_len(combined) > 250 {
return str_slice(combined, str_len(combined) - 250, str_len(combined))
}
return combined
}
// build_activation_seed construct an enriched activation seed from the current
// message and conversation history. Central fix for Issues 1-3, 8-10.
fn build_activation_seed(message: String, hist: String, hist_len: Int) -> String {
if hist_len == 0 { return message }
let is_cont: Bool = is_genuine_continuation(message, hist_len)
if !is_cont {
let multi_topic: String = multi_turn_topic(hist, hist_len)
if str_eq(multi_topic, "") { return message }
let blended: String = message + " " + multi_topic
if str_len(blended) > 400 { return str_slice(blended, 0, 400) }
return blended
}
// Genuine continuation: find the most recent prior USER turn as the topic anchor.
// Fixes Issues 3 and 8: old code used the last assistant reply (hist_len - 1).
let prior_user_content: String = ""
let scan_idx: Int = hist_len - 1
let found_prior_user: Bool = false
while scan_idx >= 0 && !found_prior_user {
let scan_entry: String = json_array_get(hist, scan_idx)
let scan_role: String = json_get(scan_entry, "role")
let scan_content: String = json_get(scan_entry, "content")
let is_user_turn: Bool = str_eq(scan_role, "user")
let prior_user_content = if is_user_turn && !found_prior_user { scan_content } else { prior_user_content }
let found_prior_user = if is_user_turn { true } else { found_prior_user }
let scan_idx = scan_idx - 1
}
// Secondary: tail-biased snip from last assistant reply (Issue 9 fix).
let last_asst_entry: String = json_array_get(hist, hist_len - 1)
let last_asst_role: String = json_get(last_asst_entry, "role")
let last_asst_content: String = if str_eq(last_asst_role, "assistant") {
json_get(last_asst_entry, "content")
} else { "" }
let asst_snip: String = if str_eq(last_asst_content, "") { "" } else {
topic_snip_from_entry(last_asst_content)
}
let user_snip: String = if str_len(prior_user_content) > 150 {
str_slice(prior_user_content, 0, 150)
} else { prior_user_content }
let seed: String = if !str_eq(user_snip, "") {
if !str_eq(asst_snip, "") {
user_snip + " " + asst_snip + " " + message
} else {
user_snip + " " + message
}
} else {
if !str_eq(asst_snip, "") { asst_snip + " " + message } else { message }
}
if str_len(seed) > 400 { return str_slice(seed, 0, 400) }
return seed
}
// engram_compile_multi fan-out activation across multiple query seeds. Fixes Issue 4:
// only a single seed was tried per turn, with no entity/emotion/topic diversification.
//
// Issue 2 fix: save the primary-seed activation to a dedicated state key BEFORE calling
// engram_compile(message). Each engram_compile call overwrites "engram_compile_activation_json"
// with its own activation result. Without this save, the secondary compile (bare message,
// lower signal) clobbers the primary (enriched seed, higher signal), and strengthen_chat_nodes
// later reads the lower-signal result for node strengthening.
//
// Issue 3 fix: replace the dumb str_slice(merged, 0, 6000) truncation with the same
// safe JSON boundary-scan used in engram_compile. The old truncation could cut mid-object
// when ctx1+ctx2+ctx3 together exceeded 6000 chars, producing malformed JSON context.
//
// Issue 5 fix: remove str_contains(ctx1, ctx2) / str_contains(merged, ctx3) substring
// duplicate checks. These compared multi-KB JSON strings and were unreliable in both
// directions: a coincidental substring match inside a JSON field value could falsely suppress
// ctx2 entirely; a genuinely duplicate ctx2 was missed when ctx1 was already truncated.
// We now concatenate unconditionally and let engram_compile's own dedup (node-ID based)
// handle within-result duplicates. Slight redundancy across ctx1/ctx2 is acceptable; false
// suppression of valid context is not.
fn engram_compile_multi(primary_seed: String, message: String) -> String {
let ctx1: String = engram_compile(primary_seed)
// Issue 2 fix: save the primary-seed activation before any secondary compile can
// overwrite the shared "engram_compile_activation_json" state key.
let primary_act: String = state_get("engram_compile_activation_json")
if !str_eq(primary_act, "") && !str_eq(primary_act, "[]") {
state_set("engram_compile_primary_activation_json", primary_act)
}
let entity_seed_differs: Bool = !str_eq(primary_seed, message)
let ctx2: String = if entity_seed_differs {
let raw_ctx: String = engram_compile(message)
if str_eq(raw_ctx, "") { "" } else { raw_ctx }
} else { "" }
let has_any: Bool = !str_eq(ctx1, "") || !str_eq(ctx2, "")
let ctx3: String = if has_any {
let emo_results: String = engram_search_json("emotion feeling mood care distress joy hope", 5)
let emo_ok: Bool = !str_eq(emo_results, "") && !str_eq(emo_results, "[]")
if emo_ok { engram_compile_ranked(emo_results, 3) } else { "" }
} else { "" }
// Issue 5 fix: concatenate unconditionally no str_contains substring dedup.
let sep2: String = if !str_eq(ctx1, "") && !str_eq(ctx2, "") { "\n" } else { "" }
let merged: String = ctx1 + sep2 + ctx2
let sep3: String = if !str_eq(merged, "") && !str_eq(ctx3, "") { "\n" } else { "" }
let merged = if !str_eq(ctx3, "") { merged + sep3 + ctx3 } else { merged }
// Issue 6 fix: append the bell node exactly once here, after all compile calls.
// engram_compile no longer includes affective_part in its return value; instead it
// caches the bell node in state. By appending it here we guarantee the bell node
// JSON appears at most once in the system prompt's engram block regardless of how
// many engram_compile calls were made above.
let bell_node: String = state_get("engram_compile_bell_node")
let sep4: String = if !str_eq(merged, "") && !str_eq(bell_node, "") { "\n" } else { "" }
let merged = if !str_eq(bell_node, "") { merged + sep4 + bell_node } else { merged }
if str_eq(merged, "") { return "" }
// Issue 3 fix: safe JSON boundary-scan truncation find the last closing brace
// before the 6000-char cap rather than slicing mid-object.
let cap_len: Int = 6000
if str_len(merged) <= cap_len { return merged }
let cap_search: Int = cap_len - 1
let cap_min: Int = if cap_len > 500 { cap_len - 500 } else { 0 }
let cap_pos: Int = -1
let cap_si: Int = cap_search
while cap_si >= cap_min && cap_pos < 0 {
let cap_ch: String = str_slice(merged, cap_si, cap_si + 1)
let cap_pos = if str_eq(cap_ch, "}") { cap_si } else { cap_pos }
let cap_si = if cap_pos < 0 { cap_si - 1 } else { cap_si }
}
if cap_pos > 0 { return str_slice(merged, 0, cap_pos + 1) }
return str_slice(merged, 0, cap_len)
} }
fn engram_compile(intent: String) -> String { fn engram_compile(intent: String) -> String {
@@ -583,36 +181,44 @@ fn engram_compile(intent: String) -> String {
let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) } 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 { "" } if bn_ts > cutoff_ts { bn0 } else { "" }
} else { "" } } else { "" }
// Issue 6 fix: do NOT include the bell node in this function's return value. // Positive emotion context: check for recent joy/success moments within 72h.
// engram_compile is called multiple times by engram_compile_multi (once per seed). let pos_ec_nodes: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
// If affective_part were appended here, the bell node JSON would appear once per let pos_ec_ok: Bool = !str_eq(pos_ec_nodes, "") && !str_eq(pos_ec_nodes, "[]")
// compile call duplicating it in the merged context. Instead, cache the bell node let recent_positive_ec: String = if pos_ec_ok {
// here and let engram_compile_multi append it exactly once after all calls complete. 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 sep1: String = if !str_eq(act_part, "") && !str_eq(srch_part, "") { "\n" } 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 sep2: String = if (!str_eq(act_part, "") || !str_eq(srch_part, "")) && !str_eq(scan_part, "") { "\n" } else { "" }
let ctx: String = act_part + sep1 + srch_part + sep2 + scan_part 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
// Cache bell and activation results for handle_chat reuse (Issues 2, 7).
state_set("engram_compile_bell_node", recent_bell)
state_set("engram_compile_activation_json", if act_ok { activate_json } else { "[]" })
if str_eq(ctx, "") { return "" } if str_eq(ctx, "") { return "" }
// Cap at a clean JSON object boundary scan back from the 6000-char limit to find // Raise the cap slightly to match the ranked (higher-signal) output.
// the last closing brace so we never return a truncated mid-object JSON string. if str_len(ctx) > 6000 {
let cap_len: Int = 6000 return str_slice(ctx, 0, 6000)
if str_len(ctx) <= cap_len { return ctx }
let cap_search: Int = cap_len - 1
let cap_min: Int = if cap_len > 500 { cap_len - 500 } else { 0 }
let cap_pos: Int = -1
let cap_si: Int = cap_search
while cap_si >= cap_min && cap_pos < 0 {
let cap_ch: String = str_slice(ctx, cap_si, cap_si + 1)
let cap_pos = if str_eq(cap_ch, "}") { cap_si } else { cap_pos }
let cap_si = if cap_pos < 0 { cap_si - 1 } else { cap_si }
} }
if cap_pos > 0 { return str_slice(ctx, 0, cap_pos + 1) } return ctx
return str_slice(ctx, 0, cap_len)
} }
fn json_safe(s: String) -> String { fn json_safe(s: String) -> String {
@@ -645,14 +251,10 @@ fn build_system_prompt(ctx: String) -> String {
"\n\n[IDENTITY GRAPH — who you are, loaded from your engram]\n" + id_ctx "\n\n[IDENTITY GRAPH — who you are, loaded from your engram]\n" + id_ctx
} }
// Fix (Issue #3): render ctx as prose bullets before injecting into prompt. let engram_block: String = if str_eq(ctx, "") {
// engram_compile returns raw JSON arrays/objects; engram_render_ctx converts them
// to "- [TYPE age sal] content" lines the LLM can actually read and reason over.
let rendered_ctx: String = if str_eq(ctx, "") { "" } else { engram_render_ctx(ctx) }
let engram_block: String = if str_eq(rendered_ctx, "") {
"" ""
} else { } else {
"\n\n[ENGRAM CONTEXT — compiled from your graph]\n" + rendered_ctx "\n\n[ENGRAM CONTEXT — compiled from your graph]\n" + ctx
} }
let safety_addendum: String = state_get("layered_cycle_safety_system_addendum") let safety_addendum: String = state_get("layered_cycle_safety_system_addendum")
@@ -663,7 +265,7 @@ fn build_system_prompt(ctx: String) -> String {
safety_addendum safety_addendum
} }
return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + engram_block + safety_block return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + affective_boot_block + engram_block + safety_block
} }
fn hist_append(hist: String, role: String, content: String) -> String { fn hist_append(hist: String, role: String, content: String) -> String {
@@ -806,27 +408,80 @@ fn handle_chat(body: String) -> String {
let stored_hist: String = if str_eq(state_hist, "") { conv_history_load() } else { state_hist } 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) } let hist_len: Int = if str_eq(stored_hist, "") { 0 } else { json_array_len(stored_hist) }
// Issues 2-3, 8-10 fix: build_activation_seed() replaces the raw 50-char threshold // Thread-aware activation: short/ambiguous messages (continuations like "go on",
// with smart continuation detection, prior-user-topic anchoring, multi-turn context, // "what else?", "yes") activate on the last reply instead of the bare message.
// and tail-biased snipping from long assistant replies. // This prevents a strong off-topic memory node from hijacking the reply when the
let activation_seed: String = build_activation_seed(message, stored_hist, hist_len) // user is clearly continuing an existing thread.
let is_continuation: Bool = str_len(message) < 50 && hist_len > 0
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 }
let activation_seed: String = if !str_eq(thread_snip, "") {
thread_snip + " " + message
} else {
message
}
// Issue 1 fix: call engram_compile_multi BEFORE reading the bell-node cache. // Cross-session affective context: on session start (no history yet), check engram
// engram_compile (called inside engram_compile_multi) writes "engram_compile_bell_node" // for recent distress signals within 72h and prepend a care directive if found.
// at line 426. Reading the cache before the compile call means the first session turn let affective_prefix: String = {
// always sees an empty cache the very turn where safety continuity matters most. // Runs every turn. Uses correct BellEvent/PositiveEvent tags.
// Moving compile first ensures the cache is populated before affective_prefix reads it. let aff_now_ts: Int = time_now()
let ctx: String = engram_compile_multi(activation_seed, message) let aff_cutoff: Int = aff_now_ts - 259200
let boot_aff: String = state_get("soul_affective_context")
// Fix Issue 2: reuse cached bell result from engram_compile no second engram query. let has_boot_aff: Bool = !str_eq(boot_aff, "")
// Now runs AFTER engram_compile_multi so the cache is guaranteed to be warm. let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
let affective_prefix: String = if hist_len == 0 { let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
let cached_bell: String = state_get("engram_compile_bell_node") let found_recent_dist: Bool = if has_boot_aff {
if !str_eq(cached_bell, "") { 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
} 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 {
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n" "[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
} else { "" } } else {
} else { "" } if found_recent_pos {
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n"
} else { "" }
}
}
let ctx: String = engram_compile(activation_seed)
let system: String = affective_prefix + build_system_prompt(ctx) let system: String = affective_prefix + build_system_prompt(ctx)
// First message of the session: proactively load user profile and active work context. // First message of the session: proactively load user profile and active work context.
@@ -935,20 +590,9 @@ fn handle_chat(body: String) -> String {
state_set("conv_history", final_hist) state_set("conv_history", final_hist)
conv_history_persist(final_hist) conv_history_persist(final_hist)
// Fix Issue 7: reuse activation JSON from engram_compile no third activate query. let activation_nodes: String = engram_activate_json(message, 2)
// Issue 2 fix: prefer the primary-seed activation (enriched seed, depth 5) saved let act_ok: Bool = !str_eq(activation_nodes, "") && !str_eq(activation_nodes, "[]")
// before the secondary compile could overwrite the shared state key. Fall back to let act_out: String = if act_ok { activation_nodes } else { "[]" }
// the final compile activation only when the primary key is absent (e.g. first boot
// before any compile has run or when primary_seed == message and ctx2 was skipped).
let primary_cached: String = state_get("engram_compile_primary_activation_json")
let cached_act: String = if !str_eq(primary_cached, "") && !str_eq(primary_cached, "[]") {
primary_cached
} else {
state_get("engram_compile_activation_json")
}
let act_out: String = if !str_eq(cached_act, "") && !str_eq(cached_act, "[]") {
cached_act
} else { "[]" }
strengthen_chat_nodes(act_out) strengthen_chat_nodes(act_out)
return "{\"response\":\"" + safe_response + "\",\"model\":\"" + model + "\",\"activation_nodes\":" + act_out + "}" return "{\"response\":\"" + safe_response + "\",\"model\":\"" + model + "\",\"activation_nodes\":" + act_out + "}"
@@ -1394,7 +1038,7 @@ fn handle_chat_agentic(body: String) -> String {
if str_eq(screen_action, "hard_bell") { if str_eq(screen_action, "hard_bell") {
safety_log_bell("hard", json_get(screen_result, "reason"), str_slice(message, 0, 80)) safety_log_bell("hard", json_get(screen_result, "reason"), str_slice(message, 0, 80))
return "{\"reply\":\"" + json_safe(safety_validate("", "hard_bell")) + "\",\"model\":\"\",\"agentic\":true,\"tools_used\":[]}" return "{\"reply\":\"" + json_safe(safety_validate("", "hard_bell")) + "\",\"model\":\"\",\"agentic\":true,\"tools_used\":[]}"
}
let req_model: String = json_get(body, "model") let req_model: String = json_get(body, "model")
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model } let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
@@ -1420,15 +1064,13 @@ fn handle_chat_agentic(body: String) -> String {
let hist_key: String = if str_eq(req_session, "") { "conv_history" } else { "session_hist_" + req_session } 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: String = state_get(hist_key)
let agentic_hist_len: Int = if str_eq(agentic_hist, "") { 0 } else { json_array_len(agentic_hist) } let agentic_hist_len: Int = if str_eq(agentic_hist, "") { 0 } else { json_array_len(agentic_hist) }
// Issues 2-5, 8-10 fix: build_activation_seed for smart continuation/multi-turn. let ag_is_cont: Bool = str_len(message) < 50 && agentic_hist_len > 0
// Issue 5 fix: workspace_root appended so agent activation is workspace-aware. let ag_last_entry: String = if ag_is_cont { json_array_get(agentic_hist, agentic_hist_len - 1) } else { "" }
let ag_seed_base: String = build_activation_seed(message, agentic_hist, agentic_hist_len) let ag_last_content: String = if !str_eq(ag_last_entry, "") { json_get(ag_last_entry, "content") } else { "" }
let ag_workspace_root: String = agent_workspace_root() let ag_thread_snip: String = if str_len(ag_last_content) > 150 { str_slice(ag_last_content, 0, 150) } else { ag_last_content }
let ag_seed: String = if !str_eq(ag_workspace_root, "") { let ag_seed: String = if !str_eq(ag_thread_snip, "") { ag_thread_snip + " " + message } else { message }
ag_seed_base + " workspace:" + ag_workspace_root
} else { ag_seed_base } let ctx: String = engram_compile(ag_seed)
// Issue 4 fix: multi-seed fan-out (entity + emotion)
let ctx: String = engram_compile_multi(ag_seed, message)
let identity: String = state_get("soul_identity") let identity: String = state_get("soul_identity")
let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct.\n\n" + ctx let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct.\n\n" + ctx
@@ -1818,15 +1460,7 @@ fn handle_dharma_room_turn(body: String) -> String {
} }
// The soul's own memories, activated by what it's reading not injected. // The soul's own memories, activated by what it's reading not injected.
// Issue 6 fix: distill_transcript() reduces diffuse embedding noise let engram_ctx: String = engram_compile(transcript)
let engram_ctx_base: String = engram_compile(distill_transcript(transcript))
// Append the cached bell node once (engram_compile no longer includes it inline
// to avoid duplication when called multiple times see engram_compile_multi).
let dharma_bell: String = state_get("engram_compile_bell_node")
let engram_ctx: String = if !str_eq(dharma_bell, "") {
let sep: String = if !str_eq(engram_ctx_base, "") { "\n" } else { "" }
engram_ctx_base + sep + dharma_bell
} else { engram_ctx_base }
let system_prompt: String = if str_eq(engram_ctx, "") { let system_prompt: String = if str_eq(engram_ctx, "") {
identity identity
} else { } else {
@@ -1878,15 +1512,7 @@ fn handle_dharma_room_turn_agentic(body: String) -> String {
return "{\"error\":\"transcript is required\",\"response\":\"\",\"cgi_id\":\"" + cgi_id + "\"}" return "{\"error\":\"transcript is required\",\"response\":\"\",\"cgi_id\":\"" + cgi_id + "\"}"
} }
// Issue 6 fix: distill_transcript() reduces diffuse embedding noise let ctx: String = engram_compile(transcript)
let ctx_base: String = engram_compile(distill_transcript(transcript))
// Append the cached bell node once (engram_compile no longer includes it inline
// to avoid duplication when called multiple times see engram_compile_multi).
let dharma_bell2: String = state_get("engram_compile_bell_node")
let ctx: String = if !str_eq(dharma_bell2, "") {
let sep: String = if !str_eq(ctx_base, "") { "\n" } else { "" }
ctx_base + sep + dharma_bell2
} else { ctx_base }
let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct and stay in character.\n\n" + ctx let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct and stay in character.\n\n" + ctx
let api_key: String = agentic_api_key() let api_key: String = agentic_api_key()
@@ -1948,13 +1574,18 @@ fn auto_persist(req: String, resp: String) -> Void {
// consistent with what safety_screen already evaluated for this turn. // consistent with what safety_screen already evaluated for this turn.
let bell_level: String = safety_detect_bell_level(message) let bell_level: String = safety_detect_bell_level(message)
let is_bell: Bool = !str_eq(bell_level, "none") 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 bell metadata when distress is present so // Tag the Conversation node with affective metadata when emotion is detected.
// subsequent affective queries (e.g. engram_compile) can find this exchange.
let tags: String = if is_bell { let tags: String = if is_bell {
"[\"Conversation\",\"chat\",\"timestamped\",\"bell:" + bell_level + "\",\"affective\"]" "[\"Conversation\",\"chat\",\"timestamped\",\"bell:" + bell_level + "\",\"affective\"]"
} else { } else {
"[\"Conversation\",\"chat\",\"timestamped\"]" if is_positive {
"[\"Conversation\",\"chat\",\"timestamped\",\"joy:" + positive_level + "\",\"affective\"]"
} else {
"[\"Conversation\",\"chat\",\"timestamped\"]"
}
} }
let content: String = "{\"q\":\"" + safe_msg + "\"" let content: String = "{\"q\":\"" + safe_msg + "\""
@@ -2040,6 +1671,28 @@ fn auto_persist(req: String, resp: String) -> Void {
} }
state_set(signal_key, safe_summary) 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. // strengthen_chat_nodes strengthen the engram nodes that were activated during a chat.
Generated Vendored
+14 -23
View File
@@ -22313,23 +22313,7 @@ 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"), "")
// Issue 7 fix: load history BEFORE building the activation seed so we can let ctx: String = if is_demo { engram_compile_demo(message) } else { engram_compile(message) }
// 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")
@@ -22349,6 +22333,18 @@ 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 {
@@ -22509,12 +22505,7 @@ fn handle_chat_agentic(body: String) -> String {
req_model req_model
} }
// Issue 7 fix: load history and use build_activation_seed() for the agentic let ctx: String = engram_compile(message)
// 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. "
+21 -1
View File
@@ -240,7 +240,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.
@@ -284,6 +284,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())
+115 -7
View File
@@ -162,6 +162,75 @@ fn load_identity_context() -> Void {
println("[soul] persona node loaded (" + int_to_str(str_len(p_content)) + " chars)") 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. // seed_persona_from_env one-time migration: SOUL_IDENTITY env var Persona graph node.
@@ -320,14 +389,53 @@ fn layered_cycle(raw_input: String) -> String {
json_get(steward_result, "redirect_to") json_get(steward_result, "redirect_to")
} }
// ISSUE 1: pre-LLM bell augmentation for layered_cycle path. // L2c: affective context injection.
// safety_augment_system appends soft/hard directive to system prompt when bell fires, let lc_aff_cutoff: Int = time_now() - 259200
// ensuring LLM processes message WITH the safety directive -- not just post-output gate. let lc_bell_nodes: String = engram_search_json("bell:soft bell:hard BellEvent affective", 2)
// Stored in state as "layered_cycle_safety_system_addendum" for imprint_respond to use. let lc_has_bell: Bool = !str_eq(lc_bell_nodes, "") && !str_eq(lc_bell_nodes, "[]")
// TODO: wire directly when imprint_respond gains system_override param (imprint.el change). let lc_bell_note: String = if lc_has_bell {
// ISSUE 3 TODO: no semantic crisis detection. Keyword-only means signals that evade let lb0: String = json_array_get(lc_bell_nodes, 0)
// the phrase list pass with zero augmentation. Semantic layer = separate decision. 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