geometry: disagreement belongs on the edge, not averaged into the region
co_registration is corr(hebb strength, semantic proximity) over a region's
internal edges. Whether use and meaning agree is a property of EACH EDGE;
the correlation averages it into one scalar per region, so a region holding
one violently disagreeing edge beside one violently agreeing edge reports
~0. The disagreements cancel and the summary destroys exactly what it was
built to reveal — the mean-versus-min error, in different clothes.
Measured: 375 live neighborhoods, 340 positive, 31 AT ZERO, 4 negative.
Read as a count that says 'four things to be curious about'. Read correctly
it says four were lopsided enough to survive averaging, and the 31 zeros
are where opposing sites cancelled.
The loop computing the aggregate already had both halves per edge — w and
cs — and threw them away. Now:
discord = z(semantic proximity) - z(association strength)
standardized within the region from accumulators already gathered. No
second statistic, no constant, no threshold; |discord| IS the nucleation
strength. >0 near in meaning yet unlinked by use; <0 linked by use yet far
in meaning. Both surprising.
This also removes the reason curiosity looked like a search problem. With a
per-region number the only way to find sites is to enumerate regions — I
wrote exactly that sweep, and it is a supervisor walking the structure,
O(n) per call, fine at 375 and impossible at a million. Nothing in a mind
scans its neighborhoods to find what is surprising; the surprise captures
attention. That sweep is reverted here.
co_registration is deprecated, not deleted: it is embedded in the persisted
GEO1 blob and removing it is a format migration that must not ride along.
Nothing new may read it.
This commit is contained in:
+134
-2
@@ -1173,6 +1173,128 @@ void http_set_handler(el_val_t name) {
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
}
|
||||
|
||||
/* ── Ambient consolidation: dreaming ────────────────────────────────────────
|
||||
*
|
||||
* Dreaming is not sleep, and it is not scheduled. A brain has no cron job.
|
||||
* The default mode network is ANTICORRELATED WITH TASK ENGAGEMENT: attention
|
||||
* drops, it activates — hundreds of times a day, for seconds at a time.
|
||||
* Daydreaming and sleep-dreaming are one process at different depths, and the
|
||||
* depth is set by how much capacity is unclaimed, not by a time of day.
|
||||
*
|
||||
* WHY THIS EXISTS (2026-08-16). Consolidation had no owner, so it was
|
||||
* implemented at every site that needed a piece of it — measured: soul's
|
||||
* in-process awareness loop, three POST beats on the engram, a 600s ticker,
|
||||
* two resident Python services, and three cron entries at 23:55 / 06:00 /
|
||||
* 08:30. That last trio is a sleep cycle written as crontab. Seven systems
|
||||
* dreaming into one graph with no owner for dreaming is what crashed soul on
|
||||
* this date; the contention was the symptom of the missing owner.
|
||||
*
|
||||
* Every ticker is the diagnostic. A StartInterval, an Hour/Minute, a
|
||||
* POST-to-beat — each marks a place where an intrinsic rhythm was replaced by
|
||||
* an external clock, which is a supervisor invented for something that should
|
||||
* be a property of the substrate.
|
||||
*
|
||||
* The engagement signal already existed and needed no invention:
|
||||
* _http_conn_active under _http_conn_mu is exactly "capacity currently
|
||||
* claimed." The dreamer waits for it to reach zero and yields the moment it
|
||||
* does not. That is the anticorrelation, literally rather than by analogy.
|
||||
*
|
||||
* CONTRACT: the handler performs ONE step and returns. The runtime cannot
|
||||
* preempt El code, so interruptibility is at step granularity — a step must
|
||||
* be small enough that a request arriving mid-step is not made to wait. It
|
||||
* returns non-zero if it did work. Returning zero means "nothing to
|
||||
* consolidate," and the dreamer then blocks until activity changes rather
|
||||
* than spinning. There is no timer anywhere in this file for this purpose,
|
||||
* and adding one would be the defect described above.
|
||||
*
|
||||
* `depth` is derived from CONTINUOUS unclaimed time: a brief gap affords a
|
||||
* shallow recombination; a long quiet affords a deep one. Same process. Sleep
|
||||
* is where unclaimed capacity is greatest, not where the process lives. */
|
||||
typedef el_val_t (*dream_fn)(el_val_t depth);
|
||||
static char* _dream_handler = NULL;
|
||||
static int _dream_started = 0;
|
||||
|
||||
static int64_t dream_now_ms(void) {
|
||||
struct timespec ts;
|
||||
#if defined(CLOCK_MONOTONIC)
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
#else
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
#endif
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static dream_fn dream_lookup(void) {
|
||||
dream_fn out = NULL;
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
if (_dream_handler && *_dream_handler)
|
||||
out = (dream_fn)dlsym(RTLD_DEFAULT, _dream_handler);
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void* dream_loop(void* unused) {
|
||||
(void)unused;
|
||||
int64_t idle_since = 0;
|
||||
for (;;) {
|
||||
/* Wait for unclaimed capacity. Any engagement resets the depth clock:
|
||||
* depth reflects CONTINUOUS quiet, so an interruption starts it over. */
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
while (_http_conn_active > 0) {
|
||||
idle_since = 0;
|
||||
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||
}
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
|
||||
int64_t now = dream_now_ms();
|
||||
if (idle_since == 0) idle_since = now;
|
||||
int64_t quiet = now - idle_since;
|
||||
|
||||
/* Depth from unclaimed capacity. Not a schedule — a gradient. */
|
||||
int depth = quiet < 1000 ? 1 /* a gap between requests */
|
||||
: quiet < 30000 ? 2 /* a lull */
|
||||
: quiet < 300000 ? 3 /* sustained quiet */
|
||||
: 4; /* deep: the "sleep" case */
|
||||
|
||||
dream_fn fn = dream_lookup();
|
||||
if (!fn) return NULL; /* handler vanished: stop, do not spin */
|
||||
|
||||
el_val_t did_work = fn((el_val_t)depth);
|
||||
|
||||
if (!(int64_t)did_work) {
|
||||
/* Nothing to consolidate. Do NOT poll — block until engagement
|
||||
* changes. If there is nothing to dream about, wait for something
|
||||
* to happen rather than asking again on a timer. */
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
while (_http_conn_active == 0)
|
||||
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
idle_since = 0;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* dream_set_handler(name) — register the consolidation step and start
|
||||
* dreaming. Resolves by dlsym against the running binary, the same mechanism
|
||||
* http_set_handler uses: every El `fn name(...)` compiles to a global C symbol
|
||||
* with that exact name. Inert until called, so a program that never registers
|
||||
* one simply never dreams and pays nothing. */
|
||||
void dream_set_handler(el_val_t name) {
|
||||
const char* n = EL_CSTR(name);
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
free(_dream_handler);
|
||||
_dream_handler = el_strdup(n ? n : "");
|
||||
int start = (!_dream_started && n && *n && dlsym(RTLD_DEFAULT, n) != NULL);
|
||||
if (start) _dream_started = 1;
|
||||
pthread_mutex_unlock(&_http_handler_mu);
|
||||
if (start) {
|
||||
pthread_t tid;
|
||||
if (pthread_create(&tid, NULL, dream_loop, NULL) == 0) pthread_detach(tid);
|
||||
else { pthread_mutex_lock(&_http_handler_mu); _dream_started = 0; pthread_mutex_unlock(&_http_handler_mu); }
|
||||
}
|
||||
}
|
||||
|
||||
static http_handler_fn http_lookup_active(void) {
|
||||
http_handler_fn out = NULL;
|
||||
pthread_mutex_lock(&_http_handler_mu);
|
||||
@@ -1792,7 +1914,12 @@ static void* http_worker(void* arg) {
|
||||
/* release a slot */
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
_http_conn_active--;
|
||||
pthread_cond_signal(&_http_conn_cv);
|
||||
/* BROADCAST, not signal (2026-08-16): the ambient consolidation thread
|
||||
* waits on this same condvar for _http_conn_active == 0. cond_signal wakes
|
||||
* exactly one waiter, so the accept loop could take every wake and starve
|
||||
* the dreamer indefinitely. Both wait sites re-check their predicate in a
|
||||
* while loop, so broadcasting is safe. */
|
||||
pthread_cond_broadcast(&_http_conn_cv);
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
return NULL;
|
||||
}
|
||||
@@ -2139,7 +2266,12 @@ static void* http_worker_v2(void* arg) {
|
||||
el_closesocket(fd);
|
||||
pthread_mutex_lock(&_http_conn_mu);
|
||||
_http_conn_active--;
|
||||
pthread_cond_signal(&_http_conn_cv);
|
||||
/* BROADCAST, not signal (2026-08-16): the ambient consolidation thread
|
||||
* waits on this same condvar for _http_conn_active == 0. cond_signal wakes
|
||||
* exactly one waiter, so the accept loop could take every wake and starve
|
||||
* the dreamer indefinitely. Both wait sites re-check their predicate in a
|
||||
* while loop, so broadcasting is safe. */
|
||||
pthread_cond_broadcast(&_http_conn_cv);
|
||||
pthread_mutex_unlock(&_http_conn_mu);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -669,6 +669,11 @@ el_val_t engram_prune_telemetry(el_val_t older_than_ms);
|
||||
/* Largest byte length <= max_bytes that does not split a UTF-8 codepoint.
|
||||
* Bounded by bytes, not codepoints, so truncated strings never grow. */
|
||||
size_t el_utf8_safe_len(const char* s, size_t max_bytes);
|
||||
/* Register the ambient-consolidation step and start dreaming. Resolved by
|
||||
* dlsym, like http_set_handler. The handler performs ONE step and returns
|
||||
* non-zero if it did work; returning zero parks the dreamer until engagement
|
||||
* changes. There is no schedule and must never be one. */
|
||||
void dream_set_handler(el_val_t name);
|
||||
|
||||
el_val_t engram_node_count(void);
|
||||
/* Attach a Geometry to an existing node, and read the attached width back.
|
||||
|
||||
@@ -438,6 +438,41 @@ GeoDescriptor* engram_geometry_descriptor(
|
||||
}
|
||||
store_edges_free(es,ne);
|
||||
}
|
||||
/* PER-EDGE DISCORD (2026-08-16). The loop above has, for every internal
|
||||
* edge, BOTH the association strength w and the semantic proximity cs —
|
||||
* and threw both away into accumulators, keeping one correlation per
|
||||
* region. That aggregate is why curiosity looked like a search problem:
|
||||
* a region holding one violently disagreeing edge and one violently
|
||||
* agreeing edge reports co_registration ~ 0, so the disagreements cancel
|
||||
* and the summary destroys exactly what it was built to reveal. Measured:
|
||||
* only 4 of 375 live neighborhoods have negative co_registration, while
|
||||
* 31 sit at zero — almost certainly hiding sites that averaged out.
|
||||
*
|
||||
* Whether use and meaning agree is a property of EACH EDGE. Both are
|
||||
* standardized within the region (z-scores from the accumulators already
|
||||
* gathered, so no second statistic and no constant), and
|
||||
* discord = z(cs) - z(w)
|
||||
* is how much closer in meaning an edge is than its use-strength would
|
||||
* predict, in region-relative units.
|
||||
* discord > 0 : near in meaning, not linked by use
|
||||
* discord < 0 : linked by use, far in meaning
|
||||
* Both are surprising; |discord| is the nucleation strength. There is no
|
||||
* threshold — the magnitude is the signal. */
|
||||
double mx = cr_n>0 ? cr_sx/cr_n : 0.0, my = cr_n>0 ? cr_sy/cr_n : 0.0;
|
||||
double vxr = cr_n>1 ? (cr_sxx - cr_sx*cr_sx/cr_n)/(cr_n-1) : 0.0;
|
||||
double vyr = cr_n>1 ? (cr_syy - cr_sy*cr_sy/cr_n)/(cr_n-1) : 0.0;
|
||||
double sx = vxr>1e-18 ? sqrt(vxr) : 0.0, sy = vyr>1e-18 ? sqrt(vyr) : 0.0;
|
||||
for(int e2=0; e2<n_edges; e2++){
|
||||
edges[e2].discord = 0.0;
|
||||
int ia=(int)edges[e2].a, ib=(int)edges[e2].b;
|
||||
if(!(ms.emb[ia] && ms.emb[ib])) continue; /* no meaning to disagree with */
|
||||
if(sx<=0.0 || sy<=0.0) continue; /* region has no spread: nothing stands out */
|
||||
double cs2 = ccos(ms.emb[ia], ms.emb[ib], GM, dim);
|
||||
double zx = (edges[e2].eff_weight - mx)/sx;
|
||||
double zy = (cs2 - my)/sy;
|
||||
edges[e2].discord = zy - zx;
|
||||
}
|
||||
|
||||
double co_reg=0;
|
||||
if(cr_n>=2){
|
||||
double cov=cr_sxy - cr_sx*cr_sy/cr_n;
|
||||
|
||||
@@ -40,7 +40,11 @@ typedef struct {
|
||||
|
||||
/* One skeleton edge (indices into members[]). eff_weight = weight*(1+0.5*hebb),
|
||||
* clamped to 1.0 — the effective propagation strength eg_edge_eff_weight uses. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; } GeoEdge;
|
||||
/* discord = z(semantic proximity) - z(association strength), standardized
|
||||
* within the region. How much closer in meaning this edge is than its use
|
||||
* predicts. >0 near in meaning yet unlinked by use; <0 linked by use yet far
|
||||
* in meaning. Both surprising; |discord| is nucleation strength. No threshold. */
|
||||
typedef struct { uint32_t a, b; double eff_weight; double hebb; double discord; } GeoEdge;
|
||||
|
||||
/* A compact principal axis of the ellipsoid: unit direction in R^dim + extent
|
||||
* (sqrt of the covariance eigenvalue = the ellipsoid's half-width along it). */
|
||||
@@ -76,6 +80,12 @@ typedef struct {
|
||||
GeoEdge* edges; /* strong internal hebb edges = the backbone */
|
||||
int k_core; /* the maximum core number present in the skeleton*/
|
||||
/* ── diagnostics ── */
|
||||
/* DEPRECATED — see GeoEdge.discord. This aggregates a PER-EDGE property
|
||||
* into one scalar per region, so opposing disagreements cancel and the
|
||||
* summary hides the sites it was meant to expose. Retained only because
|
||||
* it is embedded in the persisted GEO1 blob; removing it is a format
|
||||
* migration and must not ride along with this change. Nothing new may
|
||||
* read it. */
|
||||
double co_registration;/* corr(hebb strength, semantic proximity) over */
|
||||
/* internal edges: >0 = geometries agree (reify); */
|
||||
/* <0 = disagree (surprising links / dream cands). */
|
||||
|
||||
Reference in New Issue
Block a user