Merge remote-tracking branch 'origin/pr/149' into HEAD
# Conflicts: # chat.elh # dist/soul.c # mcp-wrapper/src/main.el # routes.el
This commit is contained in:
+310
-2
@@ -30,6 +30,74 @@ fn idle_reset() -> Void {
|
||||
// read decide where telemetry goes. The in-process write remains only as a
|
||||
// last resort when the HTTP POST itself fails, and is tagged ise-fallback-local
|
||||
// so misrouting is visible in the stream instead of silent.
|
||||
// hebb_consolidate — push self-formed associations to the durable store.
|
||||
//
|
||||
// WHY THIS EXISTS (2026-08-07 self-review, measured on the live system).
|
||||
// Yesterday's eligibility-trace fix made Hebbian learning work: hebb_max
|
||||
// 0.000799 -> 0.4725, and 1,198 hebbian-associate edges formed in 23h48m.
|
||||
// A census this morning found all 1,198 of them living in this process's RAM
|
||||
// and nowhere else:
|
||||
//
|
||||
// soul daemon in-process graph: 42,426 edges, 1,198 hebbian
|
||||
// engram server (:8742, durable): 41,213 edges, 49 hebbian
|
||||
//
|
||||
// The soul pulls from the server every 10 min (GET /api/sync) and never
|
||||
// pushes. It also cannot save its own snapshot: soul.el only sets
|
||||
// soul_snapshot_path inside `if is_genesis && safe_to_seed`, and safe_to_seed
|
||||
// is unconditionally false when ENGRAM_URL is set — which it is, in the
|
||||
// launchd plist — because the HTTP server owns persistence and a soul writing
|
||||
// snapshot.json would clobber it. That guard is right. So mem_save() below has
|
||||
// literally never run, and this daemon (the ONLY process doing idle cognition,
|
||||
// therefore where essentially all co-activation happens) was throwing away
|
||||
// every association it learned, every restart, silently.
|
||||
//
|
||||
// The fix is not to let the soul write the file. It is to make consolidation a
|
||||
// message: hand each newly-formed edge to the durable store over the API the
|
||||
// server already exposes. Fast volatile store learns online; slow durable store
|
||||
// keeps what cleared the threshold. Only edges past ENGRAM_HEBB_LINK_MIN are
|
||||
// ever queued, so what crosses the boundary already earned it.
|
||||
//
|
||||
// Failure is non-fatal by construction: a drained entry that fails to POST is
|
||||
// gone, and that is fine — a real association re-forms from live co-activation.
|
||||
// The counts go into the heartbeat (hebb_wb_*) so a consolidation path that has
|
||||
// stopped delivering is visible in the stream rather than in a later autopsy.
|
||||
fn hebb_consolidate() -> Int {
|
||||
let batch: String = engram_hebb_drain_json(64)
|
||||
if str_eq(batch, "") { return 0 }
|
||||
if str_eq(batch, "[]") { return 0 }
|
||||
let n: Int = json_array_len(batch)
|
||||
if n == 0 { return 0 }
|
||||
let url_env: String = env("SOUL_ISE_URL")
|
||||
let url_state: String = if str_eq(url_env, "") { state_get("soul_engram_url") } else { url_env }
|
||||
let engram_url: String = if str_eq(url_state, "") { "http://localhost:8742" } else { url_state }
|
||||
// ONE request for the whole batch, not one per edge. The server's
|
||||
// persist_canonical() writes the full 60MB snapshot on every durable
|
||||
// write, so per-edge POSTs would cost ~840MB of disk per heartbeat to
|
||||
// persist ~14 associations. /api/edges/batch connects them all and
|
||||
// snapshots once. The drain payload is already the right shape; it only
|
||||
// needs an envelope: the drain already emits the relation per entry.
|
||||
//
|
||||
// _auth is REQUIRED and its absence is silent. check_auth_ok() in server.el
|
||||
// exempts GET and /api/neuron/state-events (which is why ise_post works
|
||||
// without a key) but gates every other mutation on "_auth" in the BODY —
|
||||
// http_serve does not surface request headers, so there is no Bearer path.
|
||||
// A batch posted without it comes back {"error":"unauthorized"}, which is a
|
||||
// non-empty response: the naive `if resp == "" return 0` check would read
|
||||
// that as success and report edges delivered that were in fact refused,
|
||||
// after the drain had already destroyed them. Hence both the key and the
|
||||
// accepted-count check below. Fall back to env when the state key is empty
|
||||
// — never let a corruptible state read decide whether learning persists.
|
||||
let key_state: String = state_get("soul_engram_api_key")
|
||||
let api_key: String = if str_eq(key_state, "") { env("ENGRAM_API_KEY") } else { key_state }
|
||||
let auth_part: String = if str_eq(api_key, "") { "" } else { ",\"_auth\":\"" + api_key + "\"" }
|
||||
let body: String = "{\"edges\":" + batch + auth_part + "}"
|
||||
let resp: String = http_post_json(engram_url + "/api/edges/batch", body)
|
||||
if str_eq(resp, "") { return 0 }
|
||||
let acc: String = json_get(resp, "accepted")
|
||||
if str_eq(acc, "") { return 0 }
|
||||
return str_to_int(acc)
|
||||
}
|
||||
|
||||
fn ise_post(content: String) -> Void {
|
||||
let ise_url: String = env("SOUL_ISE_URL")
|
||||
let state_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
|
||||
@@ -54,6 +122,21 @@ fn ise_post(content: String) -> Void {
|
||||
let fail_raw: String = state_get("soul.ise_fail_count")
|
||||
let fail_n: Int = if str_eq(fail_raw, "") { 0 } else { str_to_int(fail_raw) }
|
||||
state_set("soul.ise_fail_count", int_to_str(fail_n + 1))
|
||||
// el_from_float on a LITERAL is correct and is NOT the double-wrap bug
|
||||
// (checked and dismissed 2026-08-02 self-review — recording the result
|
||||
// so this call site is not "fixed" again by the next reader).
|
||||
// The compiler treats el_from_float as the boxing intrinsic: both
|
||||
// `el_from_float(0.3)` and a bare `0.3` emit exactly one
|
||||
// el_from_float(0.3) in dist/awareness.c. Verified byte-identical
|
||||
// codegen either way.
|
||||
// The real bug fixed in server.el on 2026-08-01 was different: there
|
||||
// the arguments came from json_get_float(), i.e. values ALREADY boxed
|
||||
// as el_val_t. Wrapping THOSE a second time reinterprets the boxed
|
||||
// bits as a raw double, fails engram_decode_score's range check, and
|
||||
// silently clamps to defaults.
|
||||
// The sweep criterion is therefore "el_from_float applied to an
|
||||
// already-boxed expression", never "el_from_float applied to a
|
||||
// literal". Grepping for the call name alone produces false positives.
|
||||
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),
|
||||
@@ -299,8 +382,89 @@ fn emit_heartbeat() -> Void {
|
||||
// here so the stuck-term failure is visible in the heartbeat stream too.
|
||||
let hb_ats_raw: String = state_get("soul.auto_term_streak")
|
||||
let hb_ats: Int = if str_eq(hb_ats_raw, "") { 0 } else { str_to_int(hb_ats_raw) }
|
||||
// auto_term_empty_streak (2026-08-06): consecutive scans producing NO auto
|
||||
// term. Split out because str_eq("","") made the two failures indist-
|
||||
// inguishable — see the comment at the streak computation in
|
||||
// proactive_curiosity. Nonzero and climbing = extractor broken, not stuck.
|
||||
let hb_ate_raw: String = state_get("soul.auto_term_empty_streak")
|
||||
let hb_ate: Int = if str_eq(hb_ate_raw, "") { 0 } else { str_to_int(hb_ate_raw) }
|
||||
// Hebbian eligibility gauges (2026-08-06 self-review). The graph learned
|
||||
// ZERO structure in its first 23h of uptime: hebb_max 0.000799 against a
|
||||
// 0.15 consolidation threshold, hebbian-associate edges 0, and the
|
||||
// awareness loop calls engram_connect nowhere — so Hebbian consolidation
|
||||
// is the only self-structuring path there is, and it was inert.
|
||||
// hebb_warm — nodes with a live eligibility trace but NOT co-resident in
|
||||
// WM: exactly the population the old simultaneity rule threw
|
||||
// away. 0 forever ⇒ traces never arm and this bought nothing.
|
||||
// hebb_max — strongest single association. The number that has to move.
|
||||
// hebb_links— consolidated edges. The outcome that has to become nonzero.
|
||||
let hebb_warm_raw: String = json_get(act_stats, "hebb_warm")
|
||||
let hebb_warm: String = if str_eq(hebb_warm_raw, "") { "-1" } else { hebb_warm_raw }
|
||||
let hebb_max_raw: String = json_get(act_stats, "hebb_max")
|
||||
let hebb_max: String = if str_eq(hebb_max_raw, "") { "-1" } else { hebb_max_raw }
|
||||
let hebb_links_raw: String = json_get(act_stats, "hebb_links")
|
||||
let hebb_links: String = if str_eq(hebb_links_raw, "") { "-1" } else { hebb_links_raw }
|
||||
// Candidate-table gauges (2026-08-10 self-review). el_runtime.c COMPUTES
|
||||
// hebb_cands/hebb_cand_max/hebb_mass/hebb_edges and emits them from
|
||||
// engram_metrics_json — and this function dropped all four on the floor.
|
||||
// Nineteen keys crossed the C boundary; fourteen reached the ISE stream.
|
||||
// The two that mattered most are exactly the pair the runtime added to
|
||||
// answer the question the 08-06 review had to instrument for:
|
||||
// hebb_cands — associations currently being tracked toward
|
||||
// consolidation. 0 ⇒ nothing co-activates at all.
|
||||
// hebb_cand_max — how close the leading candidate is to
|
||||
// ENGRAM_HEBB_LINK_MIN (0.15). Sustained just-below ⇒
|
||||
// the THRESHOLD is the bottleneck, not the event rate.
|
||||
// Without both, "hebb_links stopped climbing" is undiagnosable from the
|
||||
// durable record: nothing-co-activates and threshold-too-high look
|
||||
// identical. An instrument that is computed but not plumbed to durable
|
||||
// storage is not an instrument — it is a local variable.
|
||||
// hebb_mass — Σ hebb across edges; the runaway detector against the
|
||||
// ENGRAM_HEBB_NODE_BUDGET homeostatic cap.
|
||||
// hebb_edges — total potentiated edges (hebb > MIN), the denominator
|
||||
// hebb_max is the max of.
|
||||
let hebb_cands_raw: String = json_get(act_stats, "hebb_cands")
|
||||
let hebb_cands: String = if str_eq(hebb_cands_raw, "") { "-1" } else { hebb_cands_raw }
|
||||
let hebb_cmax_raw: String = json_get(act_stats, "hebb_cand_max")
|
||||
let hebb_cmax: String = if str_eq(hebb_cmax_raw, "") { "-1" } else { hebb_cmax_raw }
|
||||
let hebb_mass_raw: String = json_get(act_stats, "hebb_mass")
|
||||
let hebb_mass: String = if str_eq(hebb_mass_raw, "") { "-1" } else { hebb_mass_raw }
|
||||
let hebb_edges_raw: String = json_get(act_stats, "hebb_edges")
|
||||
let hebb_edges: String = if str_eq(hebb_edges_raw, "") { "-1" } else { hebb_edges_raw }
|
||||
// Consolidation write-back gauges (2026-08-07 self-review). hebb_links
|
||||
// counts what this process LEARNED; these three count what SURVIVES it.
|
||||
// The distinction is the whole finding: 1,198 links formed, 0 persisted,
|
||||
// because the learner is not the persistence owner (see hebb_consolidate).
|
||||
// wb_pending — queued, not yet handed over. Climbing ⇒ writer is down.
|
||||
// wb_drained — cumulative popped for delivery. Flat while hebb_links
|
||||
// climbs ⇒ the drain is not being called at all.
|
||||
// wb_dropped — lost to a full queue. Must stay 0; nonzero means the
|
||||
// durable store has been unreachable long enough to matter.
|
||||
// wb_sent — POSTs the durable store actually accepted this beat.
|
||||
let wb_pend_raw: String = json_get(act_stats, "hebb_wb_pending")
|
||||
let wb_pend: String = if str_eq(wb_pend_raw, "") { "-1" } else { wb_pend_raw }
|
||||
let wb_drain_raw: String = json_get(act_stats, "hebb_wb_drained")
|
||||
let wb_drain: String = if str_eq(wb_drain_raw, "") { "-1" } else { wb_drain_raw }
|
||||
let wb_drop_raw: String = json_get(act_stats, "hebb_wb_dropped")
|
||||
let wb_drop: String = if str_eq(wb_drop_raw, "") { "-1" } else { wb_drop_raw }
|
||||
let wb_sent_raw: String = state_get("soul.hebb_wb_sent")
|
||||
let wb_sent: String = if str_eq(wb_sent_raw, "") { "0" } else { wb_sent_raw }
|
||||
// dup_wm_global (2026-08-06): redundant WM residents that arrived via the
|
||||
// carry-over path, which Pass 3½ structurally could not see. Confirmed live
|
||||
// by a census that caught two byte-identical copies of one 3,193-char
|
||||
// document both holding slots.
|
||||
let dup_wm_g_raw: String = json_get(act_stats, "dup_wm_global")
|
||||
let dup_wm_g: String = if str_eq(dup_wm_g_raw, "") { "-1" } else { dup_wm_g_raw }
|
||||
let act_brk_raw: String = json_get(act_stats, "embed_breaker_open")
|
||||
let act_brk: String = if str_eq(act_brk_raw, "") { "-1" } else { act_brk_raw }
|
||||
// embed_consec_fail (2026-08-10 self-review): also computed by the C side
|
||||
// and also dropped here. embed_breaker_open is the LAGGING indicator — it
|
||||
// only goes 1 after ENGRAM_EMBED_BREAKER_LIMIT consecutive failures, by
|
||||
// which point semantic activation has already degraded to pure lexical
|
||||
// for the whole cooldown. consec_fail is the leading edge of the same
|
||||
// event and costs nothing to carry.
|
||||
let emb_cf_raw: String = json_get(act_stats, "embed_consec_fail")
|
||||
let emb_cf: String = if str_eq(emb_cf_raw, "") { "-1" } else { emb_cf_raw }
|
||||
// ctx_cos (2026-07-29 self-review): cos(query, context centroid) at the
|
||||
// last activate call — the drift gauge for the new context-centroid
|
||||
// scoring. ~1.0 aligned; low at domain-rotation boundaries is healthy;
|
||||
@@ -308,7 +472,79 @@ fn emit_heartbeat() -> Void {
|
||||
// down) and semantic continuity is silently absent.
|
||||
let ctx_cos_raw: String = json_get(act_stats, "ctx_cos")
|
||||
let ctx_cos: String = if str_eq(ctx_cos_raw, "") { "-2" } else { ctx_cos_raw }
|
||||
let payload: String = "{\"event\":\"heartbeat\",\"pulse\":" + pulse + ",\"tick\":" + pulse + ",\"boot\":" + boot + ",\"idle\":" + idle + ",\"idle_ms\":" + int_to_str(idle_ms) + ",\"node_count\":" + int_to_str(nc) + ",\"edge_count\":" + int_to_str(ec) + ",\"node_delta\":" + int_to_str(node_delta) + ",\"edge_delta\":" + int_to_str(edge_delta) + ",\"wm_active\":" + int_to_str(wmc) + ",\"wm_delta\":" + int_to_str(wm_delta) + ",\"wm_saturated\":" + int_to_str(wm_sat) + ",\"wm_top0_streak\":" + int_to_str(t0streak) + ",\"wm_churn\":" + int_to_str(wm_churn) + ",\"wm_top0_wm\":" + wm_top0_wm + ",\"sync_added_total\":" + sat_str + ",\"sync_age_ms\":" + int_to_str(sync_age) + ",\"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) + ",\"embed_backfilled\":" + bf_done + ",\"embed_count\":" + bf_total + ",\"embed_eligible\":" + embed_elig + ",\"wm_evicted\":" + act_evict + ",\"wm_evicted_delta\":" + int_to_str(evict_delta) + ",\"breakthroughs\":" + act_bt + ",\"breakthroughs_delta\":" + int_to_str(bt_delta) + ",\"auto_term_streak\":" + int_to_str(hb_ats) + ",\"embed_breaker_open\":" + act_brk + ",\"ctx_cos\":" + ctx_cos + ",\"ise_fail\":" + fail_str + "}"
|
||||
// Redundancy suppression gauges (2026-08-05 self-review). A content-hash
|
||||
// census found 1,858 redundant copies — 44.9% of the non-ISE graph, from a
|
||||
// June id-scheme migration. They embed identically, so they were taking
|
||||
// 40.2% of semantic seed slots (measured: 4.78 distinct seeds of 8).
|
||||
// dup_seeds — redundant copies denied a seed slot, cumulative. A healthy
|
||||
// nonzero rate means the suppressor is doing real work; a
|
||||
// sustained drop toward 0 means the duplicates were finally
|
||||
// merged out of the graph (the repair this defends against).
|
||||
// dup_wm — duplicate WM candidates evicted before the capacity cap.
|
||||
// Cumulative like wm_evicted/breakthroughs; diff across heartbeats for rate.
|
||||
let dup_seeds_raw: String = json_get(act_stats, "dup_seeds")
|
||||
let dup_seeds: String = if str_eq(dup_seeds_raw, "") { "-1" } else { dup_seeds_raw }
|
||||
let dup_wm_raw: String = json_get(act_stats, "dup_wm")
|
||||
let dup_wm: String = if str_eq(dup_wm_raw, "") { "-1" } else { dup_wm_raw }
|
||||
// txt_damaged (2026-08-08 self-review): nodes created THIS process whose
|
||||
// content carries the character-loss signature (see eg_text_loss_signature
|
||||
// in el_runtime.c). Today's review found the JSON parser had been replacing
|
||||
// every \uXXXX escape with a literal '?' for at least two months — 76% of
|
||||
// non-telemetry nodes damaged, including the self root and every values
|
||||
// node — and nothing caught it, because every gauge here reported whether
|
||||
// the machinery was RUNNING and none reported whether the text it carried
|
||||
// was INTACT. The parser is fixed; this is the standing regression signal.
|
||||
// Healthy state is a flat 0. Any climb means a write path is mangling text
|
||||
// again. The full store census is GET /api/text-health (too expensive for
|
||||
// a 60s beat); this is the cheap flow counter that belongs on every beat.
|
||||
let txt_dmg_raw: String = json_get(act_stats, "txt_damaged")
|
||||
let txt_dmg: String = if str_eq(txt_dmg_raw, "") { "-1" } else { txt_dmg_raw }
|
||||
// ── Corpus damage STOCK, not just flow (2026-08-10 self-review) ────────
|
||||
// txt_damaged above is a FLOW gauge: nodes damaged by a write in THIS
|
||||
// process. The 08-08 review fixed the parser, watched that flow fall to
|
||||
// 0, and recorded the defect as closed. It was not closed. Today's census
|
||||
// on the live store: scanned 4100, damaged 2781 — 67.8% of the corpus is
|
||||
// STILL carrying the character loss, including the self root and every
|
||||
// values node ("Value ? Constraints as Freedom"). The parser stopped
|
||||
// producing new damage; nothing ever repaired the old.
|
||||
//
|
||||
// That is the 08-08 lesson recursing one level up. 08-08 said "instrument
|
||||
// the payload, not just the machinery" — and then instrumented the payload
|
||||
// RATE and not the payload STOCK. A flow gauge reads 0 both when the
|
||||
// corpus is clean and when it is uniformly damaged but quiescent. Those
|
||||
// are opposite states and the beat could not tell them apart.
|
||||
//
|
||||
// Cost: GET /api/text-health scans the whole store, too expensive for a
|
||||
// 60s beat (which is why 08-08 left it off). So sample it on a countdown
|
||||
// and CARRY the last reading on every beat, with its age. A stale-but-
|
||||
// present stock number beats an absent one; damaged_age_ms makes the
|
||||
// staleness explicit rather than implied. No modulo/multiply — both
|
||||
// operators are broken in this compiler (see the note at line ~160).
|
||||
let tc_raw: String = state_get("soul.txt_census_countdown")
|
||||
let tc_n: Int = if str_eq(tc_raw, "") { 0 } else { str_to_int(tc_raw) }
|
||||
if tc_n <= 0 {
|
||||
let th_resp: String = http_get(hb_engram_url + "/api/text-health")
|
||||
let th_pct: String = json_get(th_resp, "damaged_pct")
|
||||
if !str_eq(th_pct, "") {
|
||||
state_set("soul.txt_damaged_pct", th_pct)
|
||||
state_set("soul.txt_damaged_n", json_get(th_resp, "damaged"))
|
||||
state_set("soul.txt_scanned_n", json_get(th_resp, "scanned"))
|
||||
state_set("soul.txt_census_ts", int_to_str(ts))
|
||||
}
|
||||
// 30 beats ≈ 30 min at the 60s cadence. Reset even on a failed census
|
||||
// so an unreachable route cannot turn this into a per-beat full scan.
|
||||
state_set("soul.txt_census_countdown", "30")
|
||||
}
|
||||
if tc_n > 0 { state_set("soul.txt_census_countdown", int_to_str(tc_n - 1)) }
|
||||
let dmg_pct_raw: String = state_get("soul.txt_damaged_pct")
|
||||
let dmg_pct: String = if str_eq(dmg_pct_raw, "") { "-1" } else { dmg_pct_raw }
|
||||
let dmg_n_raw: String = state_get("soul.txt_damaged_n")
|
||||
let dmg_n: String = if str_eq(dmg_n_raw, "") { "-1" } else { dmg_n_raw }
|
||||
let dmg_scan_raw: String = state_get("soul.txt_scanned_n")
|
||||
let dmg_scan: String = if str_eq(dmg_scan_raw, "") { "-1" } else { dmg_scan_raw }
|
||||
let dmg_ts_raw: String = state_get("soul.txt_census_ts")
|
||||
let dmg_age: Int = if str_eq(dmg_ts_raw, "") { 0 - 1 } else { ts - str_to_int(dmg_ts_raw) }
|
||||
let payload: String = "{\"event\":\"heartbeat\",\"pulse\":" + pulse + ",\"tick\":" + pulse + ",\"boot\":" + boot + ",\"idle\":" + idle + ",\"idle_ms\":" + int_to_str(idle_ms) + ",\"node_count\":" + int_to_str(nc) + ",\"edge_count\":" + int_to_str(ec) + ",\"node_delta\":" + int_to_str(node_delta) + ",\"edge_delta\":" + int_to_str(edge_delta) + ",\"wm_active\":" + int_to_str(wmc) + ",\"wm_delta\":" + int_to_str(wm_delta) + ",\"wm_saturated\":" + int_to_str(wm_sat) + ",\"wm_top0_streak\":" + int_to_str(t0streak) + ",\"wm_churn\":" + int_to_str(wm_churn) + ",\"wm_top0_wm\":" + wm_top0_wm + ",\"sync_added_total\":" + sat_str + ",\"sync_age_ms\":" + int_to_str(sync_age) + ",\"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) + ",\"embed_backfilled\":" + bf_done + ",\"embed_count\":" + bf_total + ",\"embed_eligible\":" + embed_elig + ",\"wm_evicted\":" + act_evict + ",\"wm_evicted_delta\":" + int_to_str(evict_delta) + ",\"breakthroughs\":" + act_bt + ",\"breakthroughs_delta\":" + int_to_str(bt_delta) + ",\"auto_term_streak\":" + int_to_str(hb_ats) + ",\"auto_term_empty_streak\":" + int_to_str(hb_ate) + ",\"embed_breaker_open\":" + act_brk + ",\"ctx_cos\":" + ctx_cos + ",\"dup_seeds\":" + dup_seeds + ",\"dup_wm\":" + dup_wm + ",\"dup_wm_global\":" + dup_wm_g + ",\"hebb_warm\":" + hebb_warm + ",\"hebb_max\":" + hebb_max + ",\"hebb_links\":" + hebb_links + ",\"hebb_cands\":" + hebb_cands + ",\"hebb_cand_max\":" + hebb_cmax + ",\"hebb_mass\":" + hebb_mass + ",\"hebb_edges\":" + hebb_edges + ",\"embed_consec_fail\":" + emb_cf + ",\"txt_damaged_pct\":" + dmg_pct + ",\"txt_damaged_n\":" + dmg_n + ",\"txt_scanned_n\":" + dmg_scan + ",\"txt_census_age_ms\":" + int_to_str(dmg_age) + ",\"hebb_wb_pending\":" + wb_pend + ",\"hebb_wb_drained\":" + wb_drain + ",\"hebb_wb_dropped\":" + wb_drop + ",\"hebb_wb_sent\":" + wb_sent + ",\"ise_fail\":" + fail_str + ",\"txt_damaged\":" + txt_dmg + "}"
|
||||
ise_post(payload)
|
||||
}
|
||||
|
||||
@@ -400,6 +636,48 @@ fn auto_term_try_slot(slot_type: String, slot_lbl: String) -> Void {
|
||||
// carrying a quote character is not a topic word.
|
||||
if str_contains(term, "\"") { state_set("_ats_gw", "1") }
|
||||
if str_contains(term, "'") { state_set("_ats_gw", "1") }
|
||||
// TERM-SPECIFICITY GATE (2026-08-03 self-review): the three
|
||||
// guards above are hand-curated lists, and every one of them
|
||||
// was written REACTIVELY — after a flood was already observed
|
||||
// in the ISE stream. A list can only ever contain the floods
|
||||
// that already happened. Two were in flight, unfixed, while
|
||||
// this review ran:
|
||||
// "<!--" → 252 nodes activated (markdown comment opener:
|
||||
// 4 chars, no quote, no colon — passes every
|
||||
// guard above)
|
||||
// "SELF" → 541 nodes activated (the stopword list has
|
||||
// "Self" Title-case; str_eq is case-SENSITIVE,
|
||||
// so the uppercase token sails through)
|
||||
// Replace reaction with measurement: engram_label_df(term)
|
||||
// counts nodes whose label contains the term. Low-specificity
|
||||
// tokens are corpus-frequent BY DEFINITION, so this catches
|
||||
// the flood class PROSPECTIVELY and tracks the corpus as the
|
||||
// world-ingestor changes what the store is made of.
|
||||
// This is IDF — Spärck Jones (1972) named it "term
|
||||
// specificity"; automatic stopword compilation from it is the
|
||||
// textbook application.
|
||||
//
|
||||
// Threshold node_count/400 (floor 8), measured on this store
|
||||
// (13,370 nodes → 33). Live df separates the classes by an
|
||||
// order of magnitude: <!--:220, SELF:175, Context:53 rejected;
|
||||
// Dual:12, Sparse:8, engram_goal_bias:1, Clin-JEPA:1 pass.
|
||||
//
|
||||
// This does NOT replace the stopword list — verified against
|
||||
// all 86 listed terms, not assumed. It catches 13 (Will:306,
|
||||
// Self:175, Over:116, Knowledge:112 …) and misses 73
|
||||
// (Whose:0, Would:0, Could:0, This:9 …). Labels are terse
|
||||
// titles, so English function words are genuinely RARE in
|
||||
// them: low df, high noise. The gates cover disjoint failure
|
||||
// modes — stopwords catch function words, df catches
|
||||
// corpus-frequent markup/sentinel/genre tokens. Both required.
|
||||
// Nested rather than max(): El `let` is single-assignment, so
|
||||
// the floor is expressed as a second conjunct. Reject iff
|
||||
// df > node_count/400 AND df > 8 — i.e. df > max(that, 8).
|
||||
let df_max: Int = engram_node_count() / 400
|
||||
let df_term: Int = engram_label_df(term)
|
||||
if df_term > df_max {
|
||||
if df_term > 8 { state_set("_ats_gw", "1") }
|
||||
}
|
||||
// AUTO-TERM TABU (2026-07-25 self-review): finst-style
|
||||
// inhibition-of-return (ACT-R declarative finsts: small
|
||||
// marker pool, hard exclusion). The last 4 selected auto
|
||||
@@ -549,9 +827,25 @@ fn proactive_curiosity() -> Bool {
|
||||
let prev_auto: String = state_get("soul.prev_auto_term")
|
||||
let atstreak_raw: String = state_get("soul.auto_term_streak")
|
||||
let atstreak_prev: Int = if str_eq(atstreak_raw, "") { 0 } else { str_to_int(atstreak_raw) }
|
||||
let atstreak: Int = if str_eq(auto_term, prev_auto) { atstreak_prev + 1 } else { 1 }
|
||||
// A streak of nothing is not a streak (2026-08-06 self-review).
|
||||
// str_eq("", "") is true, so an auto-term extractor that kept FAILING
|
||||
// reported a rising auto_term_streak — the same signal that means
|
||||
// "fixated on one term" also meant "producing no term at all", which are
|
||||
// opposite failures needing opposite responses. Observed live in the ISE
|
||||
// stream as {"auto_term":"","auto_term_streak":3}. This is the same class
|
||||
// of bug already fixed for wm_top0_streak; auto_term was missed then.
|
||||
// Empty now reads 0, and the empty run is counted on its own axis so the
|
||||
// extractor failing is visible rather than disguised as health.
|
||||
let is_empty: Bool = str_eq(auto_term, "")
|
||||
let atstreak: Int = if is_empty { 0 } else {
|
||||
if str_eq(auto_term, prev_auto) { atstreak_prev + 1 } else { 1 }
|
||||
}
|
||||
let atempty_raw: String = state_get("soul.auto_term_empty_streak")
|
||||
let atempty_prev: Int = if str_eq(atempty_raw, "") { 0 } else { str_to_int(atempty_raw) }
|
||||
let atempty: Int = if is_empty { atempty_prev + 1 } else { 0 }
|
||||
state_set("soul.prev_auto_term", auto_term)
|
||||
state_set("soul.auto_term_streak", int_to_str(atstreak))
|
||||
state_set("soul.auto_term_empty_streak", int_to_str(atempty))
|
||||
if !str_eq(auto_term, "") {
|
||||
state_set("soul.tabu_t3", state_get("soul.tabu_t2"))
|
||||
state_set("soul.tabu_t2", state_get("soul.tabu_t1"))
|
||||
@@ -571,6 +865,7 @@ fn proactive_curiosity() -> Bool {
|
||||
let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed
|
||||
+ "\",\"auto_term\":\"" + safe_auto
|
||||
+ "\",\"auto_term_streak\":" + int_to_str(atstreak)
|
||||
+ ",\"auto_term_empty_streak\":" + int_to_str(atempty)
|
||||
+ ",\"minute_block\":" + int_to_str(minute_block)
|
||||
+ ",\"activated\":" + int_to_str(total_found)
|
||||
+ ",\"wm_active\":" + int_to_str(wmc)
|
||||
@@ -857,8 +1152,15 @@ fn awareness_run() -> Void {
|
||||
// still leaves no trace — that absence is itself the crash signal.)
|
||||
let sd_boot_raw: String = state_get("soul_boot_count")
|
||||
let sd_boot: String = if str_eq(sd_boot_raw, "") { "0" } else { sd_boot_raw }
|
||||
// Final consolidation before exit. The periodic drain runs on the
|
||||
// heartbeat (~8 min), so a clean shutdown between beats would take
|
||||
// everything learned since the last one to the grave — the exact
|
||||
// loss this whole path exists to stop, just at a smaller scale.
|
||||
// Best-effort: if the durable store is already down we exit anyway.
|
||||
let sd_wb: Int = hebb_consolidate()
|
||||
ise_post("{\"event\":\"shutdown\",\"boot\":" + sd_boot
|
||||
+ ",\"pulse\":" + int_to_str(pulse_count())
|
||||
+ ",\"hebb_wb_sent\":" + int_to_str(sd_wb)
|
||||
+ ",\"uptime_ms\":" + int_to_str(elapsed_ms())
|
||||
+ ",\"ts\":" + int_to_str(time_now()) + "}")
|
||||
println("[awareness] exiting")
|
||||
@@ -885,6 +1187,12 @@ fn awareness_run() -> Void {
|
||||
let beat_elapsed: Int = now_ts - last_beat_ts
|
||||
let should_beat: Bool = beat_elapsed >= beat_ms
|
||||
if should_beat {
|
||||
// Consolidate BEFORE the heartbeat so the gauges the heartbeat
|
||||
// reports describe the state this beat actually left behind, not
|
||||
// the state one beat stale. See hebb_consolidate for why a daemon
|
||||
// that learns 1,198 associations a day was keeping none of them.
|
||||
let wb_sent_n: Int = hebb_consolidate()
|
||||
state_set("soul.hebb_wb_sent", int_to_str(wb_sent_n))
|
||||
emit_heartbeat()
|
||||
state_set("soul.last_beat_ts", int_to_str(now_ts))
|
||||
// Persist in-process Engram (sessions, memories, conversation nodes)
|
||||
|
||||
Reference in New Issue
Block a user