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). */
|
||||
|
||||
@@ -138,21 +138,36 @@ It is also why abduction needs no trigger and no threshold. A `structurally_unan
|
||||
|
||||
**And `crystallization` is one primitive appearing twice**: the self is what identity precipitates into from its neighbourhood; a curiosity is what wonder precipitates into from an anomaly. That it shows up in both places without being imported is the evidence it is the right primitive.
|
||||
|
||||
### 5.3 The nucleation site already exists and is already named
|
||||
### 5.3 The nucleation site is per-edge, and the aggregate was hiding it
|
||||
|
||||
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — carries this comment:
|
||||
`GeoDescriptor.co_registration` — *corr(hebb strength, semantic proximity) over internal edges* — carries the comment `>0 = geometries agree (reify); <0 = disagree (surprising links / dream cands)`. It has always been computed, always persisted, and **never read**.
|
||||
|
||||
> `>0 = geometries agree (reify); <0 = disagree (surprising links / **dream cands**).`
|
||||
It is also the wrong shape, and asking whether it should exist at all is what exposed it.
|
||||
|
||||
Negative co-registration is a region where **association and meaning disagree**: things linked by use that are not close in meaning, or the reverse. That is the surprising link, it is computed on every descriptor, it is already labelled *dream candidates*, and **nothing reads it.**
|
||||
Whether use and meaning agree is a property of **each edge**. `co_registration` is a *correlation*: it averages that per-edge property 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.** This is the mean-versus-min error from §3, in different clothes.
|
||||
|
||||
**Measured:** 375 live reified neighbourhoods — 340 positive, **31 at zero**, 4 negative. Read as a count of things to be curious about, that says "four." Read correctly, it says four disagreements were lopsided enough to survive averaging, and the 31 zeros are where opposing sites cancelled.
|
||||
|
||||
It also explains why surfacing curiosity *looked like a search problem*. Once the signal is a per-region number, the only way to find sites is to enumerate regions — there is nothing local left to notice. An O(n) sweep is tolerable at 375 and impossible at a million, and more to the point, **nothing in a mind scans its neighbourhoods to find what is surprising.** The surprise captures attention; salience is bottom-up. A search asks "which of these is odd"; a mind has "something is odd *here*" for free.
|
||||
|
||||
So the disagreement goes back on the edge, where the loop that computed the aggregate already had both halves and discarded them:
|
||||
|
||||
```
|
||||
discord = z(semantic proximity) − z(association strength)
|
||||
```
|
||||
|
||||
standardized within the region from accumulators already gathered — no second statistic, no constant, **no threshold**. `discord > 0`: near in meaning yet unlinked by use. `discord < 0`: linked by use yet far in meaning. Both are surprising, and `|discord|` *is* the nucleation strength; there is nothing to compare it against.
|
||||
|
||||
**Then there is nothing to scan.** The edge carries its own disagreement, activation crossing it encounters that directly, and `|discord|` raises salience on its endpoints as part of the same operation — no separate pass, no supervisor. Curiosity does not search for nucleation sites; it goes where salience already is, which is machinery that exists (`salience`, `background_activation`, `working_memory_weight`, `wm_anchor`).
|
||||
|
||||
`co_registration` is deprecated rather than deleted only because it is embedded in the persisted GEO1 blob; removing it is a format migration and must not ride along. **Nothing new may read it.**
|
||||
|
||||
Adjacent structure already present and likewise unread:
|
||||
|
||||
- `GeoEdge.eff_weight = weight * (1 + 0.5*hebb)` — grounding-weight and hebbian strength already coupled on one edge, per §1.
|
||||
- `GeoMember.dist_centroid` + soft membership + `radius` + per-axis `extent` — the boundary of a neighbourhood, computable now.
|
||||
- `GeoMember.centrality` / `salience` — what is warm.
|
||||
|
||||
*(Correction: `engram_boundary_beat` is NOT this boundary. It is the VBD decorated-function seam, counting `_eg_aff_boundary_ops`. Two different senses of the word.)*
|
||||
*(Correction: `engram_boundary_beat` is NOT this boundary. It is the VBD decorated-function seam, counting `_eg_aff_boundary_ops`. Two senses of the word.)*
|
||||
|
||||
### 5.4 The drive
|
||||
|
||||
@@ -255,7 +270,7 @@ The reviewable question stops being *did it follow the rule* and becomes *what w
|
||||
Three connections between parts that already exist, then the rest.
|
||||
|
||||
1. **Seed *the* wonder questions.** Six nodes. Not a manifest, not maintained, never refilled. They cannot be derived — wonder cannot be bootstrapped from indifference — so they are given once. Zero question nodes exist in 13,630 today.
|
||||
2. **Read `co_registration`.** It is computed on every descriptor and discarded. Negative co-registration is already labelled *dream candidates*; surface them as nucleation sites.
|
||||
2. **Put the disagreement back on the edge** (`GeoEdge.discord`) and let `|discord|` raise salience on its endpoints as part of the same operation. Do NOT scan for nucleation sites — a sweep over regions is a supervisor, and the aggregate that made a sweep necessary is the defect.
|
||||
3. **Let a curiosity seed activation.** One activation process, two seed sources (§5.4). No thread, no scheduler, no capacity check, no timer.
|
||||
|
||||
Then:
|
||||
|
||||
Reference in New Issue
Block a user