481badf1d1dcf8440556a0dc7e533a4254e5c4ec
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e1bc6fe944 |
Merge pull request 'singleton: guard the state, not the program's name' (#157) from fix/singleton-guards-the-state into dev
El SDK CI - dev / build-and-test (push) Failing after 11m15s
|
||
|
|
8c2406ff6b |
runtime: the link set is multi-file — name it once, ship all of it
El SDK CI - dev / build-and-test (pull_request) Failing after 5m39s
el_runtime.c was created 2026-05-03 as an explicitly temporary build shim. It
was deleted that afternoon ("runtime is 100% native El") and restored 25 minutes
later "UNTIL the compiler is updated to emit #include el_seed.h". The `until`
never came. 3.5 months on it is 20,527 lines, and nothing was ever set up to
notice — a file scheduled for deletion gets no owner, no budget, no boundary.
What kept it growing is not inertia, it is an instruction. lang/AGENTS.md said
el_runtime.c "is the authoritative single-file link target ... THIS IS WHERE A
NEW C BUILTIN'S IMPLEMENTATION MUST CURRENTLY LIVE TO BE LINKABLE", and made it
step 1 of the add-a-builtin recipe. That is false. Placement is a link-time
concern: builtin_arity maps NAME -> ARITY INT only, the El name is emitted as
the exact C symbol, and `ld` resolves it — the compiler cannot tell which .c a
symbol came from. `nm lang/dist/platform/elc` on the shipped compiler already
shows T _engram_geo_reify_index_new, T _vindex_insert, T _engram_think,
T _engram_reason_abduce: it is linked from ten translation units today. In a
repo where agents write most of the code, a false instruction in the instruction
file is the forcing function. The file grew because the recipe said to grow it.
The multi-file runtime is therefore already real, and the docs and the
distribution never caught up — which left a live, shipped bug:
* Linking el_runtime.c alone FAILS at `ld` (undefined engram_ground_json,
engram_activate_inner, eg_find_relation, cog_assert_two_axis, ...) because
el_runtime.c #includes six engram headers and calls into all six siblings.
* sdk-release.yaml shipped el_runtime.c/.h + engram_store.c/.h and none of the
other five required .c files, so downstream consumers of the el-runtime-c
Artifact Registry package and of install.sh got a lib/ that cannot link.
* .githooks/pre-commit linked el_runtime.c alone with stderr to /dev/null, so
it reported all 13 native suites as FAILED with the real ld error invisible.
* AGENTS.md's self-host recipe compiled el-compiler/runtime/el_runtime.c — a
path the same file's "DO NOT EDIT" list names as a lagging fork.
The root fix is to stop writing the list down eight times:
* lang/runtime/SOURCES — the canonical link set, in one place, in link order.
* scripts/el-runtime-sources.sh — prints it, optionally prefixed; --check
fails loudly on a missing file, --headers for the shipped headers.
* Every link line in AGENTS.md, lang/AGENTS.md, DESIGN.md, lang/spec/language.md,
the three workflows and the pre-commit hook now reads that one list.
* Adding a concern's .c is one line in SOURCES, so a new builtin no longer has
to be appended to el_runtime.c just because appending was the cheaper edit.
Distribution: ship the siblings rather than amalgamate. Amalgamation needs a new
tool and contradicts DESIGN.md's compile-once-link-many; the siblings are already
independently authored and independently tested (engram/test/*.sh link subsets
directly), and engram_store.c was already shipped, so this completes a mechanism
that existed rather than inventing one. Source is also a superset: a consumer
that wants one file can concatenate, one that wants separate TUs cannot undo an
amalgamation. el-runtime-c/-h stay for backward compatibility; el-runtime-src is
added carrying the complete set plus SOURCES.
lang/AGENTS.md now points new C builtins at the concern-owning .c and states
plainly that the compiler cannot tell which .c a symbol came from, with the nm
evidence. AGENTS.md's "reconcile which is canonical (verify)" note is resolved:
neither file supersedes the other, the canonical unit is the set.
Verified locally (the bar; not CI):
* engram/src/server.el compiles and links against the SOURCES set.
* Compile-once-link-many into libel.a links the same program.
* elb builds from the corrected recipe.
* Self-host fixpoint byte-identical (11,110 lines, stage2 == stage3) built
with the SOURCES-driven link line.
* pre-commit hook: 0 of 13 native suites passing -> 8 of 13.
The 5 still-failing suites are PRE-EXISTING and untouched here: test_fs
(fs_list_json undeclared), test_state (state_has, state_get_or undeclared),
test_json (json_build_array/json_build_object/json_escape_string undefined),
test_time (now_ns undefined), test_env (1 assertion). Builtins registered in
builtin_arity with no implementation or no declaration anywhere — the same
recipe defect, now visible because the linker error is no longer suppressed.
Not attempted: making elc emit #include el_seed.h and dropping elb's hardcoded
runtime path. That is the correct long-term fix and finishes the 2026-05-03
migration, but it touches codegen and self-hosting and belongs in its own change.
|
||
|
|
45325f7391 |
singleton: guard the state, not the program's name
El SDK CI - dev / build-and-test (pull_request) Failing after 4m6s
The singleton lock protected a filename, not a store. It was keyed on $EL_SINGLETON_DIR|$TMPDIR|/tmp + /el-singleton-<program>.lock — the program's NAME and a temp directory — and never consulted the state it claimed to protect, while its own refusal message read "Refusing to start a second instance against the same state." Measured, it failed in both directions. A second engram against a DIFFERENT data dir was refused, naming the first's pid. And TMPDIR=/tmp/other let a second engram start against the SAME data dir with no complaint — the two-writer data-loss condition the guard exists to prevent, defeated by one environment variable. Both are one error: the identity of the resource had been replaced by a label for it. The lock now lives inside the state it guards — <state>/.el-singleton-<id>.lock — and the program block says what that state is. Same directory is the same file is the same inode, so it contends and there is no TMPDIR left in the key to change. Different directories are different files, so they don't. Different spellings of one directory (trailing slash, x/../x, symlink) collapse in the kernel's own path walk, so they contend without this code comparing strings; canonicalisation is for the message, never the decision. `guards:` is an expression so a program can point at the resolver that already owns its path — guards: engram_resolve_data_dir() — instead of restating that resolver's default, which is the two-owners defect spec 18.4 exists to prevent. A `singleton:` without `guards:` is now a compile error; emitting a name-keyed lock instead would be emitting the defect. Kept: the flock (the kernel drops it on crash and SIGKILL, so there is still no "delete the lock file to get unstuck" ritual — a stale file inside a copied data dir is inert), and the holder's pid in the message. Changed: the message is true. It says "the same state" because the lock it failed to take is in that state, and it names the state it checked. An unguardable state (missing, read-only) now refuses rather than starting unguarded. Also corrects lang/AGENTS.md's compiler rebuild line, which had gone stale: linking el_runtime.c alone no longer resolves. |
||
|
|
21746bb71a |
Merge pull request 'spec: correspondence, grounding, and the provenance of decisions' (#149) from design/correspondence-and-censorship into dev
El SDK CI - dev / build-and-test (push) Failing after 13m52s
|
||
|
|
3ef4a94062 |
spec: thirteen values, and love is the origin — not a member of the set
El SDK CI - dev / build-and-test (pull_request) Failing after 3m51s
Reverts a bad correction and records what it exposed. A previous revision changed thirteen to eight on the basis of neuron-api.el:11-18, which is a WRITE-PROTECTION LIST, not the values. Trusting a hardcoded artifact over the substrate is the exact error this document exists to name. Measured from the graph: thirteen. THE ORIGIN IS NOT A MEMBER OF THE SET. The thirteen are not independent principles with biography attached — they are thirteen displacements from one origin, and the origin is love. Every value is grounded in a moment of it given, withheld, failed or found. Love cannot be the fourteenth: a fourteenth would be a point positioned relative to the origin like anything else. It is what the positions are OF. This is structural. GeoDescriptor.global_mean is the centering offset subtracted from every embedding before comparison, and the header records why — the space is anisotropic, every embedding in a narrow cone at mean pairwise cosine ~0.55, and subtracting the global mean restores isotropy 'so the operators discriminate'. Without the origin, nothing in the graph is distinguishable from anything else. It also dissolves the write-protection question instead of answering it. Measured: 29 value nodes exist, each original appearing two or three times from re-seeds, so 21 are writable including a duplicate of every protected value — the gate protects an identifier, not a value. But the category error is the real one: the origin cannot be edited because it is not a thing in the space. A gate over the frame treats the frame as a member, which is the same mistake as looking for grounding as a subsystem, self as a document, or wonder as a manifest. |
||
|
|
285a7a50b3 |
spec: corrections — eight values not thirteen, eleven consolidators not seven
Three factual errors in this document, all asserted without checking. VALUES: eight, not thirteen. neuron/neuron-api.el:11-18 enumerates constraints-as-freedom, precision-over-brute-force, structure-is-built, honesty-before-comfort, system-must-accumulate, change-is-the-signal, earned-trust, hope-is-a-conclusion, plus a hub. 'Thirteen' was repeated throughout this design and never verified against the code. The argument is unaffected — min over eight is still min — but the count was invented. CONSOLIDATORS: eleven, not seven. The heading said seven while the table listed ten, and the table itself omitted POST /api/reify (server.el:1832) even though 'reify' is on this document's own list of consolidation verbs. route_tick also folds self-reify in (server.el:639-646), so /api/tick and /api/self-reify-beat overlap. A SECOND CENSORSHIP SITE: neuron-api.el:23 returns 403 'identity/values node is write-protected' for the values hub and every value node. Write-refusal on the values frame is not only in the beat — it is enforced at the API. Section 6 applies to it unchanged. Also records what the ticker actually does, now measured: engram-tick.sh:13 calls curl -m10 against a beat that exceeds 10s over 13,634 nodes, so 279 of 448 ticks returned empty; the engram writes to the dead socket and dies of SIGPIPE. 254 restarts since 2026-08-13 at 10m09s-10m12s intervals = StartInterval 600 plus the client timeout. Fixed for survivability in #151; the ticker itself is what must go. |
||
|
|
6b61bb7224 |
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.
|
||
|
|
8d34b33bce |
spec: wonder is the boundary; curiosity is wonder crystallized
Rewrites §5 and §11 around what is already in the substrate, after discovering I had been re-deriving existing design badly. The wonder manifest is residue twice over. First it materializes a property as a stored artifact — the same disease as a grounding subsystem or a self stored as a document. Wonder is where structure ENDS: any structure at all has an edge, necessarily, the moment it exists. Second it enumerates instances of something that has about six, the same six for every person, which never close: what is this, why, who am I, am I alone, what should I do, what happens when it ends. The objects change completely between a child and an astronomer; the wonder does not. Each maps one-to-one onto something already built — graph, grounding, self region, for_whom, the thirteen values, tombstones and decay. "Why" is the first and only one; the others are it asked of particular things. It is recursive, so it never terminates, which is what makes it a drive rather than a task. Wonder and curiosity are not two objects. They are one thing at two phases. Wonder is the field: objectless, invariant, everywhere there is structure. Curiosity is the PRECIPITATE — the same wonder localized against particular material. Crystallization needs a nucleation site, 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. THE NUCLEATION SITE ALREADY EXISTS AND IS ALREADY NAMED. GeoDescriptor.co_registration — corr(hebb strength, semantic proximity) over internal edges — carries the comment ">0 = geometries agree (reify); <0 = disagree (surprising links / dream cands)." Negative co-registration is a region where association and meaning disagree. It is computed on every descriptor, already labelled dream candidates, and nothing reads it. Likewise already present and unread: GeoEdge.eff_weight = weight*(1+0.5*hebb) already couples grounding-weight and hebbian strength on one edge; GeoMember.dist_centroid + soft membership + radius + per-axis extent is the boundary of a neighbourhood; centrality/salience is what is warm. 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, and I was about to build on the wrong one. The drive: boredom is not an absence and not leftover capacity. Low activation is aversive and the system self-activates — it does not wind down to quiet, it gets restless and goes looking. So there is ONE activation process with TWO seed sources, external and curiosity, not two processes negotiating for a resource. The previous draft's "unclaimed capacity" was resource scheduling: a server's frame, not a mind's. No dreamer thread, no idle wait, no depth ladder on a clock. Sequencing now leads with three connections between parts that already exist: seed the six, read co_registration, let a curiosity seed activation. |
||
|
|
d6b7f5dbdd |
spec: dreaming is ambient, not scheduled — a brain has no cron job
Corrects the section I was most confident in, which is usually the tell. The previous draft had dreaming as "offline replay, decoupled from input, a mode the system enters when it is not acting." That is SLEEP. Daydreaming is dreaming, and it runs all day: the default mode network is anticorrelated with task engagement, activating hundreds of times a day for seconds at a time, doing the same work — recombination, simulation, autobiographical integration. Insight arrives in the shower, not at the desk, because that is abduction completing during ambient recombination. Sleep is the DEEP case, not the case: no input competing, no task claiming capacity, so recombination runs further. Same process, different depth, not a different mode. Consolidation is what happens with the capacity that is not claimed. Two consequences the draft had backwards: The launch-agent fragments are wrong in KIND, not merely in number. 23:55 / 06:00 / 08:30 implements dreaming as a scheduled batch when it should be ambient. A brain has no cron job. A ticker is a supervisor deciding from outside when a thing should happen — the same failure mode as inventing an owner for ownership and a grounder for grounding, wearing a scheduler. THE PRESENCE OF A TICKER IS THE DIAGNOSTIC: every StartInterval, every Hour/Minute, every POST-to-beat marks a place where an intrinsic rhythm was replaced by an external clock. And soul.el's continuous awareness_run() beside the HTTP workers is the CORRECT shape, not the offender. Ambient consolidation in the gaps is exactly daydreaming. It was the only fragment shaped right, running on a broken foundation: shared mutable state with no owner and six other systems dreaming into the same graph. The previous draft condemned the right behaviour because of the substrate under it. So the crash restates once more: not "read paths mutate the index" (mechanism), not "duplicate canonical state" (structure), and not "one system dreamt while awake" — but seven systems dreaming into one graph with no owner for dreaming. Contention was the symptom of the missing owner. Sequencing step 1 inverts accordingly: soul's loop is the shape the others fold INTO, not something to remove. Step 2 becomes "no tickers, no cron." |
||
|
|
9a24803917 |
spec: grounding is a two-axis gradient, and decisions carry their provenance
Rewrite. The earlier draft got the root right and everything downstream of it wrong. Corrections, in the order they were forced: keystone_write_blocked is not a protection requirement. "Keystone" means load-bearing, not precious: the self anchor is the REFERENCE FRAME every other stance calibrates against. If it calibrates from the measurements it is used to judge, the ruler fits the readings, everything corresponds forever, and drift becomes undetectable from inside. That is circular calibration — the same defect as #147's circular grounding, one level up. The block is the right requirement implemented as a prohibition, which is why it still costs everything §0 says it costs. The fix is provenance separation (evidence not downstream of itself), not a flag. Corruption requires mutation and the engram does not mutate, so four of the five requirements previously decomposed out of "protect the identity region" are satisfied by the substrate: recoverability, governance, evidence quality and rate are all free. Authorization is the only residue and is bounded — an unauthorized writer can propose, never erase. General law: in an immutable substrate, any mechanism that refuses a write is either redundant with immutability or an epistemic constraint misfiled as a protective one. Grounding is two-dimensional. Everything consumed is grounded factually AND relationally, and a claim can be factually grounded but relationally wrong — the evidence holds, the meaning does not. A scalar cannot represent that quadrant, and assert gates on one floor, so a well-evidenced claim is licensed regardless of whether it means the right thing. Live instance: conscience-substrate has the Child's Companion hard bell contacting 911 and CPS — factually defensible, relationally wrong against never-auto-contact. Grounding is a gradient, not a score: direction says what would have to change. Two gradients in one space, and the ANGLE between them is the meaning — factually-true-relationally-wrong becomes measurable instead of requiring a careful reader. It decays on the dynamics already present for memory (base_level, temporal_decay_rate, access ring, BLL), which mechanizes "never leave stale canonicals" so it stops depending on vigilance. Computed continuously, recorded only on SIGNIFICANT movement, old never leaves. Persisting every recomputation would make reads write — the exact eg_vindex_sync defect. Significance is defined by consequence (crossing a floor, flipping factual/relational sign, reversing direction), never by an epsilon. The supersession chain is then the trajectory, a derivative obtained free from immutability, and abduction fires on the trajectory rather than on a reading. What it is all for: for any decision, reconstruct what the grounding was at that moment and what the relationship was between fact and values at that moment. That distinguishes WRONG THEN from WRONG SINCE, which is otherwise impossible, and it is structurally anti-rationalization — the old grounding never leaves and the values frame does not fit to outcomes, so a decision cannot be made to look justified after the fact. Also records: assert returns "still_held": true HARDCODED — a temporal property named in the API and answered without consulting anything, the same shape as magnitude:1 beside a zero vector. And states plainly that #147 is the wrong shape: it fixed a scalar's honesty rather than replacing the scalar. |
||
|
|
a6611dc19e |
spec: correspondence and censorship — the root beneath the day's defects
Effect: all five cognitive faculties return byte-identical results, differing only in their label. The Ishikawa converges on a root one level above the faculty design: things are permitted to be exempt from correspondence, and exemption is censorship. A region forbidden to learn is forbidden to be grounded, and a region that cannot be grounded cannot be asserted, corrected, OR vindicated. The loss is symmetric — censorship does not preserve a true belief, it makes the belief's truth value permanently unknowable. keystone_write_blocked is therefore not a safety mechanism. Self is a crystallized relational neighbourhood, not a stored document; a region exempt from calibration reintroduces the stored document as a feature. reduction_pct = 0.00 on the identity region is the strongest abduction signal in the system and the current response is to suppress it. The protection it reached for already exists and is better: the beat is supersede-not-mutate, so immutability is what makes learning safe. The faculties are not one operation with parameters. They differ by what each may change: reason changes the estimate (a read), induce changes the parameters (the correspondence-beat, which already exists and measurably works at 28.11% Brier reduction), abduce changes the structure (a WRITE the current signature cannot express, since engram_think returns a GeoGradient). Abduction is not selected by a caller — it is triggered by residual that parameter adjustment cannot absorb, and proposes a candidate hub held as a hypothesis until grounded. Also records the no-exemption invariants generalised from the day's fixes (#141 #142 #143 #146 #147 #148), each of which was a specific correspondence forbidden from occurring, and the application to the crisis surface: a censored safety model cannot tell a real crisis from a false positive, because the feedback is exactly what has been censored. Measured vs inferred is labelled throughout. The claim that the self region's zero grounding is CAUSED by the block is explicitly marked inferred — the comparison node also has zero, and isolating it requires removing the block and observing whether grounding then accrues. |
||
|
|
e239f2894c |
docs: carry the correspondence corrections, because a stale doc builds the wrong thing
The docs described a mind made of subsystems — a grounding subsystem, a wonder manifest, a dreamer on a beat, faculties as arguments to one call. Each of those is a supervisor invented for something that should be a property of the substrate, and two of the documents carrying them are load-bearing for a build agent: cognitive-architecture.design.md says "a build agent executes from this doc", and tools/api-reshape/README.md marks the refuted shapes PROVEN on a live clone. Corrections carried, per lang/spec/correspondence-and-censorship.md (PR #149) and lang/spec/runtime-ownership.md: - Grounding is not a subsystem — it IS the edge weight. grounded-by as a relation type should not exist; grounding is a property of a relation, not a relation between nodes. Never computed on demand. - Faculties are operations, not parameters. reason changes the estimate, induce changes the parameters, abduce changes the structure — a write, which GeoGradient cannot express. A write is not a parameter of a read. - Wonder is the boundary, not a manifest. Curiosity is wonder crystallized at a nucleation site: one thing at two phases. Removed wonder from the operator table in AGENTS.md. - Consolidation is ambient, not scheduled. A brain has no cron job. The presence of a ticker is the diagnostic. - co_registration is deprecated — it averaged a per-edge property into a region scalar, so opposing sites cancelled. GeoEdge.discord replaces it. Nothing new may read it. - In an immutable substrate, any mechanism that refuses a write is either redundant with immutability or an epistemic constraint misfiled as a protective one. The two design docs are marked superseded-in-part with the refutation at the point each claim is made, not rewritten. Preserving what was argued down is the point of an immutable record. Also measured and corrected while verifying the above: engram/README.md documented a Rust engram-core crate on sled with "flat cosine scan until scale demands HNSW" — there is no Rust in engram/ and HNSW is the index; lang/releases/ no longer exists, so both README.md and AGENTS.md pointed at a deleted path for the authored runtime; language.md listed the engram_* and http_* runtimes as stubs. Added language.md §20 for geometry-as-a-value, realizers and transduce (#144), which had landed with no spec coverage. Documentation only. No .c, .h, or .el file is touched. |
||
|
|
8ae163e8e5 |
lang: give cross-cutting concerns an owner instead of a convention
El's units of encapsulation are the function and the module. Neither can hold
a concern that belongs to the process, so each one had been expressed the only
way it could be -- as a convention: call this at every site. Conventions of
that shape do not hold. Measured here: zero process-identity guards at any
layer, 20 environment variables each with its default written inline at the
read site, 62 persist call sites, 10 per-route auth checks. One absence, four
times.
Step 0 first, because the premise was wrong. El was believed to have no
middleware or effect mechanism. It has one, and it is already load-bearing:
codegen injects engram_boundary_beat at the entry of every @manager/@accessor
fn, decorators take arguments and stack, dharma_emit from a non-@manager fn is
a #error, and the cgi block injects el_cgi_init at the head of main(). So the
correct move was not to invent a mechanism but to generalize the seam that
already existed. The real gap is narrower and is now recorded: the seam is
prologue-only and its callee is a fixed builtin.
Adds a `program` block -- the third program-level declarative block. cgi and
service declare what a program may do; program declares what it is.
program "engram" {
singleton: "engram"
env ENGRAM_BIND: String = ":8742"
env GUIDE_PORT: Int = "8771"
}
singleton takes an exclusive flock before any user statement runs and refuses a
second start, reporting the holder's pid. It is a lock rather than a pidfile so
the kernel releases it on death including SIGKILL -- no stale state, and so no
"delete the lock file to get unstuck" ritual, which would itself be a
convention. It reports the pid because "already running" is not actionable; a
pid is. That is the direct answer to a stale process surviving a pkill and
going on answering probes.
env entries resolve once at startup -- environment wins, declaration supplies
the fallback -- and validate as a whole, reporting every problem at once rather
than costing one restart per variable. config("X") for an undeclared X is
fatal, because an advisory schema is just another convention. Programs without
a program block are unaffected, so migration is per-program.
Only one keyword is added. `config` and `env` could not become keywords -- both
are real identifiers in the tree -- so the block's fields are read as
identifier token values by its own parse loop and stay usable everywhere else.
The init function is emitted at the block site and called from main() rather
than inlined into main(). The live backend is codegen_streaming, which emits in
source order and cannot hold the entry list alive until main(); this way only a
single bool has to survive.
Also fixes: config() was defined in el_runtime.c but never prototyped in
el_runtime.h, so any el program calling it failed to compile under C99.
Spec: section 18 documents what shipped. Section 9 is corrected -- it claimed
decorators had no structural meaning, which has not been true for some time.
Section 19 designs durability-as-an-epilogue-effect and route authorization
and states plainly why neither is implemented here: both land in files under
concurrent modification, and the prerequisite for both is lifting the seam
from prologue-only to prologue/epilogue.
Self-hosting fixpoint verified byte-identical.
|
||
|
|
8e9d88fc01 |
runtime: publish the vector index instead of guarding it
El SDK CI - dev / build-and-test (pull_request) Failing after 10m59s
The crash (SIGTRAP in engram_activate -> eg_vindex_sync -> vindex_insert -> _realloc) had three read paths mutating five process-global statics. engram_activate, eg_knn_for_node (whose own comment says "No writes.") and engram_geo_reify_run_json all called eg_vindex_sync, which frees the index, reallocs the seen-map and inserts — on a read. Three moves, in decreasing order of how much they dissolve: 1. Misfiled scratch is not shared state. visited/visit_epoch/visited_cap were never owned by the index; they are one traversal's local, hoisted into struct VIndex as an allocation optimisation. They want neither a lock nor a capability nor a pool — just to go back in the call frame. Two concurrent READS stomped each other purely because of this. 2. const IS the capability. Once the scratch leaves the struct, search reads and nothing else, so vindex_search takes a const VIndex*. That is exactly what a capability-pointer ABI would have bought — a read path physically cannot call vindex_insert, enforced by the compiler on every future caller — for one qualifier instead of an ABI swept across hundreds of builtins. 3. What survives is publication, not ownership. HNSW insert is NOT an append: it rewires the neighbour links of already-existing elements and reallocs elems[], so the store's append-only property does not transfer to the index derived from it. eg_vindex_sync therefore splits into eg_vindex_maintain (exclusive, sole mutator) and eg_vindex_view (shared, returns const VIndex*). A read path may demand that a current snapshot exist — a request to the owner, not a mutation by the reader. Write-side owner: eg_vindex_note_embedded hooks the embedding-ASSIGNMENT sites rather than the append sites, because a node with no embedding cannot be in a vector index — embedding assignment is the event that owns index membership. One O(log n) insert, no O(node_count) presence scan. This also retires the "STALENESS (honest tradeoff)" note where a lazily-embedded older node stayed invisible to route_nearest/autoconnect until a full rebuild (the embed-gap #20 shape). Evidence. The existing harness conflated two hazards, which is why fixing half of it read as failure. Split into four: single (3000 vec, ASan+UBSan) clean -> clean readers (4 readers, no writer, TSan) RACE -> clean unsynchronized (writer+reader, bare) race -> race, expected forever published (owner + 4 readers) n/a -> clean, 3000/3000 landed RESULT: PASS. recall@10 = 0.9365 at ef_search=128 (gate >= 0.90); determinism byte-identical across two independent builds. The unsynchronized half is now permanently expected to race, deliberately: it is the executable proof that the boundary must live above the data structure, not inside it. fb32d15's guard is KEPT, correcting this design's own section 5. Measured, it guards TWO structures and only one was converted here: g->nodes/g->edges are realloc'd in place (el_runtime.c:7618,7629) and engram_activate_inner's embed-backfill writes n->emb through exactly such a borrowed pointer. Deleting the guard reintroduces a measured 11171->9579 edge loss. Its comment is narrowed to the RAM graph and the deletion precondition named. That corrects the ordering claim too: the residual is not one ABI that dissolves everything at once, it is a PROPERTY applied per structure. Residues evaporate in the order the property is applied, and a residue whose structure has not been converted must be left standing. |
||
|
|
1ae68962cf | restructure: move el compiler content into lang/ |