self-review 2026-07-25: short-term inhibition-of-return + explicit embedding backfill
Working memory was winner-take-all: suppression_count never entered the promotion score and was reset on promotion, so two high-salience nodes pinned a saturated 24-slot WM for hours. Add Lebiere-Best (CogSci 2009) short-term inhibition — raw_wm *= t_n/(t_n + 120s) from the most recent recorded access — producing emergent round-robin over WM candidates. embedded_count stalled at 93/12175 after restart: the lazy backfill only runs inside engram_activate, which nothing calls on the authoritative store in production, and in-RAM vectors were never snapshotted. Add engram_embed_backfill(n) + GET/POST /api/embed-backfill route that persists the canonical snapshot whenever it embeds anything; the soul heartbeat pumps it at 32/min.
This commit is contained in:
@@ -5680,6 +5680,24 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
#define ENGRAM_BLL_TAU (-3.0)
|
||||
#define ENGRAM_BLL_S 0.4
|
||||
|
||||
/* Short-term inhibition-of-return (2026-07-25 self-review).
|
||||
* Lebiere & Best 2009 ("Balancing Long-Term Reinforcement and Short-Term
|
||||
* Inhibition", CogSci): subtract ln(1 + (t_n/t_s)^-d_s) from activation,
|
||||
* where t_n = time since the MOST RECENT access. With their best-fit
|
||||
* d_s = 1.0 the exp of the subtraction reduces to the clean multiplier
|
||||
* m(t_n) = t_n / (t_n + t_s)
|
||||
* applied to raw_wm in Pass 2. Immediately after a promotion the node's
|
||||
* score is crushed (m → 0), then self-heals as a power law — producing an
|
||||
* emergent round-robin over WM candidates instead of winner-take-all.
|
||||
* This replaces the non-decaying suppression_count as the damping
|
||||
* mechanism (that counter never entered the score at all — it only ever
|
||||
* pushed nodes TOWARD surfacing via breakthrough; kept for that role).
|
||||
* t_s = 4× the ~30 s curiosity-scan interval per the paper's guidance
|
||||
* (t_s ≈ peak re-occurrence lag). A node promoted every scan holds
|
||||
* m ≈ 0.2 until it loses its slot; after ~2 min unretrieved, m ≥ 0.5.
|
||||
* Source: act-r.psy.cmu.edu/.../894Cogsci09-Lebiere-Best.pdf */
|
||||
#define ENGRAM_STI_TS 120.0
|
||||
|
||||
/* qsort comparator — descending double, used by WM cap enforcement. */
|
||||
static int engram_cmp_double_desc(const void* a, const void* b) {
|
||||
double da = *(const double*)a;
|
||||
@@ -7830,6 +7848,23 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
raw_wm += ENGRAM_EMBED_WM_WEIGHT
|
||||
* (cosq[i] - ENGRAM_EMBED_S0) / (1.0 - ENGRAM_EMBED_S0);
|
||||
}
|
||||
/* Short-term inhibition-of-return (2026-07-25, Lebiere-Best):
|
||||
* damp by t_n/(t_n + t_s) where t_n = seconds since the most
|
||||
* recent recorded access (WM promotion / strengthen). Applied
|
||||
* after the cosine term so the WHOLE score rotates — a node that
|
||||
* just held a WM slot yields it even to semantically weaker
|
||||
* competitors, and recovers as t_n grows. access_ts is recorded
|
||||
* at promotion, so persistent WM residents self-inhibit. Nodes
|
||||
* with no access history (never promoted) are uninhibited.
|
||||
* Layer-0 override in Pass 3 still floors safety nodes. */
|
||||
if (n->access_filled > 0) {
|
||||
int32_t sti_last = (n->access_head + ENGRAM_BLL_K - 1)
|
||||
% ENGRAM_BLL_K;
|
||||
double sti_tn = (double)(now_ms - n->access_ts[sti_last])
|
||||
/ 1000.0;
|
||||
if (sti_tn < 0.1) sti_tn = 0.1;
|
||||
raw_wm *= sti_tn / (sti_tn + ENGRAM_STI_TS);
|
||||
}
|
||||
/* Threshold gate: must exceed per-type threshold to enter working
|
||||
* memory. Type threshold replaces the old flat 0.2 filter. */
|
||||
if (raw_wm >= type_threshold) {
|
||||
@@ -9080,6 +9115,43 @@ el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b) {
|
||||
return el_from_float(eg_cosine(a->emb, b->emb, a->emb_dim));
|
||||
}
|
||||
|
||||
/* engram_embed_backfill — explicitly drive the lazy embedding backfill.
|
||||
* (2026-07-25 self-review.) The per-activate backfill (8 nodes/call) only
|
||||
* runs inside engram_activate, and on the authoritative HTTP store nothing
|
||||
* calls /api/activate in production — so embedded_count stalled at ~93 of
|
||||
* 12k after a restart from a snapshot without vectors. This entry point
|
||||
* lets the server expose a route the soul's heartbeat can pump. Embeds up
|
||||
* to `count` eligible un-embedded nodes (clamped to [1, 64]), newest
|
||||
* first, respecting the same eligibility rules and circuit breaker as the
|
||||
* lazy path. Returns JSON: {"embedded":k,"embedded_count":m,"node_count":c}.
|
||||
* k = 0 with the breaker open or embedder down — never blocks past the
|
||||
* per-fetch timeout chain. */
|
||||
el_val_t engram_embed_backfill(el_val_t count) {
|
||||
EngramStore* g = engram_get();
|
||||
int64_t want = (int64_t)EL_INT(count);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 64) want = 64;
|
||||
int64_t done = 0;
|
||||
for (int64_t i = g->node_count - 1; i >= 0 && done < want; i--) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
if (n->emb || !eg_embed_eligible(n)) continue;
|
||||
int32_t d = 0;
|
||||
float* v = eg_embed_fetch(n->content, &d);
|
||||
if (!v) break; /* embedder down / breaker open — stop this call */
|
||||
n->emb = v; n->emb_dim = d;
|
||||
done++;
|
||||
}
|
||||
int64_t total = 0;
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
if (g->nodes[i].emb) total++;
|
||||
}
|
||||
char buf[160];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"embedded\":%lld,\"embedded_count\":%lld,\"node_count\":%lld}",
|
||||
(long long)done, (long long)total, (long long)g->node_count);
|
||||
return el_wrap_str(el_strdup(buf));
|
||||
}
|
||||
|
||||
/* engram_list_layers_json — serialized counterpart of engram_list_layers.
|
||||
* Returns a JSON array, sorted by activation_priority ascending. */
|
||||
el_val_t engram_list_layers_json(void) {
|
||||
|
||||
@@ -618,6 +618,7 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t d
|
||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||
el_val_t engram_stats_json(void);
|
||||
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b);
|
||||
el_val_t engram_embed_backfill(el_val_t count);
|
||||
el_val_t engram_list_layers_json(void);
|
||||
/* Working memory introspection — count, mean weight, and top-N snapshot.
|
||||
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
|
||||
|
||||
Reference in New Issue
Block a user