self-review 2026-07-22: ACT-R/Petrov base-level WM decay replaces per-call multiplicative carry-over
The old carry-over (weight *= 0.7 per engram_activate call) was call-rate- dependent — carried context died in seconds under rapid curiosity scans and lingered for hours under quiet loops — and a decayed scalar cannot represent access frequency at all. Now: k=10 access-timestamp ring + Petrov (2006) closed-form tail, d=0.5. WM promotion and engram_strengthen record presentations; carry-over evicts at base-level tau=-3.0 (Soar forgetting, ~403s single-touch) and shapes the weight held at promotion (wm_anchor) with the ACT-R retrieval logistic (s=0.4) — a pure function of wall-clock time, idempotent per call. Persisted as access_ts/wm_anchor in snapshots; legacy nodes fall back to the optimized form ln(n/(1-d)) - d*ln(L). base_level exposed in both node serializers for observability. Backing spec: 2026-07-21 integration brief (bl-b17facdd). Verified live: carried weight ~anchor seconds after two disjoint activations (old code: 0.49x); frequency-hot nodes hold B=1.9 vs -0.14 single-touch.
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
+33
-2
@@ -24,6 +24,7 @@ el_val_t route_save(el_val_t method, el_val_t path, el_val_t body);
|
|||||||
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body);
|
||||||
|
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t check_auth_ok(el_val_t method, el_val_t body);
|
el_val_t check_auth_ok(el_val_t method, el_val_t body);
|
||||||
@@ -35,6 +36,7 @@ el_val_t port;
|
|||||||
el_val_t data_dir_raw;
|
el_val_t data_dir_raw;
|
||||||
el_val_t data_dir;
|
el_val_t data_dir;
|
||||||
el_val_t snapshot_path;
|
el_val_t snapshot_path;
|
||||||
|
el_val_t boot_snap;
|
||||||
|
|
||||||
el_val_t parse_port(el_val_t bind) {
|
el_val_t parse_port(el_val_t bind) {
|
||||||
el_val_t colon = str_index_of(bind, EL_STR(":"));
|
el_val_t colon = str_index_of(bind, EL_STR(":"));
|
||||||
@@ -147,7 +149,7 @@ el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
el_val_t dir = ({ el_val_t _if_result_3 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_3 = (EL_STR("/tmp/engram")); } else { _if_result_3 = (dir_raw); } _if_result_3; });
|
el_val_t dir = ({ el_val_t _if_result_3 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_3 = (EL_STR("/tmp/engram")); } else { _if_result_3 = (dir_raw); } _if_result_3; });
|
||||||
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.json"));
|
||||||
engram_save(snap_path);
|
engram_save(snap_path);
|
||||||
el_val_t snap = fs_read(snap_path);
|
el_val_t snap = fs_read(snap_path);
|
||||||
if (str_eq(snap, EL_STR(""))) {
|
if (str_eq(snap, EL_STR(""))) {
|
||||||
@@ -252,7 +254,7 @@ el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
el_val_t dir = ({ el_val_t _if_result_16 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_16 = (EL_STR("/tmp/engram")); } else { _if_result_16 = (dir_raw); } _if_result_16; });
|
el_val_t dir = ({ el_val_t _if_result_16 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_16 = (EL_STR("/tmp/engram")); } else { _if_result_16 = (dir_raw); } _if_result_16; });
|
||||||
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.json"));
|
||||||
engram_save(snap_path);
|
engram_save(snap_path);
|
||||||
el_val_t snap = fs_read(snap_path);
|
el_val_t snap = fs_read(snap_path);
|
||||||
if (str_eq(snap, EL_STR(""))) {
|
if (str_eq(snap, EL_STR(""))) {
|
||||||
@@ -262,6 +264,23 @@ el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
|
el_val_t p = json_get_string(body, EL_STR("path"));
|
||||||
|
if (str_eq(p, EL_STR(""))) {
|
||||||
|
return err_json(EL_STR("path is required"));
|
||||||
|
}
|
||||||
|
if (str_eq(fs_read(p), EL_STR(""))) {
|
||||||
|
return err_json(EL_STR("file missing or empty"));
|
||||||
|
}
|
||||||
|
el_val_t before_n = engram_node_count();
|
||||||
|
el_val_t before_e = engram_edge_count();
|
||||||
|
engram_load_merge(p);
|
||||||
|
el_val_t added_n = (engram_node_count() - before_n);
|
||||||
|
el_val_t added_e = (engram_edge_count() - before_e);
|
||||||
|
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"nodes_added\":"), int_to_str(added_n)), EL_STR(",\"edges_added\":")), int_to_str(added_e)), EL_STR(",\"node_count\":")), int_to_str(engram_node_count())), EL_STR("}"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||||
if (str_eq(content, EL_STR(""))) {
|
if (str_eq(content, EL_STR(""))) {
|
||||||
@@ -385,6 +404,9 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
|
||||||
return route_load(method, path, body);
|
return route_load(method, path, body);
|
||||||
}
|
}
|
||||||
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load-merge")) || str_eq(clean, EL_STR("/load-merge")))) {
|
||||||
|
return route_load_merge(method, path, body);
|
||||||
|
}
|
||||||
if (str_eq(method, EL_STR("GET")) && str_eq(clean, EL_STR("/api/sync"))) {
|
if (str_eq(method, EL_STR("GET")) && str_eq(clean, EL_STR("/api/sync"))) {
|
||||||
return route_sync(method, path, body);
|
return route_sync(method, path, body);
|
||||||
}
|
}
|
||||||
@@ -401,6 +423,15 @@ int main(int _argc, char** _argv) {
|
|||||||
data_dir = ({ el_val_t _if_result_25 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_25 = (EL_STR("/tmp/engram")); } else { _if_result_25 = (data_dir_raw); } _if_result_25; });
|
data_dir = ({ el_val_t _if_result_25 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_25 = (EL_STR("/tmp/engram")); } else { _if_result_25 = (data_dir_raw); } _if_result_25; });
|
||||||
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
||||||
engram_load(snapshot_path);
|
engram_load(snapshot_path);
|
||||||
|
boot_snap = fs_read(snapshot_path);
|
||||||
|
if (!str_eq(boot_snap, EL_STR(""))) {
|
||||||
|
if (engram_node_count() == 0) {
|
||||||
|
println(EL_STR("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes \xe2\x80\x94 preserving copy at snapshot.failed-load.json"));
|
||||||
|
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.failed-load.json")), boot_snap);
|
||||||
|
} else {
|
||||||
|
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.boot-backup.json")), boot_snap);
|
||||||
|
}
|
||||||
|
}
|
||||||
println(EL_STR("[engram] runtime-native graph engine"));
|
println(EL_STR("[engram] runtime-native graph engine"));
|
||||||
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
|
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
|
||||||
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
|
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
|
||||||
|
|||||||
@@ -5581,10 +5581,10 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
|||||||
* 0.25 Lesson, 0.30 Belief/Entity, 0.40 Note/Memory/Working). This constant
|
* 0.25 Lesson, 0.30 Belief/Entity, 0.40 Note/Memory/Working). This constant
|
||||||
* is NOT used in engram_activate(); it matches the Canonical tier value only
|
* is NOT used in engram_activate(); it matches the Canonical tier value only
|
||||||
* by coincidence. (2026-07-01 self-review: clarified stale doc)
|
* by coincidence. (2026-07-01 self-review: clarified stale doc)
|
||||||
* ENGRAM_WM_DECAY: per-turn decay applied to working_memory_weight for
|
* ENGRAM_WM_DECAY: SUPERSEDED (2026-07-22 self-review, bl-b17facdd) — the
|
||||||
* nodes NOT re-activated in the current turn (conversational thread
|
* per-turn multiplicative carry-over decay was replaced by the ACT-R/
|
||||||
* continuity: a node promoted in turn N persists with reduced weight
|
* Petrov base-level scheme (see ENGRAM_BLL_* below). Kept for reference;
|
||||||
* into turn N+1 without re-activation cost).
|
* no longer used in engram_activate.
|
||||||
* ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions
|
* ENGRAM_SUPPRESSION_BREAKTHROUGH: after this many consecutive suppressions
|
||||||
* a latent node forces itself into working memory at reduced weight,
|
* a latent node forces itself into working memory at reduced weight,
|
||||||
* modelling the brain's "intrusive thought" / unresolved-tension surfacing.
|
* modelling the brain's "intrusive thought" / unresolved-tension surfacing.
|
||||||
@@ -5617,6 +5617,42 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
|||||||
#define ENGRAM_WM_CAP 24
|
#define ENGRAM_WM_CAP 24
|
||||||
#define ENGRAM_INHIBITION_FACTOR 0.1
|
#define ENGRAM_INHIBITION_FACTOR 0.1
|
||||||
|
|
||||||
|
/* ── ACT-R / Petrov hybrid base-level learning (2026-07-22 self-review) ──────
|
||||||
|
* Replaces the per-turn multiplicative WM carry-over decay (weight *= 0.7 per
|
||||||
|
* engram_activate call) with wall-clock power-law decay over actual access
|
||||||
|
* history. The old scheme was call-rate-dependent: 10 curiosity scans in 10
|
||||||
|
* seconds decayed a carried node as much as 10 scans across an hour, and a
|
||||||
|
* scalar weight loses access FREQUENCY entirely — a node touched 50 times
|
||||||
|
* decayed identically to one touched once.
|
||||||
|
*
|
||||||
|
* Petrov (2006) hybrid: keep the K most recent access timestamps exactly,
|
||||||
|
* approximate the older tail in closed form:
|
||||||
|
*
|
||||||
|
* B = ln( Σ_{j=1..k} t_j^(-d)
|
||||||
|
* + (n-k) · (t_n^(1-d) - t_k^(1-d)) / ((1-d) · (t_n - t_k)) )
|
||||||
|
*
|
||||||
|
* d = 0.5 (canonical ACT-R decay), t_j = seconds since j-th recent access,
|
||||||
|
* t_k = seconds since oldest RETAINED access, t_n = seconds since first
|
||||||
|
* presentation (node creation), n = total presentations.
|
||||||
|
*
|
||||||
|
* This is the scheme Soar ships for working-memory forgetting (Derbinsky &
|
||||||
|
* Laird 2012). Calibration: a single-touch node has B(t) = -0.5·ln(t); with
|
||||||
|
* τ = -3.0 it falls below threshold at t = e^6 ≈ 403 s. Frequently-touched
|
||||||
|
* nodes accumulate Σ t^-0.5 mass and persist proportionally longer — recency
|
||||||
|
* AND frequency in one formula, which a decayed scalar cannot represent.
|
||||||
|
*
|
||||||
|
* Carry-over weight above threshold is shaped by the ACT-R retrieval-
|
||||||
|
* probability logistic P = σ((B − τ)/s), s = 0.4, applied to the weight the
|
||||||
|
* node held at promotion (wm_anchor) — NOT re-multiplied per call, so the
|
||||||
|
* carried weight is a pure function of wall-clock time regardless of how
|
||||||
|
* often engram_activate runs.
|
||||||
|
* Sources: alexpetrov.com/pub/iccm06 · Soar cli/cmd_wm defaults ·
|
||||||
|
* arxiv.org/html/2412.05112v1 (2026-07-21 integration brief, bl-b17facdd). */
|
||||||
|
#define ENGRAM_BLL_K 10
|
||||||
|
#define ENGRAM_BLL_D 0.5
|
||||||
|
#define ENGRAM_BLL_TAU (-3.0)
|
||||||
|
#define ENGRAM_BLL_S 0.4
|
||||||
|
|
||||||
/* qsort comparator — descending double, used by WM cap enforcement. */
|
/* qsort comparator — descending double, used by WM cap enforcement. */
|
||||||
static int engram_cmp_double_desc(const void* a, const void* b) {
|
static int engram_cmp_double_desc(const void* a, const void* b) {
|
||||||
double da = *(const double*)a;
|
double da = *(const double*)a;
|
||||||
@@ -5753,8 +5789,75 @@ typedef struct EngramNode {
|
|||||||
* created via engram_node / engram_node_full and for snapshots that
|
* created via engram_node / engram_node_full and for snapshots that
|
||||||
* predate the layered schema. */
|
* predate the layered schema. */
|
||||||
uint32_t layer_id;
|
uint32_t layer_id;
|
||||||
|
/* ACT-R base-level learning state (2026-07-22 self-review, ENGRAM_BLL_*).
|
||||||
|
* access_ts: ring buffer of the most recent access timestamps (ms).
|
||||||
|
* access_head: next write slot. access_filled: valid entries (≤ K).
|
||||||
|
* wm_anchor: the WM weight the node held when last promoted; carry-over
|
||||||
|
* decay is computed from this anchor as a pure function of wall-clock
|
||||||
|
* time. All fields zero via memset in creation/load paths; snapshots
|
||||||
|
* without them degrade gracefully to the optimized-form approximation
|
||||||
|
* in engram_bll_base_level. */
|
||||||
|
int64_t access_ts[ENGRAM_BLL_K];
|
||||||
|
int32_t access_head;
|
||||||
|
int32_t access_filled;
|
||||||
|
double wm_anchor;
|
||||||
} EngramNode;
|
} EngramNode;
|
||||||
|
|
||||||
|
/* Record an access (ACT-R "presentation") into the base-level ring buffer. */
|
||||||
|
static void engram_bll_record_access(EngramNode* n, int64_t now_ms) {
|
||||||
|
n->access_ts[n->access_head] = now_ms;
|
||||||
|
n->access_head = (n->access_head + 1) % ENGRAM_BLL_K;
|
||||||
|
if (n->access_filled < ENGRAM_BLL_K) n->access_filled++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Petrov hybrid base-level activation B in nats. Exact sum over the retained
|
||||||
|
* ring, closed-form tail for older presentations. Falls back to the ACT-R
|
||||||
|
* optimized-learning approximation B = ln(n/(1−d)) − d·ln(L) for nodes with
|
||||||
|
* no retained access history (legacy snapshots). */
|
||||||
|
static double engram_bll_base_level(const EngramNode* n, int64_t now_ms) {
|
||||||
|
/* Total presentations: creation counts as the first, retrievals add. */
|
||||||
|
double n_total = (double)n->activation_count + 1.0;
|
||||||
|
double t_life = (double)(now_ms - n->created_at) / 1000.0;
|
||||||
|
if (t_life < 1.0) t_life = 1.0;
|
||||||
|
if (n->access_filled == 0) {
|
||||||
|
return log(n_total / (1.0 - ENGRAM_BLL_D))
|
||||||
|
- ENGRAM_BLL_D * log(t_life);
|
||||||
|
}
|
||||||
|
double sum = 0.0;
|
||||||
|
double t_oldest_ring = 0.0; /* seconds since oldest retained access */
|
||||||
|
for (int32_t j = 0; j < n->access_filled; j++) {
|
||||||
|
double t = (double)(now_ms - n->access_ts[j]) / 1000.0;
|
||||||
|
if (t < 1.0) t = 1.0;
|
||||||
|
sum += pow(t, -ENGRAM_BLL_D);
|
||||||
|
if (t > t_oldest_ring) t_oldest_ring = t;
|
||||||
|
}
|
||||||
|
/* Closed-form tail for the (n − k) presentations older than the ring.
|
||||||
|
* With d = 0.5 this reduces to 2·(n−k)/(√t_n + √t_k). */
|
||||||
|
double n_older = n_total - (double)n->access_filled;
|
||||||
|
if (n_older > 0.0 && t_life > t_oldest_ring) {
|
||||||
|
sum += n_older * (pow(t_life, 1.0 - ENGRAM_BLL_D)
|
||||||
|
- pow(t_oldest_ring, 1.0 - ENGRAM_BLL_D))
|
||||||
|
/ ((1.0 - ENGRAM_BLL_D) * (t_life - t_oldest_ring));
|
||||||
|
}
|
||||||
|
if (sum <= 0.0) return -99.0;
|
||||||
|
return log(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse a persisted "access_ts" comma list (chronological order) into the
|
||||||
|
* ring buffer. Tolerates absence, empty strings, and stray whitespace. */
|
||||||
|
static void engram_bll_parse_access(EngramNode* nn, const char* s) {
|
||||||
|
if (!s) return;
|
||||||
|
const char* p = s;
|
||||||
|
while (*p) {
|
||||||
|
char* endp = NULL;
|
||||||
|
long long v = strtoll(p, &endp, 10);
|
||||||
|
if (endp == p) break;
|
||||||
|
if (v > 0) engram_bll_record_access(nn, (int64_t)v);
|
||||||
|
p = endp;
|
||||||
|
while (*p == ',' || *p == ' ') p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typedef struct EngramEdge {
|
typedef struct EngramEdge {
|
||||||
char* id;
|
char* id;
|
||||||
char* from_id;
|
char* from_id;
|
||||||
@@ -6284,6 +6387,10 @@ static el_val_t engram_node_to_map(const EngramNode* n) {
|
|||||||
m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight));
|
m = el_map_set(m, EL_STR(el_strdup("working_memory_weight")), el_from_float(n->working_memory_weight));
|
||||||
m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count);
|
m = el_map_set(m, EL_STR(el_strdup("suppression_count")), (el_val_t)n->suppression_count);
|
||||||
m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id);
|
m = el_map_set(m, EL_STR(el_strdup("layer_id")), (el_val_t)(int64_t)n->layer_id);
|
||||||
|
/* Observability (2026-07-22): expose the current ACT-R base-level and
|
||||||
|
* promotion anchor so heartbeat ISEs / API consumers can see decay state. */
|
||||||
|
m = el_map_set(m, EL_STR(el_strdup("wm_anchor")), el_from_float(n->wm_anchor));
|
||||||
|
m = el_map_set(m, EL_STR(el_strdup("base_level")), el_from_float(engram_bll_base_level(n, engram_now_ms())));
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6579,6 +6686,8 @@ void engram_strengthen(el_val_t node_id) {
|
|||||||
n->activation_count++;
|
n->activation_count++;
|
||||||
n->last_activated = engram_now_ms();
|
n->last_activated = engram_now_ms();
|
||||||
n->updated_at = n->last_activated;
|
n->updated_at = n->last_activated;
|
||||||
|
/* Explicit strengthen is a presentation too (2026-07-22 self-review). */
|
||||||
|
engram_bll_record_access(n, n->last_activated);
|
||||||
}
|
}
|
||||||
|
|
||||||
void engram_forget(el_val_t node_id) {
|
void engram_forget(el_val_t node_id) {
|
||||||
@@ -7093,7 +7202,7 @@ static double engram_temporal_proximity_bonus(int64_t node_created,
|
|||||||
*
|
*
|
||||||
* Working memory persistence (turn continuity):
|
* Working memory persistence (turn continuity):
|
||||||
* Nodes promoted in the previous turn retain a decayed working_memory_weight
|
* Nodes promoted in the previous turn retain a decayed working_memory_weight
|
||||||
* (weight *= ENGRAM_WM_DECAY) without needing re-activation. This models
|
* (ACT-R base-level carry-over, 2026-07-22) without needing re-activation. This models
|
||||||
* conversational thread continuity — once a topic is in working memory,
|
* conversational thread continuity — once a topic is in working memory,
|
||||||
* it persists slightly into the next turn.
|
* it persists slightly into the next turn.
|
||||||
*
|
*
|
||||||
@@ -7527,12 +7636,34 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
if (!reached[i] && g->nodes[i].working_memory_weight > 0.0) {
|
if (!reached[i] && g->nodes[i].working_memory_weight > 0.0) {
|
||||||
/* Carry-over decay: node held WM weight from prior activation but
|
/* Carry-over decay: node held WM weight from prior activation but
|
||||||
* the current query's BFS fan-out did not reach it. Apply decay
|
* the current query's BFS fan-out did not reach it.
|
||||||
* rather than zero so recently-active context persists. */
|
*
|
||||||
double decayed = g->nodes[i].working_memory_weight * ENGRAM_WM_DECAY;
|
* 2026-07-22 self-review (bl-b17facdd): replaced the per-call
|
||||||
g->nodes[i].working_memory_weight = (decayed < 0.01) ? 0.0 : decayed;
|
* multiplicative decay (weight *= ENGRAM_WM_DECAY) with the
|
||||||
|
* ACT-R/Petrov base-level scheme. The old form was call-rate-
|
||||||
|
* dependent — carried context died in seconds under rapid
|
||||||
|
* curiosity scans and lingered for hours under quiet loops.
|
||||||
|
* Now: hard-evict when base-level < τ (Soar-style forgetting);
|
||||||
|
* above τ, shape the weight held at promotion (wm_anchor) by the
|
||||||
|
* retrieval-probability logistic. Pure function of wall-clock
|
||||||
|
* time — idempotent no matter how often activate is called. */
|
||||||
|
EngramNode* cn = &g->nodes[i];
|
||||||
|
double anchor = (cn->wm_anchor > 0.0) ? cn->wm_anchor
|
||||||
|
: cn->working_memory_weight;
|
||||||
|
double B = engram_bll_base_level(cn, now_ms);
|
||||||
|
if (B < ENGRAM_BLL_TAU) {
|
||||||
|
cn->working_memory_weight = 0.0;
|
||||||
|
} else {
|
||||||
|
double keep = 1.0 / (1.0 + exp(-(B - ENGRAM_BLL_TAU)
|
||||||
|
/ ENGRAM_BLL_S));
|
||||||
|
double w = anchor * keep;
|
||||||
|
cn->working_memory_weight = (w < 0.01) ? 0.0 : w;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
g->nodes[i].working_memory_weight = wm_weights[i];
|
g->nodes[i].working_memory_weight = wm_weights[i];
|
||||||
|
/* Anchor the promotion weight: carry-over decay above computes
|
||||||
|
* from this fixed point rather than compounding per call. */
|
||||||
|
if (wm_weights[i] > 0.0) g->nodes[i].wm_anchor = wm_weights[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7605,6 +7736,9 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
if (n->working_memory_weight <= 0.0) continue; /* evicted by cap */
|
if (n->working_memory_weight <= 0.0) continue; /* evicted by cap */
|
||||||
n->last_activated = now_ms;
|
n->last_activated = now_ms;
|
||||||
n->activation_count++;
|
n->activation_count++;
|
||||||
|
/* Base-level presentation: WM promotion is the retrieval event.
|
||||||
|
* (2026-07-22 self-review — feeds engram_bll_base_level.) */
|
||||||
|
engram_bll_record_access(n, now_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Collect all background-activated nodes for the return value ────
|
/* ── Collect all background-activated nodes for the return value ────
|
||||||
@@ -7688,6 +7822,23 @@ static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) {
|
|||||||
snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp);
|
snprintf(tmp, sizeof(tmp), ",\"working_memory_weight\":%g", n->working_memory_weight); jb_puts(b, tmp);
|
||||||
snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp);
|
snprintf(tmp, sizeof(tmp), ",\"suppression_count\":%d", n->suppression_count); jb_puts(b, tmp);
|
||||||
snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp);
|
snprintf(tmp, sizeof(tmp), ",\"layer_id\":%u", n->layer_id); jb_puts(b, tmp);
|
||||||
|
snprintf(tmp, sizeof(tmp), ",\"wm_anchor\":%g", n->wm_anchor); jb_puts(b, tmp);
|
||||||
|
snprintf(tmp, sizeof(tmp), ",\"base_level\":%g",
|
||||||
|
engram_bll_base_level(n, engram_now_ms())); jb_puts(b, tmp);
|
||||||
|
/* Base-level access history: chronological (oldest→newest) compact
|
||||||
|
* string. Loaders replay it through engram_bll_record_access; absent
|
||||||
|
* field = empty ring (optimized-form fallback). (2026-07-22) */
|
||||||
|
if (n->access_filled > 0) {
|
||||||
|
jb_puts(b, ",\"access_ts\":\"");
|
||||||
|
for (int32_t j = 0; j < n->access_filled; j++) {
|
||||||
|
int32_t idx = (n->access_head - n->access_filled + j
|
||||||
|
+ 2 * ENGRAM_BLL_K) % ENGRAM_BLL_K;
|
||||||
|
snprintf(tmp, sizeof(tmp), "%s%lld", j ? "," : "",
|
||||||
|
(long long)n->access_ts[idx]);
|
||||||
|
jb_puts(b, tmp);
|
||||||
|
}
|
||||||
|
jb_putc(b, '"');
|
||||||
|
}
|
||||||
jb_putc(b, '}');
|
jb_putc(b, '}');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7926,6 +8077,13 @@ el_val_t engram_load(el_val_t path) {
|
|||||||
} else {
|
} else {
|
||||||
nn->layer_id = ENGRAM_LAYER_DEFAULT;
|
nn->layer_id = ENGRAM_LAYER_DEFAULT;
|
||||||
}
|
}
|
||||||
|
/* Base-level state (2026-07-22): absent fields leave the ring
|
||||||
|
* empty (memset) → optimized-form fallback. */
|
||||||
|
nn->wm_anchor = eg_get_num_field(obj, "wm_anchor");
|
||||||
|
{
|
||||||
|
char* ats = eg_get_str_field(obj, "access_ts");
|
||||||
|
if (ats) { engram_bll_parse_access(nn, ats); free(ats); }
|
||||||
|
}
|
||||||
int64_t load_idx = g->node_count;
|
int64_t load_idx = g->node_count;
|
||||||
g->node_count++;
|
g->node_count++;
|
||||||
if (nn->id && *nn->id) engram_idmap_put(g, nn->id, load_idx);
|
if (nn->id && *nn->id) engram_idmap_put(g, nn->id, load_idx);
|
||||||
@@ -8119,6 +8277,13 @@ el_val_t engram_load_merge(el_val_t path) {
|
|||||||
} else {
|
} else {
|
||||||
nn->layer_id = ENGRAM_LAYER_DEFAULT;
|
nn->layer_id = ENGRAM_LAYER_DEFAULT;
|
||||||
}
|
}
|
||||||
|
/* Base-level history merges with the node (2026-07-22);
|
||||||
|
* wm_anchor stays 0 — WM state is local-only (see the
|
||||||
|
* working_memory_weight comment above). */
|
||||||
|
{
|
||||||
|
char* ats = eg_get_str_field(obj, "access_ts");
|
||||||
|
if (ats) { engram_bll_parse_access(nn, ats); free(ats); }
|
||||||
|
}
|
||||||
int64_t merge_idx = g->node_count;
|
int64_t merge_idx = g->node_count;
|
||||||
g->node_count++;
|
g->node_count++;
|
||||||
added_nodes++;
|
added_nodes++;
|
||||||
|
|||||||
Reference in New Issue
Block a user