diff --git a/awareness.el b/awareness.el index 84a4d19..bf62a01 100644 --- a/awareness.el +++ b/awareness.el @@ -236,7 +236,24 @@ fn auto_term_try_slot(slot_type: String, slot_lbl: String) -> Void { if !str_eq(slot_lbl, "") { let sp: Int = str_find_chars(slot_lbl, " :([") if sp > 3 { - state_set("cseed_auto", str_slice(slot_lbl, 0, sp)) + // GENRE-WORD BLOCKLIST (2026-07-23 self-review): world-ingestor + // titles open with classifier prefixes ("Method paper ...", + // "Theory paper ..."), so the first word is a genre tag, not a + // topic. Verified live: every scan after Knowledge admission + // seeded on 'Method'. Skip these; a lower-WM slot with a + // topical first word wins instead. + let term: String = str_slice(slot_lbl, 0, sp) + state_set("_ats_gw", "0") + if str_eq(term, "Method") { state_set("_ats_gw", "1") } + if str_eq(term, "Theory") { state_set("_ats_gw", "1") } + if str_eq(term, "Finding") { state_set("_ats_gw", "1") } + if str_eq(term, "Survey") { state_set("_ats_gw", "1") } + if str_eq(term, "Paper") { state_set("_ats_gw", "1") } + if str_eq(term, "Knowledge") { state_set("_ats_gw", "1") } + if str_eq(term, "Value") { state_set("_ats_gw", "1") } + if str_eq(state_get("_ats_gw"), "0") { + state_set("cseed_auto", term) + } } } } diff --git a/dist/awareness.c b/dist/awareness.c index 1ee81d5..3b1cee3 100644 --- a/dist/awareness.c +++ b/dist/awareness.c @@ -188,7 +188,32 @@ el_val_t auto_term_try_slot(el_val_t slot_type, el_val_t slot_lbl) { if (!str_eq(slot_lbl, EL_STR(""))) { el_val_t sp = str_find_chars(slot_lbl, EL_STR(" :([")); if (sp > 3) { - state_set(EL_STR("cseed_auto"), str_slice(slot_lbl, 0, sp)); + el_val_t term = str_slice(slot_lbl, 0, sp); + state_set(EL_STR("_ats_gw"), EL_STR("0")); + if (str_eq(term, EL_STR("Method"))) { + state_set(EL_STR("_ats_gw"), EL_STR("1")); + } + if (str_eq(term, EL_STR("Theory"))) { + state_set(EL_STR("_ats_gw"), EL_STR("1")); + } + if (str_eq(term, EL_STR("Finding"))) { + state_set(EL_STR("_ats_gw"), EL_STR("1")); + } + if (str_eq(term, EL_STR("Survey"))) { + state_set(EL_STR("_ats_gw"), EL_STR("1")); + } + if (str_eq(term, EL_STR("Paper"))) { + state_set(EL_STR("_ats_gw"), EL_STR("1")); + } + if (str_eq(term, EL_STR("Knowledge"))) { + state_set(EL_STR("_ats_gw"), EL_STR("1")); + } + if (str_eq(term, EL_STR("Value"))) { + state_set(EL_STR("_ats_gw"), EL_STR("1")); + } + if (str_eq(state_get(EL_STR("_ats_gw")), EL_STR("0"))) { + state_set(EL_STR("cseed_auto"), term); + } } } } diff --git a/dist/memory.c b/dist/memory.c index a464f29..bab9d89 100644 --- a/dist/memory.c +++ b/dist/memory.c @@ -169,7 +169,7 @@ el_val_t mem_boot_count_inc(void) { } el_val_t content = el_str_concat(EL_STR("soul:boot_count:"), int_to_str(next)); el_val_t tags = EL_STR("[\"soul-meta\",\"boot-counter\"]"); - el_val_t boot_node_id = engram_node_full(content, EL_STR("Memory"), EL_STR("soul:boot_count"), el_from_float(0.9), el_from_float(0.9), el_from_float(1.0), EL_STR("Canonical"), tags); + el_val_t boot_node_id = engram_node_full(content, EL_STR("Memory"), EL_STR("soul:boot_count"), el_from_float(0.55), el_from_float(0.2), el_from_float(1.0), EL_STR("Working"), tags); if (str_eq(boot_node_id, EL_STR(""))) { println(el_str_concat(el_str_concat(EL_STR("[memory] mem_boot_count_inc: write rejected (empty id) \xe2\x80\x94 boot counter node lost (count="), int_to_str(next)), EL_STR(")"))); return next; @@ -178,6 +178,32 @@ el_val_t mem_boot_count_inc(void) { if (str_eq(boot_readback, EL_STR("")) || str_eq(boot_readback, EL_STR("{}"))) { println(el_str_concat(el_str_concat(el_str_concat(EL_STR("[memory] mem_boot_count_inc: WRITE VERIFY FAILED id="), boot_node_id), EL_STR(" count=")), int_to_str(next))); } + el_val_t wb_url = env(EL_STR("ENGRAM_URL")); + el_val_t wb_key = env(EL_STR("ENGRAM_API_KEY")); + if (!str_eq(wb_url, EL_STR("")) && !str_eq(wb_key, EL_STR(""))) { + el_val_t auth_body = el_str_concat(el_str_concat(EL_STR("{\"_auth\":\""), json_safe(wb_key)), EL_STR("\"}")); + el_val_t srv_old = http_get(el_str_concat(wb_url, EL_STR("/api/search?q=soul:boot_count&limit=20"))); + if (!str_eq(srv_old, EL_STR("")) && !str_eq(srv_old, EL_STR("[]"))) { + el_val_t srv_len = json_array_len(srv_old); + el_val_t si = 0; + while (si < srv_len) { + el_val_t srv_node = json_array_get(srv_old, si); + el_val_t srv_content = json_get(srv_node, EL_STR("content")); + if (str_starts_with(srv_content, EL_STR("soul:boot_count:"))) { + el_val_t srv_id = json_get(srv_node, EL_STR("id")); + if (!str_eq(srv_id, EL_STR(""))) { + http_delete_json(el_str_concat(el_str_concat(wb_url, EL_STR("/api/nodes/")), srv_id), auth_body); + } + } + si = (si + 1); + } + } + el_val_t wb_body = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"content\":\""), content), EL_STR("\",\"node_type\":\"Memory\",\"label\":\"soul:boot_count\",\"salience\":0.55,\"importance\":0.2,\"tier\":\"Working\",\"tags\":\"[\\\"soul-meta\\\",\\\"boot-counter\\\"]\",\"_auth\":\"")), json_safe(wb_key)), EL_STR("\"}")); + el_val_t wb_resp = http_post_json(el_str_concat(wb_url, EL_STR("/api/nodes")), wb_body); + if (str_contains(wb_resp, EL_STR("\"error\""))) { + println(el_str_concat(EL_STR("[memory] mem_boot_count_inc: HTTP write-back failed (count in-memory only): "), wb_resp)); + } + } return next; return 0; } diff --git a/memory.el b/memory.el index 542ee6c..c22150f 100644 --- a/memory.el +++ b/memory.el @@ -188,10 +188,20 @@ fn mem_boot_count_inc() -> Int { } let content: String = "soul:boot_count:" + int_to_str(next) let tags: String = "[\"soul-meta\",\"boot-counter\"]" + // TELEMETRY DEMOTION (2026-07-24 self-review): this counter was written at + // salience 0.9 / importance 0.9 / tier Canonical — Canonical gets +0.2 + // goal bias and the 0.15 promotion threshold, so three stale copies of a + // BOOT COUNTER held the top working-memory slots (wm 0.31+) for 23h, + // crowding out real context. It is plumbing, not memory. Working tier: + // 0.40 threshold, no tier bias, and the /api/sync route excludes + // Working-tier nodes — so the counter also stops leaking to the engram + // server graph, which is where the duplicate copies accumulated (the + // prune below only reaches the soul's local graph). Persistence across + // restarts comes from the soul's own snapshot (mem_save), not from sync. let boot_node_id: String = engram_node_full( content, "Memory", "soul:boot_count", - el_from_float(0.9), el_from_float(0.9), el_from_float(1.0), - "Canonical", tags + el_from_float(0.55), el_from_float(0.2), el_from_float(1.0), + "Working", tags ) if str_eq(boot_node_id, "") { println("[memory] mem_boot_count_inc: write rejected (empty id) — boot counter node lost (count=" + int_to_str(next) + ")") @@ -201,6 +211,46 @@ fn mem_boot_count_inc() -> Int { if str_eq(boot_readback, "") || str_eq(boot_readback, "{}") { println("[memory] mem_boot_count_inc: WRITE VERIFY FAILED id=" + boot_node_id + " count=" + int_to_str(next)) } + // HTTP WRITE-BACK (2026-07-24 self-review): in HTTP-engram mode the server + // owns persistence and the soul's in-process graph dies with the process — + // the local create above is invisible to the next boot. The counter only + // ever "persisted" via a long-gone push path, which is why the log shows + // boot #5 on three consecutive boots. Mirror the persona write-back + // pattern: delete stale server copies (dupes were the 23h WM-pollution + // bug), then create the demoted replacement server-side. Boot seeding + // reads /api/nodes, which includes Working-tier nodes, so the count + // survives restarts; the periodic /api/sync excludes Working tier, so it + // never re-imports mid-session. Fail-soft: on any HTTP error the counter + // is in-memory-only this session, same as before. + let wb_url: String = env("ENGRAM_URL") + let wb_key: String = env("ENGRAM_API_KEY") + if !str_eq(wb_url, "") && !str_eq(wb_key, "") { + let auth_body: String = "{\"_auth\":\"" + json_safe(wb_key) + "\"}" + let srv_old: String = http_get(wb_url + "/api/search?q=soul:boot_count&limit=20") + if !str_eq(srv_old, "") && !str_eq(srv_old, "[]") { + let srv_len: Int = json_array_len(srv_old) + let si: Int = 0 + while si < srv_len { + let srv_node: String = json_array_get(srv_old, si) + // Match by CONTENT prefix, not label: route_create_node sets + // label = content, so server-side counter nodes carry labels + // like "soul:boot_count:6". (2026-07-24) + let srv_content: String = json_get(srv_node, "content") + if str_starts_with(srv_content, "soul:boot_count:") { + let srv_id: String = json_get(srv_node, "id") + if !str_eq(srv_id, "") { + http_delete_json(wb_url + "/api/nodes/" + srv_id, auth_body) + } + } + let si = si + 1 + } + } + let wb_body: String = "{\"content\":\"" + content + "\",\"node_type\":\"Memory\",\"label\":\"soul:boot_count\",\"salience\":0.55,\"importance\":0.2,\"tier\":\"Working\",\"tags\":\"[\\\"soul-meta\\\",\\\"boot-counter\\\"]\",\"_auth\":\"" + json_safe(wb_key) + "\"}" + let wb_resp: String = http_post_json(wb_url + "/api/nodes", wb_body) + if str_contains(wb_resp, "\"error\"") { + println("[memory] mem_boot_count_inc: HTTP write-back failed (count in-memory only): " + wb_resp) + } + } return next }