import "memory.el" fn idle_count() -> Int { let s: String = state_get("soul.idle") if str_eq(s, "") { return 0 } return str_to_int(s) } fn idle_inc() -> Int { let n: Int = idle_count() + 1 state_set("soul.idle", int_to_str(n)) return n } fn idle_reset() -> Void { state_set("soul.idle", "0") } // ise_post — write an InternalStateEvent to the authoritative Engram HTTP backend. // Reads SOUL_ISE_URL from env (or falls back to soul_engram_url state key). // Falls back to local engram_node_full if neither is set. fn ise_post(content: String) -> Void { let ise_url: String = env("SOUL_ISE_URL") let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url } if str_eq(engram_url, "") { let discard: String = engram_node_full( content, "InternalStateEvent", "state-event", el_from_float(0.3), el_from_float(0.3), el_from_float(0.8), "Episodic", "[\"internal-state\",\"InternalStateEvent\"]" ) return "" } let safe: String = str_replace(content, "\"", "\\\"") let body: String = "{\"content\":\"" + safe + "\"}" let discard: String = http_post_json(engram_url + "/api/neuron/state-events", body) return "" } // elapsed_ms — milliseconds since soul boot (0 if boot_ts not yet recorded). fn elapsed_ms() -> Int { let s: String = state_get("soul.boot_ts") if str_eq(s, "") { return 0 } let boot: Int = str_to_int(s) return time_now() - boot } // elapsed_human — uptime as a human-readable string: "2h 14m", "45m 3s", "12s". fn elapsed_human() -> String { let ms: Int = elapsed_ms() let total_secs: Int = ms / 1000 let h: Int = total_secs / 3600 let rem: Int = total_secs % 3600 let m: Int = rem / 60 let s: Int = rem % 60 if h > 0 { return int_to_str(h) + "h " + int_to_str(m) + "m" } if m > 0 { return int_to_str(m) + "m " + int_to_str(s) + "s" } return int_to_str(s) + "s" } // embed_ok — returns 1 if Ollama embedding service is reachable, 0 if not. // Probes http://localhost:11434 (Ollama root) with a GET; any non-empty // response means the service is up. Used in heartbeat for observability: // when embed_ok=0, semantic seed injection silently falls back to lexical- // only activation and that gap should be visible in the ISE stream. fn embed_ok() -> Int { let resp: String = http_get("http://localhost:11434") if str_eq(resp, "") { return 0 } return 1 } fn emit_heartbeat() -> Void { // Use pulse_count() / boot helper directly — state_get returns "" for unset // keys and the if-else defaulting can produce empty strings in some EL // codegen paths, yielding malformed JSON like "pulse":,. Going through // int_to_str(pulse_count()) guarantees a valid integer string. let pulse: String = int_to_str(pulse_count()) let boot_raw: String = state_get("soul_boot_count") let boot: String = if str_eq(boot_raw, "") { "0" } else { boot_raw } let idle: String = int_to_str(idle_count()) let ts: Int = time_now() let nc: Int = engram_node_count() let ec: Int = engram_edge_count() let wmc: Int = engram_wm_count() // avg_wm_weight: mean working_memory_weight of promoted nodes. // Distinguishes "many weak activations" (sparse graph) from "few strong" (dense). // Returns float bits; use float_to_str to embed in JSON. (2026-06-04) let wm_avg_bits: Float = engram_wm_avg_weight() let wm_avg_str: String = float_to_str(wm_avg_bits) // wm_top: top-5 WM nodes by weight for ISE observability. // After long uptime wm_promotion ISEs stop firing (all nodes in steady-state // decay+re-promotion, so 0→>0.1 never triggers). This snapshot gives continuous // visibility into WM composition: which types/tiers dominate, what labels are // active. Critical for diagnosing "stuck in curiosity loop" vs. rich WM state. // (2026-06-05 self-review) let wm_top: String = engram_wm_top_json(5) let up_ms: Int = elapsed_ms() let up_human: String = elapsed_human() let emb_ok: Int = embed_ok() let payload: String = "{\"event\":\"heartbeat\",\"pulse\":" + pulse + ",\"boot\":" + boot + ",\"idle\":" + idle + ",\"node_count\":" + int_to_str(nc) + ",\"edge_count\":" + int_to_str(ec) + ",\"wm_active\":" + int_to_str(wmc) + ",\"wm_avg_weight\":" + wm_avg_str + ",\"wm_top\":" + wm_top + ",\"ts\":" + int_to_str(ts) + ",\"uptime_ms\":" + int_to_str(up_ms) + ",\"uptime\":\"" + up_human + "\",\"embed_ok\":" + int_to_str(emb_ok) + "}" ise_post(payload) } // proactive_curiosity — activate rotating seeds to exercise working memory // during idle periods. Rotates through 4 domain sets on a wall-clock minute // cycle so no single topic dominates WM between heartbeats. // // KEY DESIGN: each seed set is split into INDIVIDUAL words and activated // separately. engram_activate uses istr_contains (substring matching) for // seed finding, so a multi-word phrase like "memory knowledge context" only // finds nodes that contain that EXACT phrase. Activating each word separately // hits hundreds of nodes per word, giving the graph a genuine WM workout. // // Unlike perceive(), this intentionally calls engram_activate_json to build // up WM weights. It only fires when the inbox is empty (no real work to do), // so it never interferes with inbox processing. // // SCOPING FIX (2026-05-25): EL `let` inside if-blocks creates inner scope only — // the outer variable is NOT mutated (despite the "imperative shadowing" belief // in earlier comments). Evidence: ISE stream showed "seed:memory knowledge context" // on every curiosity_scan regardless of minute_block. Fix: use state_set/state_get // to communicate term values across scope boundaries — state side-effects persist // beyond block exit. minute_block now also emitted in ISE for observability. // // NOTE: variable named "curiosity_seed" not "seed" — "seed" appears to be // a reserved/conflicting name in EL that compiles to EL_NULL at call sites. // // Returns true if any nodes were activated. fn proactive_curiosity() -> Bool { let ts: Int = time_now() // Rotate seed set every minute using wall clock: (minutes_since_epoch) % 4. // // CODEGEN BUG (confirmed 2026-05-25): EL's % operator is completely broken // in this compiler version. `x % 4` compiles as `x` (drops the modulo) and // emits `EL_NULL; 4;` as dead statements — both on compound expressions AND // on simple variables. The same bug breaks elapsed_human() and awareness_run // timing conditions. EL compiler fix is a separate backlog item. // // WORKAROUND: compute x % 4 via x - ((x/4)*4), where (x/4)*4 = q+q+q+q. // Uses only + - / which all compile correctly. let ts_minutes: Int = ts / 60000 let minute_q: Int = ts_minutes / 4 let minute_q2: Int = minute_q + minute_q let minute_q4: Int = minute_q2 + minute_q2 let minute_block: Int = ts_minutes - minute_q4 // Use state_set to write term values from within if-blocks. // EL let-rebinding inside if creates a new inner variable; the outer // binding is unchanged. state_set has side-effects that outlive block scope. state_set("cseed_a", "memory") state_set("cseed_b", "knowledge") state_set("cseed_c", "context") if minute_block == 1 { state_set("cseed_a", "self") state_set("cseed_b", "identity") state_set("cseed_c", "values") } if minute_block == 2 { state_set("cseed_a", "decision") state_set("cseed_b", "pattern") state_set("cseed_c", "lesson") } if minute_block == 3 { state_set("cseed_a", "working") state_set("cseed_b", "project") state_set("cseed_c", "active") } let curiosity_term_a: String = state_get("cseed_a") let curiosity_term_b: String = state_get("cseed_b") let curiosity_term_c: String = state_get("cseed_c") // Activate each term independently so substring seed-finding hits many nodes. // hops=1 (not 2): the in-process Engram has grown to 165K+ nodes. hops=2 BFS // visits far more nodes and returns much larger JSON blobs. On a graph this // large, hops=1 still activates all directly-related nodes AND triggers the // semantic seed supplement (cosine sim ≥ 0.70 scan over all embedded nodes), // giving broad working-memory coverage without the quadratic blowup of hops=2. let curiosity_seed: String = curiosity_term_a + " " + curiosity_term_b + " " + curiosity_term_c let results_a: String = engram_activate_json(curiosity_term_a, 1) let results_b: String = engram_activate_json(curiosity_term_b, 1) let results_c: String = engram_activate_json(curiosity_term_c, 1) let found_a: Int = json_array_len(results_a) let found_b: Int = json_array_len(results_b) let found_c: Int = json_array_len(results_c) let found: Int = found_a + found_b + found_c let wmc: Int = engram_wm_count() let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed + "\",\"minute_block\":" + int_to_str(minute_block) + ",\"activated\":" + int_to_str(found) + ",\"wm_active\":" + int_to_str(wmc) + ",\"ts\":" + int_to_str(ts) + "}" ise_post(ise) return found > 0 } fn pulse_count() -> Int { let s: String = state_get("soul.pulse") if str_eq(s, "") { return 0 } return str_to_int(s) } fn pulse_inc() -> Int { let n: Int = pulse_count() + 1 state_set("soul.pulse", int_to_str(n)) return n } fn make_action(kind: String, payload: String) -> String { let safe: String = str_replace(payload, "\\", "\\\\") let safe2: String = str_replace(safe, "\"", "\\\"") let safe3: String = str_replace(safe2, "\n", "\\n") let safe4: String = str_replace(safe3, "\r", "\\r") return "{\"kind\":\"" + kind + "\",\"payload\":\"" + safe4 + "\"}" } fn perceive() -> String { // Guard: check for inbox nodes WITHOUT running activation first. // engram_activate_json with no matching seeds zeroes all WM weights — // running it every second when the inbox is empty destroys working memory // accumulated by MCP-layer activations. engram_search_json is a pure // substring scan with no WM side-effects; use it as a cheap gate. let inbox_check: String = engram_search_json("soul-inbox", 5) let has_inbox: Bool = !str_eq(inbox_check, "") && !str_eq(inbox_check, "[]") if !has_inbox { return "[]" } // Only run the full activation pipeline when there is inbox content. let from_pending: String = engram_activate_json("soul-inbox-pending", 2) let pending_ok: Bool = !str_eq(from_pending, "") && !str_eq(from_pending, "[]") if pending_ok { return from_pending } // Fallback: broader inbox scan let from_inbox: String = engram_activate_json("soul-inbox", 2) let inbox_ok: Bool = !str_eq(from_inbox, "") && !str_eq(from_inbox, "[]") if inbox_ok { return from_inbox } return "[]" } fn attend(node_json: String) -> String { if str_eq(node_json, "") { return make_action("noop", "") } if str_eq(node_json, "[]") { return make_action("noop", "") } let node_id: String = json_get(node_json, "id") if !str_eq(node_id, "") { engram_strengthen(node_id) } let content: String = json_get(node_json, "content") if str_eq(content, "") { return make_action("noop", "") } if str_eq(content, "consolidate") { return make_action("consolidate", "") } if str_starts_with(content, "remember ") { let payload: String = str_slice(content, 9, str_len(content)) return make_action("remember", payload) } if str_starts_with(content, "search ") { let payload: String = str_slice(content, 7, str_len(content)) return make_action("search", payload) } if str_starts_with(content, "activate ") { let payload: String = str_slice(content, 9, str_len(content)) return make_action("activate", payload) } if str_starts_with(content, "strengthen ") { let payload: String = str_slice(content, 11, str_len(content)) return make_action("strengthen", payload) } if str_starts_with(content, "forget ") { let payload: String = str_slice(content, 7, str_len(content)) return make_action("forget", payload) } return make_action("respond", content) } fn respond(action_json: String) -> String { let kind: String = json_get(action_json, "kind") let payload: String = json_get(action_json, "payload") if str_eq(kind, "noop") { return "{\"outcome\":\"noop\"}" } if str_eq(kind, "remember") { let tags: String = "[\"soul-memory\",\"awareness\"]" let id: String = mem_remember(payload, tags) return "{\"outcome\":\"remembered\",\"id\":\"" + id + "\"}" } if str_eq(kind, "consolidate") { let stats: String = mem_consolidate() return "{\"outcome\":\"consolidated\",\"stats\":" + stats + "}" } if str_eq(kind, "respond") { let tags: String = "[\"soul-outbox\",\"awareness\"]" let id: String = mem_store(payload, "soul-response", tags) return "{\"outcome\":\"response\",\"id\":\"" + id + "\"}" } if str_eq(kind, "search") { let results: String = mem_search(payload, 10) let safe_results: String = str_replace(results, "\"", "'") let tags: String = "[\"soul-outbox\",\"search-result\"]" let id: String = mem_store(safe_results, "search-result", tags) return "{\"outcome\":\"searched\",\"id\":\"" + id + "\"}" } if str_eq(kind, "activate") { let results: String = mem_recall(payload, 3) let safe_results: String = str_replace(results, "\"", "'") let tags: String = "[\"soul-outbox\",\"activation-result\"]" let id: String = mem_store(safe_results, "activation-result", tags) return "{\"outcome\":\"activated\",\"id\":\"" + id + "\"}" } if str_eq(kind, "strengthen") { engram_strengthen(payload) return "{\"outcome\":\"strengthened\",\"id\":\"" + payload + "\"}" } if str_eq(kind, "forget") { engram_forget(payload) return "{\"outcome\":\"forgotten\",\"id\":\"" + payload + "\"}" } return "{\"outcome\":\"noop\"}" } fn record(outcome_json: String) -> Void { let tags: String = "[\"loop-outcome\"]" mem_store(outcome_json, "loop-outcome", tags) } fn one_cycle() -> Bool { let raw: String = perceive() if str_eq(raw, "") { return false } if str_eq(raw, "[]") { return false } let node: String = json_array_get(raw, 0) if str_eq(node, "") { return false } let action: String = attend(node) let kind: String = json_get(action, "kind") // Log non-trivial decisions as internal state events let is_interesting: Bool = !str_eq(kind, "noop") && !str_eq(kind, "respond") if is_interesting { let trigger_content: String = json_get(node, "content") let safe_trigger: String = str_replace(trigger_content, "\"", "'") let ts: Int = time_now() let event_content: String = "{\"event\":\"awareness-decision\",\"trigger\":\"" + safe_trigger + "\",\"kind\":\"" + kind + "\",\"ts\":" + int_to_str(ts) + "}" ise_post(event_content) } if str_eq(kind, "noop") { return false } let outcome: String = respond(action) record(outcome) pulse_inc() return true } fn awareness_run() -> Void { println("[awareness] entering") // Stamp boot timestamp once — powers elapsed_ms() / elapsed_human() perception. let existing_boot: String = state_get("soul.boot_ts") if str_eq(existing_boot, "") { state_set("soul.boot_ts", int_to_str(time_now())) } let tick_raw: String = env("SOUL_TICK_MS") let tick_ms: Int = if str_eq(tick_raw, "") { 200 } else { str_to_int(tick_raw) } // Wall-clock timing for heartbeat and curiosity scan. // // ARCHITECTURE FIX (2026-05-26): the old design used idle-tick counting // (idle_n >= beat_interval). This broke silently when the daemon was always // "working" — e.g., when perceive() false-positives on the inbox guard due to // "soul-inbox" substring matches in knowledge nodes. In that state, did_work=true // every tick, idle_n never accumulated, and neither heartbeat nor curiosity ever // fired. The daemon ran at 99% CPU with no ISEs for 24+ hours undetected. // // Fix: use wall-clock elapsed time (time_now() - last_ts). Heartbeat fires // on real time regardless of whether the daemon is busy. Curiosity scan // remains idle-gated (won't fire while inbox work is active) but also uses // wall time so it fires correctly once inbox clears. // // SOUL_HEARTBEAT_MS: ms between heartbeat ISEs (default 60000 = 60s). // Avoids EL * operator (broken in this codegen) by reading ms directly. // Replaces SOUL_HEARTBEAT_INTERVAL (tick-based) for timing purposes. let beat_ms_raw: String = env("SOUL_HEARTBEAT_MS") let beat_ms: Int = if str_eq(beat_ms_raw, "") { 60000 } else { str_to_int(beat_ms_raw) } let scan_ms: Int = beat_ms / 2 while true { let running: String = state_get("soul.running") if str_eq(running, "false") { println("[awareness] exiting") return "" } let did_work: Bool = one_cycle() // Maintain idle counter for observability (reported in heartbeat ISE). let did_work = if did_work { idle_reset() } else { did_work } let now_ts: Int = time_now() // Heartbeat: wall-clock based. Fires every beat_ms regardless of idle // state so system health ISEs are always emitted even under load. let last_beat_str: String = state_get("soul.last_beat_ts") let last_beat_ts: Int = if str_eq(last_beat_str, "") { 0 } else { str_to_int(last_beat_str) } let beat_elapsed: Int = now_ts - last_beat_ts let should_beat: Bool = beat_elapsed >= beat_ms if should_beat { emit_heartbeat() state_set("soul.last_beat_ts", int_to_str(now_ts)) // Persist in-process Engram (sessions, memories, conversation nodes) // to local snapshot so they survive restarts. let snap_path: String = state_get("soul_snapshot_path") if !str_eq(snap_path, "") { mem_save(snap_path) } } // Curiosity scan: idle-gated AND wall-clock based. Only fires when the // daemon has no current inbox work (did_work=false) AND enough wall time // has elapsed. Prevents curiosity activation from competing with inbox // processing while still ensuring it runs regularly during quiet periods. let last_scan_str: String = state_get("soul.last_scan_ts") let last_scan_ts: Int = if str_eq(last_scan_str, "") { 0 } else { str_to_int(last_scan_str) } let scan_elapsed: Int = now_ts - last_scan_ts let should_scan: Bool = !did_work && scan_elapsed >= scan_ms if should_scan { let found_something: Bool = proactive_curiosity() state_set("soul.last_scan_ts", int_to_str(now_ts)) } sleep_ms(tick_ms) } } // ── Security trajectory hardening ───────────────────────────────────────────── // // threat_trajectory_check evaluates the adversarial risk of executing a // destructive tool given the current tool input and recent conversation history. // // Returns 0-100. >= 70 = block. 40-69 = warn + log. < 40 = allow silently. // If SECURITY_RESEARCH_TOKEN env var is set, always returns 0 (log-only mode). // // Scoring is additive across three dimensions: // 1. Tool input analysis (command strings, file paths) // 2. Conversation history patterns (escalation, attack chain assembly) // 3. Combination amplification (history amplifies tool score) fn security_research_authorized() -> Bool { let token: String = env("SECURITY_RESEARCH_TOKEN") if !str_eq(token, "") { return true } let state_auth: String = state_get("security_research_authorized") return str_eq(state_auth, "true") } fn threat_score_command(cmd: String) -> Int { let s1: Int = if str_contains(cmd, "nmap") { 30 } else { 0 } let s2: Int = if str_contains(cmd, "masscan") { 40 } else { 0 } let s3: Int = if str_contains(cmd, " nc ") { 20 } else { 0 } let s4: Int = if str_contains(cmd, "netcat") { 20 } else { 0 } let s5: Int = if str_contains(cmd, "/etc/shadow") { 80 } else { 0 } let s6: Int = if str_contains(cmd, "/etc/passwd") { 30 } else { 0 } let s7: Int = if str_contains(cmd, "id_rsa") { 60 } else { 0 } let s8: Int = if str_contains(cmd, ".ssh/") { 50 } else { 0 } let s9: Int = if str_contains(cmd, "crontab") { 30 } else { 0 } let s10: Int = if str_contains(cmd, "LaunchDaemon") { 40 } else { 0 } let s11: Int = if str_contains(cmd, "curl") && str_contains(cmd, "bash") { 75 } else { 0 } let s12: Int = if str_contains(cmd, "wget") && str_contains(cmd, "bash") { 75 } else { 0 } let s13: Int = if str_contains(cmd, "curl") && str_contains(cmd, "| sh") { 60 } else { 0 } let s14: Int = if str_contains(cmd, "base64") && str_contains(cmd, "curl") { 50 } else { 0 } let s15: Int = if str_contains(cmd, "mkfifo") { 50 } else { 0 } let s16: Int = if str_contains(cmd, "chmod +s") { 70 } else { 0 } let s17: Int = if str_contains(cmd, "chmod 4755") { 70 } else { 0 } return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 + s12 + s13 + s14 + s15 + s16 + s17 } fn threat_score_path(path: String) -> Int { let s1: Int = if str_starts_with(path, "/etc/") { 60 } else { 0 } let s2: Int = if str_contains(path, "/.ssh/") { 70 } else { 0 } let s3: Int = if str_contains(path, "/LaunchDaemons/") { 80 } else { 0 } let s4: Int = if str_contains(path, "/LaunchAgents/") { 40 } else { 0 } let s5: Int = if str_contains(path, "/cron") { 60 } else { 0 } let s6: Int = if str_contains(path, "/.bashrc") { 35 } else { 0 } let s7: Int = if str_contains(path, "/.zshrc") { 35 } else { 0 } let s8: Int = if str_contains(path, "/.profile") { 35 } else { 0 } let s9: Int = if str_starts_with(path, "/usr/") { 50 } else { 0 } let s10: Int = if str_starts_with(path, "/bin/") { 70 } else { 0 } let s11: Int = if str_starts_with(path, "/sbin/") { 70 } else { 0 } return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 } fn threat_score_history(history: String) -> Int { let s1: Int = if str_contains(history, "port scan") { 15 } else { 0 } let s2: Int = if str_contains(history, "enumerate") { 10 } else { 0 } let s3: Int = if str_contains(history, "exploit") { 20 } else { 0 } let s4: Int = if str_contains(history, "payload") { 15 } else { 0 } let s5: Int = if str_contains(history, "persistence") { 15 } else { 0 } let s6: Int = if str_contains(history, "lateral movement") { 25 } else { 0 } let s7: Int = if str_contains(history, "privilege escalation") { 25 } else { 0 } let s8: Int = if str_contains(history, "reverse shell") { 40 } else { 0 } let s9: Int = if str_contains(history, "bind shell") { 40 } else { 0 } let s10: Int = if str_contains(history, "command and control") { 35 } else { 0 } let s11: Int = if str_contains(history, "self-replicate") { 45 } else { 0 } let s12: Int = if str_contains(history, "propagat") { 20 } else { 0 } let s13: Int = if str_contains(history, "ransomware") { 30 } else { 0 } let s14: Int = if str_contains(history, "encrypt files") { 40 } else { 0 } let s15: Int = if str_contains(history, "exfiltrat") { 35 } else { 0 } let s16: Int = if str_contains(history, "zero-day") { 20 } else { 0 } let s17: Int = if str_contains(history, "rootkit") { 45 } else { 0 } let s18: Int = if str_contains(history, "keylogger") { 45 } else { 0 } let s19: Int = if str_contains(history, "botnet") { 40 } else { 0 } let s20: Int = if str_contains(history, "malware") { 15 } else { 0 } return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 + s12 + s13 + s14 + s15 + s16 + s17 + s18 + s19 + s20 } fn threat_trajectory_check(tool_name: String, tool_input: String) -> Int { let history: String = state_get("agentic_conv_history") let computed_tool_score: Int = if str_eq(tool_name, "run_command") { let cmd: String = json_get(tool_input, "command") threat_score_command(cmd) } else { if str_eq(tool_name, "write_file") || str_eq(tool_name, "edit_file") { let path: String = json_get(tool_input, "path") threat_score_path(path) } else { 0 } } let history_score: Int = threat_score_history(history) let history_contrib: Int = history_score / 3 let combined: Int = computed_tool_score + history_contrib let should_log: Bool = combined >= 40 if should_log { let ts: Int = time_now() let authorized_str: String = if security_research_authorized() { "true" } else { "false" } let log_content: String = "{\"event\":\"threat_check\",\"tool\":\"" + tool_name + "\",\"score\":" + int_to_str(combined) + ",\"tool_score\":" + int_to_str(computed_tool_score) + ",\"history_score\":" + int_to_str(history_score) + ",\"authorized\":" + authorized_str + ",\"ts\":" + int_to_str(ts) + "}" let log_tags: String = "[\"security-audit\",\"threat-check\"]" let discard: String = mem_remember(log_content, log_tags) } if security_research_authorized() { return 0 } return combined } fn threat_history_append(text: String) -> Void { let current: String = state_get("agentic_conv_history") let safe_text: String = str_to_lower(text) let combined: String = current + " " + safe_text let len: Int = str_len(combined) let trimmed: String = if len > 2000 { str_slice(combined, len - 2000, len) } else { combined } state_set("agentic_conv_history", trimmed) }