690df89610
proactive_curiosity() now uses the top working-memory node's first label word as a 4th activation seed alongside the 4 rotating fixed sets. This breaks deterministic exploration that was reinforcing the same subgraph every cycle and creates a self-referencing loop: curiosity radiates from whatever is most salient right now, mirroring the brain's default-mode- network resting-state dynamics. str_find_chars on " :([" extracts the first meaningful word; sp > 3 guards against bracket-prefixed labels. auto_term field added to curiosity_scan ISE for observability.
676 lines
30 KiB
EmacsLisp
676 lines
30 KiB
EmacsLisp
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 ""
|
|
}
|
|
// Proper JSON string escaping: backslashes first, then quotes, then control chars.
|
|
// Previously only escaped " — this caused ise_post to produce malformed JSON when
|
|
// content contained \n (backslash-n) from wm_top label escaping: the HTTP Engram
|
|
// server would decode \n as a literal newline in the stored content field, making
|
|
// the heartbeat ISE unparseable as JSON. (2026-06-10 self-review)
|
|
let safe1: String = str_replace(content, "\\", "\\\\")
|
|
let safe2: String = str_replace(safe1, "\"", "\\\"")
|
|
let safe3: String = str_replace(safe2, "\n", "\\n")
|
|
let safe4: String = str_replace(safe3, "\r", "\\r")
|
|
let body: String = "{\"content\":\"" + safe4 + "\"}"
|
|
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", "12s".
|
|
//
|
|
// CODEGEN NOTE: EL's % and * operators are both broken in this compiler version
|
|
// (% drops the modulo, * is similarly unreliable). We avoid them entirely:
|
|
// - For h*60: use repeated doubling. 60 = 64 - 4 = 2^6 - 2^2.
|
|
// Build h*64 via three doublings of h*4, then subtract h*4.
|
|
// - For m-within-hour: total_minutes - h*60 (subtraction only).
|
|
// - For s-within-minute not shown when m > 0: avoids the s%60 problem entirely.
|
|
// (2026-06-07 self-review: fixed from broken "44h 2694m" output)
|
|
fn elapsed_human() -> String {
|
|
let ms: Int = elapsed_ms()
|
|
let total_secs: Int = ms / 1000
|
|
let total_minutes: Int = total_secs / 60
|
|
let h: Int = total_minutes / 60
|
|
if h > 0 {
|
|
// h*60 via repeated doubling (avoids broken * operator). 60 = 64-4.
|
|
let h4: Int = h + h + h + h
|
|
let h8: Int = h4 + h4
|
|
let h16: Int = h8 + h8
|
|
let h32: Int = h16 + h16
|
|
let h64: Int = h32 + h32
|
|
let h60: Int = h64 - h4
|
|
let m: Int = total_minutes - h60
|
|
return int_to_str(h) + "h " + int_to_str(m) + "m"
|
|
}
|
|
// For < 1h: total_minutes < 60, no modulo needed.
|
|
if total_minutes > 0 {
|
|
return int_to_str(total_minutes) + "m"
|
|
}
|
|
return int_to_str(total_secs) + "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
|
|
|
|
// WM-autobiographical 4th seed: extract the first word from the top working-memory
|
|
// node's label and activate it as an additional term. This creates a self-referencing
|
|
// curiosity loop — exploration radiates outward from whatever is most salient right now,
|
|
// mirroring the brain's default-mode-network resting-state dynamics. Breaks the fixed
|
|
// 4-set determinism that otherwise reinforces the same subgraph every rotation cycle.
|
|
//
|
|
// str_find_chars finds the first space/colon/bracket delimiter. sp > 3 guards against
|
|
// very short or bracket-prefixed labels like "[BacklogItem]" (sp=0, not > 3 → skipped).
|
|
// EL scoping: state_set/state_get pattern used because let inside if creates inner scope.
|
|
// (2026-06-11 self-review)
|
|
state_set("cseed_auto", "")
|
|
let wm_top_j: String = engram_wm_top_json(1)
|
|
let wm_top_n: String = json_array_get(wm_top_j, 0)
|
|
let wm_top_lbl: String = json_get(wm_top_n, "label")
|
|
if !str_eq(wm_top_lbl, "") {
|
|
let sp: Int = str_find_chars(wm_top_lbl, " :([")
|
|
if sp > 3 {
|
|
state_set("cseed_auto", str_slice(wm_top_lbl, 0, sp))
|
|
}
|
|
}
|
|
let auto_term: String = state_get("cseed_auto")
|
|
let results_auto: String = if str_eq(auto_term, "") { "[]" } else { engram_activate_json(auto_term, 1) }
|
|
let found_auto: Int = json_array_len(results_auto)
|
|
let total_found: Int = found + found_auto
|
|
let safe_auto: String = str_replace(auto_term, "\"", "'")
|
|
|
|
let wmc: Int = engram_wm_count()
|
|
let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed
|
|
+ "\",\"auto_term\":\"" + safe_auto
|
|
+ "\",\"minute_block\":" + int_to_str(minute_block)
|
|
+ ",\"activated\":" + int_to_str(total_found)
|
|
+ ",\"wm_active\":" + int_to_str(wmc)
|
|
+ ",\"ts\":" + int_to_str(ts) + "}"
|
|
ise_post(ise)
|
|
return total_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))
|
|
}
|
|
|
|
// Engram sync: periodically fetch a non-ISE snapshot from the HTTP Engram
|
|
// and merge it into the soul's in-process store so that Knowledge/Memory/
|
|
// BacklogItem nodes are always available for curiosity activation and WM.
|
|
let refresh_ms_raw: String = env("SOUL_REFRESH_MS")
|
|
let refresh_ms: Int = if str_eq(refresh_ms_raw, "") { 600000 } else { str_to_int(refresh_ms_raw) }
|
|
let last_refresh_str: String = state_get("soul.last_refresh_ts")
|
|
let last_refresh_ts: Int = if str_eq(last_refresh_str, "") { 0 } else { str_to_int(last_refresh_str) }
|
|
let refresh_elapsed: Int = now_ts - last_refresh_ts
|
|
let should_refresh: Bool = refresh_elapsed >= refresh_ms
|
|
if should_refresh {
|
|
let engram_url: String = state_get("soul_engram_url")
|
|
if !str_eq(engram_url, "") {
|
|
let sync_json: String = http_get(engram_url + "/api/sync")
|
|
if !str_eq(sync_json, "") && !str_eq(sync_json, "{}") {
|
|
let cgi_id: String = state_get("soul_cgi_id")
|
|
let tmp: String = "/tmp/soul-sync-" + cgi_id + ".json"
|
|
fs_write(tmp, sync_json)
|
|
let added: Int = engram_load_merge(tmp)
|
|
let ts2: Int = time_now()
|
|
ise_post("{\"event\":\"engram_sync\",\"added\":" + int_to_str(added) + ",\"ts\":" + int_to_str(ts2) + "}")
|
|
}
|
|
}
|
|
state_set("soul.last_refresh_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)
|
|
}
|