c9da8d917a3973ff8dcf7f8157b9100c7ad41ae7
95 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c9da8d917a |
chore: regenerate dist/soul.c after rebasing onto main
A clean rebase is not a consistent build input: git resolves the compiled amalgam as an ordinary file and picks a winner, so dist/soul.c ends up holding one side's code and not the other. The stamp gate catches it; this commit fixes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7f445a4b95 |
feat(engine): tools + agentic loop on the OpenAI wire, and two chat-breaking fixes found proving it
Teaches the OpenAI-format lane (Groq/OpenAI/Grok/Gemini/Ollama) to offer tools,
execute them, and loop — the capability that until now existed only on the
Anthropic wire. The tool-execution, consent, bridge and run-progress machinery is
reused unchanged; only the wire dialect is new.
Two pre-existing defects were found while proving it, and are fixed here because
both silently break chat:
1. PROVIDER WIRING NEVER CONNECTED. The launcher exports SOUL_LLM_PROVIDER /
SOUL_LLM_BASE_URL and puts the provider key in ANTHROPIC_API_KEY + SOUL_API_KEY;
the engine's provider fork read only NEURON_LLM_0_*, which nothing sets in a
customer build. So use_openai was ALWAYS false: every non-Anthropic user's turns
went to api.anthropic.com carrying, say, a Groq key, and came back
"llm unavailable". Proven side-by-side against the pinned round-9 brain
(sha256 15cf7d1b…): identical env, shipped brain = "llm unavailable" both chat
modes with ZERO calls to the configured endpoint; this build = a real answer,
with the probe logging POST /v1/chat/completions and Bearer <provider key>.
Fixed brain-side only (env fallbacks) — no app or launcher change needed.
2. TRUNCATION SPLITS UTF-8 CHARACTERS. The session preload cuts recalled memory at
fixed BYTE lengths (continuity snippet 350; session_preload_bullets per bullet).
A cut landing inside a multi-byte character leaves a dangling lead byte in the
SYSTEM PROMPT, making the whole request body invalid UTF-8 — providers reject it
and the user sees an unexplained failure. Captured from a real body: 18,710 bytes,
decode fails at 18,248 on 'e2', a box-drawing rule (U+2500 = E2 94 80) sliced in
half. Trigger is ordinary content — em dash, curly quote, accented name, emoji,
table border — and it gets MORE likely as memory grows. Shared code: this hit the
Anthropic wire too. Fixed with utf8_safe_slice() applied at BOTH cut sites.
WHAT IS IN THE PORT
- llm_base_url / llm_wire_format / agentic_api_key: fall back to the launcher's own
SOUL_LLM_* names; anthropic deliberately still returns "" so its native path is
untouched (endpoint configurability remains neuron#62).
- openai_tools_json(): Anthropic tool schema -> OpenAI function schema; entries with
no input_schema (Anthropic's server-side web_search) are skipped — they cannot
execute on this wire.
- agentic_tools_no_web(): the standard set minus that server tool.
- openai_agentic_loop(): forked rather than parameterised, so agentic_loop — which
carries every round-7/8/9 fix — is provably untouched. Same envelopes, same state
keys, same consent policy (ask_all / escalate / builtin / always-allow), same
client-bridge contract, same run-progress ledger, same 12-iteration cap.
- ADR-0005 mirrored on this wire: parallel_tool_calls:false is sent explicitly, and
if a provider ignores it we honour the FIRST call and echo only that one, so the
conversation we send is never self-contradictory. The drop is logged loudly.
- The assistant turn echoes the provider's own content bytes (json_get_raw), so a
JSON null stays null and nothing is lost to a decode/re-encode round trip.
- Tool results are embedded already-escaped (dispatch_tool json_safe's them);
truncation trims a dangling escape so a cut can't invalidate the body.
- bridge_save() gains a "wire" scalar and agentic_resume branches on it, so a
suspended turn resumes on the wire it suspended on. Legacy blobs (no field) resume
as anthropic. The field is read from the blob's SCALAR HEAD only — an unbounded
first-match scan would run on into messages_raw, which is model-controlled, and
that is exactly the round-9 resume defect. Pinned by a test.
- Three fork sites: handle_chat_agentic, handle_dharma_room_turn_agentic,
agentic_resume. Tool assembly is computed once per lane at both entry points
(it makes an HTTP call to the connector bridge; it was being paid for twice).
TOOLING THAT DID NOT EXIST
- tests/run-el-test.sh — engine tests were never runnable: elc is a compiler, it
emits C and exits. This emits the test to C, compiles soul.c with main renamed
away, links the rest + the repo-pinned runtime, and runs it. It also COMPUTES THE
VERDICT, because every counted test file's "N passed, M failed" summary is a
permanent 0/0 — the counters increment inside if BLOCKS, which El scoping
discards (9 files; real fix filed as neuron#116). Proven to discriminate with a
deliberately-broken assertion.
- tests/gate-openai/ — deterministic OpenAI-dialect provider stub + scenarios +
driver + hostile modes, and a strict request validator that rejects any
Anthropic-shaped field so dialect leakage fails loudly.
VERIFICATION (rungs named)
- E2E-VERIFIED against a LIVE provider (Anthropic's OpenAI-compatible endpoint,
confirmed live): real answer; a tool call whose out-of-root path was DENIED by the
guard, after which the model refused to claim success ("I won't tell you I did it,
because I didn't"); then a valid path -> file physically on disk with exact content,
honest reply, ledger with per-round entries + {done:true}.
- Deterministic lane gate: 11/12 in both consent configurations (bridge + local);
hostile providers produce no hang and no fabricated answer; the 12-iteration cap
trips with its honest message. The one FAIL is oa-tools-off and is NOT this port —
see "Known, not fixed here".
- ANTHROPIC LANE UNCHANGED: gate9 32/32 on this build and on the pinned round-9
brain; request bytes differ only within the noise band that two runs of the
UNMODIFIED brain also produce (proven with a baseline-vs-baseline control), and
the preload sections — the shared code touched here — are byte-identical.
The rig discriminates: the round-8 brain scores 24/32 on it.
- verify-soul-contract.sh: PASS (27/27 routes, no hard-deletes).
- Unit: test_bridge_serialization 36/36 (incl. 8 new wire/field-order assertions),
test_utf8_slice 18/18, test_agentic_tools 18 PASS / 0 FAIL / 3 documented skips.
KNOWN, NOT FIXED HERE (deliberate)
- Tools:Off on an OpenAI provider still fails: the non-agentic path goes through the
el-runtime provider chain, which appends /v1/chat/completions to a base URL that
already ends in /v1 -> /v1/v1/... 404. Runtime/plain-chat territory, untouched
mid-beta. Note openai_chat_complete() has zero callers — that lane is served
entirely by the runtime chain.
- The 12-iteration cap does not bound a chain of BRIDGED tools (iteration is
per-invocation and resume starts fresh). Parity with the Anthropic lane.
- run_progress resets on each resume, so a client rendering cumulative steps across a
consent pause sees earlier legs vanish. Parity with the Anthropic lane.
- verify-soul-contract.sh needs bash >= 4; under macOS's stock bash 3.2 it dies
instantly with a FALSE red ("local: -n: invalid option").
- Groq-specific live E2E not run: no Groq key exists on this machine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
aad988ecbf |
chore: regenerate dist/soul.c after rebasing onto main
The rebase merged cleanly at source level but left dist/soul.c holding one side's
code and not the other — main's regenerated amalgam vs this branch's. The stamp
gate added in
|
||
|
|
7b86e6f72c |
feat(soul): Stage 1 structural audit as a real route — an annotated characterization, not a score (#91)
`runStructuralAudit` has been an advertised MCP tool with nothing behind it: the
dispatcher GET'd /session/begin and returned that unrelated session digest under
an audit tool's name. Meanwhile the failure the audit would have caught ran
silently for about three weeks — the soul reporting 103,089 nodes while the
engram, which OWNS persistence, held ~79,900, a crash discarding the difference,
and every boot reporting green throughout, because nothing in the system ever
compared the two sides.
WHAT THE PATENT SPECIFIES, AND HOW IT SHAPED THIS
CGI provisional, 05-detailed-description.md, "Stage 1: Structural audit 430".
Two clauses did the design work. First the four things the module evaluates:
the density and typed distribution of causal edges; value/execution-record
consistency; the richness and connectivity of the self-model; and wonder-
manifest authenticity. Second, and decisively: it "produces a coherence
assessment 432 — NOT A BINARY SCORE but an annotated characterization of the
graph's structural properties."
So every finding carries its numbers AND a plain-language note saying what
they mean and how they were obtained. There is no pass/fail and no composite
health figure, and `"score":null` is emitted explicitly so a reader cannot
mistake its absence for an omission.
WHAT IS IN STAGE 1 (four findings)
owner_runtime_divergence — the motivating case. Runtime counts vs the owner's
own GET /api/stats, the delta, and the trend against the previous audit, so
a second call answers "is the gap growing?" rather than restating it.
self_model_connectivity — the three identity pillars plus the self root:
present, content length, one-hop degree. This RETIRES the Claude-side vitals
identity block, which lived outside the system it was checking and went on
reporting green while the memory-philosophy pillar was absent from the live
graph. Asking the running soul is the designed mechanism; a shell probe was
the fourth patch on the same hole.
typed_edge_distribution — exact counts against the claim-10 vocabulary, plus
density, plus a separate count of LOWERCASE near-misses ("causes" vs
"Causes"): "the vocabulary is unused" and "the vocabulary is misspelled by
the write paths" are different defects with different fixes.
orphans_and_dangling_edges — the tool's own long-standing promise.
WHAT IS DEFERRED, AND WHY IT IS DATA RATHER THAN A COMMENT
Value/execution-record consistency and wonder-manifest authenticity ship as a
`deferred` array that MEASURES the populations they would need (Prediction and
WonderQuestion nodes) and reports those counts as the reason. Both are ~0 today
— WonderQuestion because of a known write/read node-type mismatch. Asserting
value coherence or a pull-weight correlation on an empty population would be a
fabricated result, which is worse than a stated gap.
MEASUREMENT HONESTY: EXACT WHERE CHEAP, SAMPLED WHERE NOT, ALWAYS LABELLED
Counts, edge typing and self-model connectivity are exact. Orphan and dangling
rates are sampled, because engram_find_node_index is a linear scan — an
exhaustive dangling check is O(nodes x edges), ~2.2e9 string compares at today's
scale. Samples are UNIFORM across the whole population (str_index_of_all gives
every edge offset in one pass, so any index is O(1); json_array_get would have
been O(n^2)), never head-of-list, and each figure ships with its own sampled /
population / exhaustive fields. ?edge_sample= and ?node_sample= at population
size run either check exhaustively. The real fix is an id index in the runtime.
ONE BUG THIS FOUND IN ITSELF, CAUGHT IN TEST
http_get does not return "" when the owner is unreachable — it returns a JSON
error object. Testing only for "" made a DEAD owner read as reachable with
node_count 0, so the audit reported 100% divergence and named it data loss.
Reachability is now proved by the presence of the node_count field, and the
owner's raw reply is attached. A confident wrong answer is exactly what this
route exists to stop.
Edge findings need relation labels and the runtime has no edge-enumeration
builtin, so they use the same scratch export GET /api/graph/edges already uses
(engram_save to TMPDIR, never the owner's canonical file — #117). That is a
large write on a large graph, so this is a manual route, not a timer; ?edges=0
skips it.
neuron-api.el:900-1273 handler + helpers
routes.el:567,752 GET and POST /api/neuron/audit/structural
mcp-wrapper/src/main.el:113,682 tool description + dispatch off /session/begin
dist/soul.c regenerated (1255 bodies)
Rung: E2E-VERIFIED. Soul built from this branch (gen-soul-amalgam + cc-brain,
921,192 bytes, 16 warnings, 0 errors), booted on throwaway ports 7893/7896/7897
with throwaway HOMEs against a stub owner on 7894. Three scenarios pass: owner
reachable (runtime 62 vs owner 42, delta 20 / 32.2%, trend flat on the second
call; 12/20 edges claim-10 typed, 3 lowercase near-misses; 50/62 orphans, 3/20
dangling — every figure matches the fixture by construction), owner unreachable
(reported as a finding with the raw reply, not a crash), and file mode (owner
"none", divergence undefined). Reached end-to-end through the MCP tool via a
locally built wrapper. verify-soul-contract.sh: GATE PASS, 27/27 routes +
immutability. No process left running; live :7770 and :8742 untouched (GET only).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9fd8c11670 |
chore(engine): make dist/soul.c drift a build failure instead of a silent ship (#133)
#133 regenerated the amalgam once and said so itself: 'Nothing in the tree regenerates this file. Only a human running the recipe. It lags in batches, never per-change, and it will drift again.' It drifted again. Every binary deployed on 2026-08-09 was built by build-soul.sh from a scratch amalgam that never touches dist/soul.c, so the committed build input fell 2,761 bytes behind the sources by a different route than #133 describes. Auto-regeneration is not available: the CI workflow records that elc needs 24GB+ of virtual memory and would OOM the runner. So the build cannot regenerate the file. It can refuse to compile a stale one, for free and with no compiler. tools/soulc-stamp.sh records a content fingerprint of every .el source at the moment the amalgam is generated. --check recomputes and compares; divergence exits 1 and names the changed files and the recipe. Wired into CI ahead of the compile. dist/soul.c regenerated from current sources: 1,176,361 -> 1,179,122 bytes, 1,247 inlined bodies (gate wants >=1200), and verified to compile clean at 903,552 bytes. Demonstrated to FAIL on the bad input, per postmortem 0004's rule that a gate which only passes on good input proves nothing: fresh stamp -> OK, exit 0 one .el modified -> FAIL, exit 1, names memory.el source restored -> OK, exit 0 Refs #133, #111 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dd952c0e46 |
feat(soul): write-through to the persistence owner — memories survive restart (#117)
The soul obeys half of its own ownership rule. soul.el:571-573 says "when
ENGRAM_URL is set the HTTP Engram owns persistence — the soul must NEVER write
to the local snapshot", and it doesn't. But nothing was ever built to hand the
soul's writes TO that owner: sync is pull-only (/api/sync -> engram_load_merge),
so every node created inside the soul lived in process RAM and was shed on
restart. Measured live 2026-08-07: soul node_count=102184, engram 79197.
SCOPE CORRECTION vs the earlier internal spec: engram provisional claim 17's
"pull-then-push" is a PEER-ENGRAM to PEER-ENGRAM protocol (claims 15-18 say so
explicitly). The soul is a CALLER of the database API, not a peer. Claim 17 is
NOT authority for a soul<->engram contract and is no longer cited as such. The
design here follows from the ownership rule alone.
Mechanism: a new Accessor, persist.el, is the single boundary. Writes stage a
delta to a filesystem spool and are pushed to the owner via POST /api/load-merge
— NOT POST /api/nodes, which mints a new server-side id (breaking dedup and
edges) and drops label/tier/tags/importance/confidence (verified in a sandbox:
a tier "Canonical" probe came back "Working"). load-merge preserves the id and
every field, dedups nodes by id and edges by (from,to,relation) so retries are
no-ops, and calls persist_canonical() so THE OWNER writes its own file — the
ownership rule is honoured rather than worked around.
Spool-and-drain rather than push-per-write: measured ~0.38s per load-merge at
live scale (79k nodes/176MB), and a chat turn writes 5-7 nodes. The spool is on
disk, not in process state, because the soul serves each connection on its own
pthread and a shared buffer would lose entries to a read-modify-write race. That
also buys crash recovery: writes orphaned by kill -9 are drained on next boot.
Honesty: api_persisted (the gate all 10 MCP write handlers pass through) and
mem_store now assert AT THE OWNER instead of reading back the soul's own RAM.
With the owner down a write returns {"ok":false,"error":"write_not_persisted"}
and the delta is queued — where main returns {"ok":true} for a write that dies.
Coverage: 35 node sites + 9 edge sites routed through the boundary. Deliberately
excluded, with reasons in persist.el: 4 InternalStateEvent sites (Will's own
telemetry carve-out), the boot counter and the persona (both already have
bespoke owner-side write-backs), and soul.el's 54 genesis identity edges
(file-mode only). engram_strengthen and engram_forget are NOT propagated —
load-merge cannot update or delete, and hard-deleting at the owner would fail
verify-soul-contract.sh section B.
Also fixed here:
- routes.el GET /api/graph/edges engram_save()'d straight over the owner's
canonical snapshot.json — a read route, in a non-owner process, clobbering the
canonical on every call. Same defect class Will removed from the engram in el
dc39a61. Now exports to a scratch path. With this gone the soul writes nothing
at all in HTTP mode.
- persist.el must clear the runtime's _tl_fs_read_len hint after every fs_read.
In vendored runtime v1.0.0-20260501 that hint becomes the NEXT response's
Content-Length, so reading a spool file mid-request made an 86-byte reply go
out as 497 bytes with 411 bytes of adjacent heap trailing it. Caught and fixed
at our boundary; the runtime class was fixed upstream in el 43636ae, which is
not the pinned runtime here.
Rung: E2E-VERIFIED, discriminating. Same harness, same engram binary:
write-through: LEG 1 PRESENT at owner, LEG 2 SURVIVED kill -9 + restart
main: LEG 1 ABSENT at owner, LEG 2 LOST
verify-soul-contract.sh: GATE PASS on both builds (27/27 routes, immutability).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9ea41eed78 |
fix(engine): the model was signing its own answers with our receipt — strip it
FOUND BY E2E, NOT BY REASONING. The previous commit's design note asserted the provenance
receipt "never reaches the user: it is appended to the history copy, not the reply." That
was FALSE, and only running the thing showed it. On the first live run against the built
DMG brain, two agentic turns out of two came back with
"Your favourite colour is chartreuse and your project is called Perihelion.
[[RECEIPT - recorded by the soul, not written by the model: no tools ran on this turn.]]"
— the receipt in the user-visible reply.
MECHANISM: the receipt is stored inside the assistant turn, and the agentic path replays
history VERBATIM as Anthropic message objects. So the model sees its own previous answers
ending in [[RECEIPT ...]] and does the obvious thing — it imitates the format and signs the
next answer the same way. The plain path did NOT leak, which is the tell: there, history is
rendered into the SYSTEM prompt as labelled lines rather than replayed as assistant turns,
and a model imitates its own turns far more readily than a transcript.
FIX, two layers, because one of them is not a guarantee:
- receipt_rule() names the marker in both system prompts (plain and agentic): these lines
are written by the system, read them as evidence, never write one. Reduces occurrence.
- receipt_strip() truncates any [[RECEIPT ...]] out of model output before it becomes the
reply — plain path in layered_generate, agentic path on final_text in agentic_loop.
Deterministic. A guard that depends on the model choosing to obey is exactly the class of
thing round 8 exists to stop shipping, so the instruction is the optimisation and the
strip is the guarantee.
Placed ABOVE agentic_loop's empty-check on purpose: a turn whose entire output was an
imitated receipt has produced no answer, and must be reported as no answer.
The receipt stays in HISTORY, which is the whole point and is proven to work: asked "What
source did you use for that?" one turn after a live web_search, this brain answered
"I used Weather Underground (https://www.wunderground.com/weather/is/reykjav%C3%ADk) for the
current temperature in Reykjavik" — a real source, no apology. That is the false confession
dead, and it is dead BECAUSE the model can read the receipt.
BUILT: 887,112 bytes, sha256 54a2eff84d4fa44f8d2db6781dcf225df4b5075a8ec5f1058018bd40cc1af10b
BUG-PLAINCHAT-1 miscompile guard: zero sites.
Refs neuron#109
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ff421d39f6 |
fix(engine): history keeps its provenance and its session — the false confession and the blank stare
DESIGN FIT: three of round 7's five defects share ONE root — the conversation-history
layer persists only {role, content}, discarding tool provenance, session scoping, and the
distinction between a real user turn and an internal utility call. Fixes A and B RESTORE
Will's design rather than extend it: his agentic path already scopes history per session,
the plain path never got it, and his own source carries the TODO admitting the resulting
race (chat.el, handle_chat: "process-global key; concurrent /api/chat requests without
session_id race on this read-append-write"). Fix C repairs one join Will wrote that was
correct for a year and one we added last week. E1/E2 are ours.
FIX A — tool provenance in history (kills the FALSE CONFESSION)
Root cause, EXECUTED-verified: handle_chat_agentic recorded turns via hist_append, which
emits {"role","content"} only. server_tool_use blocks, web_search_tool_result blocks and
every citation were discarded, then replayed as text. On the next turn the model saw a
data-rich answer with zero evidence a search had happened, and its own permanent rule
("never describe a search you did not perform") left one conclusion available: that it
had fabricated the data. It apologised for a search it HAD run — four independent lines
of evidence confirm the search was real. The defect is not the model's honesty. It is
that we deleted the evidence and then asked it to account for itself.
Change: agentic_loop accumulates the source URLs it already walks past (citations and
web_search_tool_result content) and returns them as "sources"; handle_chat_agentic folds
tools_used + sources into a receipt line stored WITH the assistant turn. Receipts are
unconditional — a negative receipt ("no tools ran") is the other half of the guarantee,
because "no evidence of a tool" and "evidence of no tool" were previously identical in
the transcript. conv_history_block splits the receipt off before snipping so a long
answer cannot truncate away the evidence. The user never sees it: it is appended to the
history copy, not the reply.
FIX B — one history key for both paths (kills the BLANK STARE)
Root cause, EXECUTED-verified: the agentic path keyed history on session_hist_<id>; the
plain path was hard-wired to the process-global conv_history and never read session_id.
One conversation, two buckets. Proven in the guest engram: the scoped node held exactly
two turns starting at "Try again" while the earlier exchanges sat unscoped.
Change: conv_hist_key/conv_hist_label are now the single definition, used by BOTH paths;
session_id is threaded route -> layered_cycle -> layered_generate / conv_history_record.
The 2-line fallback (plain path reads the agentic key) was REJECTED: it keeps the
process-global bucket as a live write target, which is the bleed the TODO describes.
Also found and closed while threading: layered_cycle read session_id from the state key
"current_session_id", which is read here and WRITTEN NOWHERE in the entire source. It
was unconditionally "", so TODO(reliability #4) — per-session steward continuity — was
dead code that could never fire. It fires now.
LAZY SESSION, decided explicitly: we create the session EAGERLY at the door (app half,
ui#223) rather than migrating orphaned turns. Migration would copy the CONTENTS of a
process-global bucket, possibly another conversation's, into a named session — the bleed,
performed deliberately. Eager creation makes the situation impossible instead. Migration
is deliberately not implemented and must not be added without solving provenance first.
FIX C — the two text-join seams ("to.Good", byte-verified 0x77 0x2e 0x47)
Two bare `+` joins, written a year apart, had drifted into two answers to one question:
within-response block joins (Will's, 2026-05-03, latent until server-side web_search
began interleaving non-text blocks) and across-round joins (ours,
|
||
|
|
635f6febe4 |
feat(engine): plain chat generates at L3 — inside the safety cycle, not around it
Non-agentic /api/chat (the desktop app's default "Tools: Off" mode) returned the
user's own screened text as a bare non-JSON string. Every JSON client failed to
parse it and showed "Couldn't reach Neuron - it may be offline."
Root cause:
|
||
|
|
710761e2d5 |
fix(engine): send tool_choice.disable_parallel_tool_use on agentic loop (STOPGAP)
The agentic loop keeps only the FIRST tool_use block per round (chat.el:2281,
"Capture first tool_use block only"). Anthropic lets a model emit several
tool_use blocks in one message and requires a tool_result for every one, so a
parallel-tool turn is answered once, the rest are dropped, and the next request
dies with:
tool_use ids were found without tool_result blocks immediately after
(neuron#78 quotes this as "tool_use ids found without tool_result"; the above is
the API's actual wording - recorded so the next person's grep matches.)
This constrains the wire to match what the loop can assemble:
"tool_choice":{"type":"auto","disable_parallel_tool_use":true}
STOPGAP - AND THE DURABLE FIX ALREADY EXISTS. A correct multi-tool loop is
already in Will's EL runtime, in C, and the soul does not call it. Verified on
el:origin/main lang/el-compiler/runtime/el_runtime.c: llm_register_tool:9616,
llm_build_tool_results:9743 - which walks EVERY content block, emits one
tool_result per tool_use, and sets is_error for an unregistered tool -
llm_call_agentic:9817 calling it at :9918, iteration cap 10 at :9847. Will's
commit 12d5e77 (2026-04-30). grep for llm_call_agentic/llm_register_tool across
every neuron/*.el returns nothing; dist/soul.c has zero references. chat.el
hand-rolls its own single-tool loop instead, and that is the one that breaks.
The durable fix is therefore to register the soul's tools via llm_register_tool
and call llm_call_agentic - deleting a loop, not writing one. See ADR 0005.
Our own approved spec called this seven weeks ago:
docs/research/agentic-tool-approval-design.md (2026-06-12, "Approved for build"),
line 20 on the defect, line 30 on the goal ("Execute all tool_use blocks in a
turn (one result per block)").
Two edits, because dist/soul.c cannot be regenerated here (Will's gated elc/elb
toolchain is not on this machine):
(a) chat.el:2255 - source of truth, so a later regen carries the fix.
One edit covers all three routes: agentic_loop is called from chat.el:2152
(/api/chat agentic), :2676 (dharma room) and :2496 (agentic_resume).
(b) dist/soul.c:28173 - generated form, hand-spliced. Line 27624 is the
non-agentic/OpenAI-compat req_body (no tools) and was left untouched.
Prior art reused rather than reinvented: soul-narrated-runs-20260713.patch
(27,824 bytes) line 78 spliced this same string into the same concat chain on
2026-07-13.
Deliberately NOT ported from that patch, having read it: max_tokens is not
changed by it (16384 sits on both sides of the hunk; our main's 4096 is a
separate output-truncation concern), and its pause_turn pairing fix - same
defect class - is unreachable today because no server-side web_search is wired
(agentic_tools_with_web at chat.el:1418 is never called), so it is untestable
and logged instead.
Proven E2E on a scratch profile and port 7791, never the live chain. A/B against
a pristine origin/main control built from the same vendored runtime: fixed
completed the mission (tools_used read_file x3, 4 iterations, correct answer);
control failed 3/3. Direct API probe confirmed the mechanism - without the field
the model emits 3 parallel tool_use blocks and replaying the unfixed loop's next
turn returns HTTP 400; with it, exactly 1 block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e610a412b9 |
regen soul.c from reconciled tree + harden contract gate (#199 by-id, isolation)
dist/soul.c: regenerated amalgamation (1.15MB) from the reconciled sources via the hide-.elh + elc --target=c recipe, so the shipped translation unit CI compiles now actually carries every landed fix — genesis-boot SIGSEGV (#150), safety-contact 988 truncation (#96), url-decode multi-word search, honest receipts (#100/#101), immutability arc (#83), and the bounded payloads (#103). verify-soul-contract.sh, two non-weakening fixes (both false-NEGATIVE bugs that spuriously failed a CORRECT soul; neither relaxes what fails a defective one): 1. #199 by-id gate: verify tombstone/KEPT via /api/neuron/graph?id=<id>&depth=1 (a compact neighborhood) instead of grepping engram_scan_nodes_json(9999,0) — a multi-MB, salience-ordered, 9999-capped whole-graph dump in which the salience-0.01 tombstone marker sorts past the cap and vanished. 2. Isolation: pin SOUL_ISE_URL to the dead axon port. Unsetting ENGRAM_URL was not enough — the periodic engram sync defaults its source to the LIVE engram (http://localhost:8742), so the 'isolated' gate pulled the operator's real brain (56 -> 12k nodes in seconds), which both broke Section B determinism and read live state. Now the soul stays on its own store. Verified GREEN on a throwaway port/HOME (live :7770/:8742/~/.neuron untouched): gate PASS x3 (presence 27/27, immutability all 5 KEPT); safety-contact POST 218B / GET 208B full untruncated; multi-word search (%20 and +) returns ranked hits with an all-gibberish control at 0; bounded session/begin 1370B; honest ok:false on a missing-id delete; genesis (ntn-genesis) boots clean through mem_save with no SIGSEGV. |
||
|
|
8ba35a0d56 |
reconcile: merge Will's self-review WIP (456267a) into main
Union of all ship-critical soul fixes for the Mac beta: - Keeps main's honest receipts (#100/#101), immutability arc + #199 by-id gate (#83), Track B threat routing (#76), bounded beginSession (#103), CI hardening (#85/#86), elc typo hotfix (#77), neuron-dev-setup (#84). - Brings WIP's genesis-boot SIGSEGV fix (#150/#95), safety-contact 988 truncation fix (#96), bounded-persona floor (#93), and 10 self-review commits (importance flattening, curiosity DF gating, WM/heartbeat observability, boot-counter telemetry). - Folds the multi-word ranked-search fix: api_query_param now url_decode()s the extracted value so q=foo%20bar / foo+bar tokenize as two words. Conflicts (neuron-api.el payload-bound comments, mcp-wrapper tool_forget) resolved toward the correct end state: main's verified honest-receipt read-back is kept; WIP's improved forget description is kept. Generated dist/*.c taken from main and will be regenerated from this reconciled source in the following commit. |
||
|
|
9bbb4f2af8 |
Bound beginSession/compileCtx payloads to a compact digest
Port the payload-bounding fix (PR #102, commit
|
||
|
|
456267a771 |
self-review 2026-08-01: fix importance flattening in remember/evolve/cultivate paths
Four sites passed the Float local 'sal' through el_from_float() a second time. el_val_t is the bit-pattern of the double, so re-wrapping performs an int64->double VALUE conversion of the bits before re-bitcasting — garbage that fails engram_decode_score's range check and clamps to defaults. Net effect: importance="critical" stored 0.5/0.5 — importance levels were cosmetic on the MCP memory path. Verified fixed live: critical now stores salience/importance 0.95/0.95. Same bug fixed today in engram server.el route_create_node (foundation/el 7f03876). Literal wraps (el_from_float(0.9)) are safe — elc passes numeric literals raw. |
||
|
|
edb0670670 |
self-review 2026-08-01: emit discrete wm_saturation_transition ISE
wm_saturated was a sampled boolean — the 0->1 onset and 1->0 release moments were only recoverable by hand-diffing consecutive heartbeats. Emit a low-rate transition ISE at each edge carrying the WM top-5 at that instant, so the composition that caused the regime change is captured rather than the composition up to 59s later. First beat of a boot never fires (restart is not a transition). |
||
|
|
872120c757 |
Bound beginSession/compileCtx payloads to a compact digest
The session-init endpoints concatenated unbounded engram activate/scan
results as FULL node objects (content up to ~90KB per node), producing a
~900KB response. After the MCP wrapper re-escapes that into a stringified
text block the client dropped the socket ('connection closed unexpectedly')
on every beginSession call. Cap each list (8-10 activated, 10-20 recent)
and project every node to a light identity plus a bounded, UTF-8-safe
content snippet. Response drops from ~900KB to ~12KB; full content stays
available on demand via recall/fetch/inspectGraph.
|
||
|
|
f3660e92a1 |
self-review 2026-07-31: heartbeat deltas for cumulative counters, embed_eligible; fix stale semantic-seeding comment
Runtime activation counters are now cumulative, so the heartbeat emits wm_evicted/breakthroughs as totals plus wm_evicted_delta/ breakthroughs_delta (state-tracked change since the previous beat) — events between beats are no longer lost. Adds embed_eligible from /api/stats so coverage reads as embed_count/embed_eligible instead of the misleading absolute count, and surfaces auto_term_streak in the heartbeat stream. Replaces the false 'semantic seeding NOT implemented' comment: engram_activate embeds the query, seeds semantic top-K, gates propagation on cosine, and scores WM promotion semantically. |
||
|
|
b0f4d6c493 |
self-review 2026-07-30: auto_term stopword filter, real idle_ms, bounded beginSession
- awareness.el: curiosity auto_term was the raw first word of a WM label with no term-quality scoring — observed seeds included What, Colon, Prose, Context. Replaced the 7-word genre blocklist whack-a- mole with a delimited stopword membership test (function words + document-structure words); topical terms pass untouched. Verified live: seeds now ReasonEdit, Reasoning-model, Self-review. - routes.el + awareness.el: idle counter only reset on rare inbox synthesis-requests, so idle==pulse always (zero information). handle_request now stamps soul.last_activity_ts on every inbound HTTP request; heartbeat emits idle_ms = ms since last request (-1 until first request of a boot). - neuron-api.el: beginSession concatenated a depth-2 spread plus the unbounded self-hub neighbor dump — multi-MB response, doubled by wrapper re-escaping, socket died on every call. Now depth-1 and the hub dump dropped (identity loading has its own tool). Verified: beginSession returns instead of closing the socket. |
||
|
|
627eb534a2 |
self-review 2026-07-28: close ISE lifecycle observability gaps
- session_start now also posted to the HTTP Engram via ise_post: the local engram_node_full write never crossed to the observable stream (sync flows HTTP->soul only), so boots 5+ were invisible — last visible session_start was boot 4, two weeks ago - graceful shutdown emits a final ISE with boot/pulse/uptime; a boot with no shutdown event now reliably signals a crash/SIGKILL - empty /api/sync responses emit a sync_empty warn ISE instead of being silently skipped — unreachable engram no longer looks identical to quiet-but-healthy - sync backflow prune reads ENGRAM_ISE_RETENTION_MS instead of duplicating the 48h magic number server.el already honors |
||
|
|
2b612ed5d4 |
self-review 2026-07-27: heartbeat carries activation observability
Fold engram_act_stats_json() into the heartbeat ISE: wm_evicted and breakthroughs (per curiosity-scan activate call) plus embed_breaker_open — the failure mode embed_ok structurally cannot see (it pings the Ollama root, not the embed pipeline). WM-cap eviction, breakthrough-floor flooding, and silent lexical degradation are now one-glance diagnosable from telemetry. |
||
|
|
8392f44c45 |
self-review 2026-07-26: heartbeat wm_churn + wm_top0_wm; fix streak counting on empty id
- wm_churn: count of top-5 WM ids absent from previous beat — separates 'one stuck node' from 'whole WM frozen' without hand-correlating ISEs. - wm_top0_wm: leader's weight; a frozen anchor reads as a constant here. - Streak guard: before the runtime emitted id in wm_top JSON, json_get(...,"id") was always empty and the streak incremented on ""=="" every beat — wm_top0_streak measured uptime, not fixation. Empty id now resets the streak to 0. |
||
|
|
58a9eda311 |
self-review 2026-07-25: break curiosity positive-feedback loop; observability for WM regime
proactive_curiosity strengthened its top result unconditionally every
scan — a positive-feedback fixed point that pinned auto_term on the same
node's first word for hours ('Fast-slow' era). Strengthen now fires only
when the top node changed since the last scan, and a 4-deep finst-style
tabu ring (ACT-R declarative finsts) hard-excludes recently used auto
terms (~2 min at the 30s cadence). Quoted-title guard stops '"The'
leaking through the >3-char stopword check and seeding lexical floods.
Heartbeat now pumps /api/embed-backfill?n=32 on the authoritative store
(its lazy backfill had no production trigger; coverage stalled at
93/12175) and emits wm_saturated, wm_top0_streak, embed_backfilled,
embed_count. Curiosity ISE emits auto_term_streak. The stuck-WM failure
mode is now a one-glance signal instead of manual ISE cross-referencing.
|
||
|
|
fb0bb553f3 |
self-review 2026-07-24: boot counter — demote to telemetry weight, restore persistence via HTTP write-back
Three stale soul:boot_count copies (salience .9, importance .9, Canonical: +0.2 tier bias, 0.15 threshold) held the top WM slots for 23h — a boot counter outcompeting real context. Demoted to salience .55 / importance .2 / tier Working: plumbing, not memory. Persistence was also broken: in HTTP-engram mode the server owns state and nothing wrote the counter back — the log shows boot #5 on three consecutive boots. mem_boot_count_inc now mirrors the persona write-back: delete stale server copies (matched by content prefix — route_create_node sets label=content), create the replacement server-side. Working tier is in the boot seed (/api/nodes) but excluded from periodic /api/sync, so the count survives restarts without re-importing mid-session. Verified: restart incremented 1->2 with exactly one server-side counter node. |
||
|
|
9e59c51f3c |
self-review 2026-07-23: title-derived Knowledge labels + Knowledge admitted to curiosity auto-term
Sentinel labels (knowledge:captured/evolved/canonical) made every capture anonymous in WM telemetry — 35 identical wm_top entries — and starved the curiosity auto-term seeder, which derives scan seeds from WM top-10 labels and had returned empty on every scan since boot 6 because WM became Knowledge-dominated while Knowledge was excluded from seeding. - capture/evolve/promote now pass title (or empty → engram_node_full's content[:60] derivation) instead of sentinels - auto_term_try_slot admits Knowledge slots; sentinel-shaped labels (colon, no space) are skipped so legacy nodes cannot seed 'knowledge' - verified: probe capture labeled 'Label derivation probe 2026-07-23' |
||
|
|
3ae07cc7b0 |
harden(neuron-dev-setup): fix 7 fresh-Mac onboarding installer bugs (#99)
Co-authored-by: Neuron <will.anderson@neurontechnologies.ai> Co-committed-by: Neuron <will.anderson@neurontechnologies.ai> |
||
|
|
a45a3ca379 |
Fix truncated POST/GET /api/safety-contact response
Saving the 988 crisis-line contact returned truncated, unparseable JSON —
cut mid-"set_at" at the file's byte length (e.g. 178 of a 218-byte
response). The contact written to disk was complete; only the HTTP response
was clipped, so a real customer's crisis-contact save came back corrupt.
Root cause is in the el runtime's response writer, not a handler buffer:
fs_read stores the file's byte count in a thread-local (_tl_fs_read_len)
for binary-safe file serving, and the response writer uses that length when
non-zero instead of strlen(body) (el_runtime.c:1409). Both safety-contact
handlers call fs_read (the POST read-back verify; the GET file read) and
then return a LONGER wrapped JSON string, so the response is capped to the
file size.
Soul-source fix (no runtime change needed):
- POST: verify persistence via fs_write's return (1 = all bytes written)
instead of an fs_read read-back — removes the fs_read, so nothing caps the
response.
- GET: fs_read is required, so reset the thread-local after it with a no-op
fs_read("") (fs_read zeroes the length before it opens a path) so the
wrapped response is sent in full.
Verified: POST (crisis-line + custom) and GET now return complete, valid
JSON (parses cleanly, full contact incl. set_at). Regenerated dist/soul.c +
dist/safety.c (3GB RSS watchdog, release el_runtime v1.0.0-20260501).
Full suite still green: verify-soul-contract GATE PASS (PRESENCE +
IMMUTABILITY), genesis boot survives (/health 200, no segfault), bounded-
persona floor still compiled in.
NOTE: the underlying runtime leak (any handler that fs_reads then returns a
longer string) is worth a proper fix in el_runtime.c (use the max of
strlen and _tl_fs_read_len) so this class can't recur.
|
||
|
|
091cc1fc0e |
Fix issue #150: fresh-install genesis boot SIGSEGV in mem_save
A fresh-install (SOUL_CGI_ID=ntn-genesis) boot crashed with
"Segmentation fault: 11" right after the http server came up — a real
customer's very first boot. Backtrace:
strcmp(0x1) <- str_eq (el_runtime.c:219) <- mem_save <- awareness_run
Root cause: the el runtime's engram_save returns an Int (1 = ok, 0 =
failure), but mem_save did `str_eq(engram_save(path), "")`, treating the
return as a String. str_eq runs EL_CSTR on it, which is a raw cast:
EL_CSTR(1) = (char*)0x1. On a SUCCESSFUL save (return 1) strcmp then
dereferences 0x1 and segfaults. Genesis is the first path that both seeds
the brain AND saves it successfully on the very first awareness pass, so it
crashes there; non-genesis boots (contract gate, refusal test) don't hit a
successful early mem_save, which is why they passed. handle_api_consolidate
had the identical latent bug.
Fix: read engram_save's Int result and compare `== 0` instead of str_eq'ing
it — in mem_save (memory.el) and handle_api_consolidate (neuron-api.el).
Regression: pre-existing, NOT introduced by the immutability/floor rebuild.
The pre-immutability build (
|
||
|
|
6527988eb9 |
Make engram deletes/updates/forgets immutable on the launch branch
The ship-soul builds from this branch, which has the bounded-persona floor (#93) but never received the tombstone/supersede immutability fix (that went to main; hotfix diverged before it). So the launch soul failed verify-soul-contract IMMUTABILITY on the delete/update/forget routes — they hard-removed engram nodes via engram_forget/mem_forget. Apply the same fix, mirroring the knowledge routes' supersede pattern: - node/update -> create new node + "supersedes" edge to the original, KEEP the original (no engram_forget). - node/delete, memory/delete, memory/forget, cultivate forget, and the autonomous awareness forget -> TOMBSTONE via the canonical mem_tombstone (memory.el): keep the node + its edges, record a Tombstone marker, hide from default bounded list reads (?include_deleted recovers). Never engram_forget. The MCP forget tool now routes to the tombstoning delete instead of faking a delete. Internal GC that genuinely removes transient nodes (awareness inbox-trigger consume, consolidation dedup, session-summary replace, telemetry pruning) still calls engram_forget directly and is unchanged. Regenerated dist/soul.c (single-TU) + per-module dist/{memory,awareness, neuron-api}.c from THIS branch's sources under a 3GB physical-RSS watchdog (peak ~32MB), built against the release el_runtime (v1.0.0-20260501). The bounded-persona floor is preserved — verified in the emitted C and the linked binary (BOUNDED PERSONA / SOUL_PERSONA_NAME strings present). verify-soul-contract.sh: GATE PASS — PRESENCE all 27 routes, IMMUTABILITY 5/5 KEPT (memory-update, memory-delete, node-update, node-delete, memory-forget). |
||
|
|
c63e3d1a68 |
self-review 2026-07-21: break perceive→respond→store feedback loop in awareness
The soul daemon leaked ~104 orphan in-memory nodes/min (17.6GB RSS, OOM-killed) because the perceive gate substring-matched 'soul-inbox' against the loop's own verbatim-copy output, the trigger node was strengthened but never consumed, and record() persisted a Memory node per cycle. Fixes: perceive gates and activates only on the dedicated soul-inbox-pending tag; one_cycle requires the tag on the node's tags field before attending (makes consumption safe); processed triggers are consumed via engram_forget; loop outcomes route through ISE telemetry (48h prune) instead of permanent Memory nodes. Verified post-restart: node_delta 104→~0, curiosity scans resumed, WM average unfrozen (0.120833→0.0676), RSS 17.6GB→184MB. |
||
|
|
50cf67bd66 |
self-review 2026-07-19: close silent sync-starvation hole + heartbeat deltas
- engram refresh URL now resolves env -> state -> localhost:8742, same hardening ise_post got after the boot-4 blackout. Previously a corrupted/ empty soul_engram_url state key silently disabled sync forever while heartbeats kept flowing — WM starves of Knowledge nodes with no outward sign. - heartbeat ISE: node_delta, edge_delta (growth vs stall vs flood is now one field, not cross-ISE forensics), sync_age_ms from a new soul.last_sync_ok_ts stamp (-1 = never; >> SOUL_REFRESH_MS = refresh path broken). Verified live: pulse 1 sync_age_ms=-1, sync fired +1.6s, age counts up between syncs. |
||
|
|
40d800195a |
Tombstone the remaining forget paths (close the immutability arc)
Phase 1 tombstoned memory/delete + node/delete, but the generic forget
path was still destructive. This routes every forget through the same
tombstone semantics so nothing in the daemon can hard-delete an engram
node anymore.
Canonical helper: mem_tombstone (memory.el, imported first so every module
can call it) — keep the node + its edges, record a Tombstone marker, never
engram_forget. neuron-api's tombstone_node now delegates to it (single
source of truth).
Per-path before -> after:
- memory.el `mem_forget` hard delete (engram_forget) -> tombstone. This
alone fixes both callers: the /api/neuron/memory/forget route
(handle_api_forget) and the cultivate op=="forget".
- awareness.el autonomous `forget` action engram_forget -> mem_tombstone.
The soul can no longer autonomously hard-delete a memory.
- mcp-wrapper tool_forget was a FAKE no-op that returned {"ok","deleted"}
without deleting OR tombstoning -> now routes to the soul's tombstoning
/api/neuron/memory/delete; tool description fixed to say it
supersedes/tombstones (recoverable), not "Remove a node".
Left intentionally as hard deletes (internal GC / lifecycle, not user
memory, all call engram_forget directly): session-summary replace
(chat.el), mem_consolidate dedup (memory.el), session-start telemetry
pruning (soul.el), session lifecycle (sessions.el).
Regenerated both ship paths under a 3GB physical-RSS watchdog (peak ~32MB):
dist/soul.c (single-TU amalgamation, macOS/Linux) and the per-module
dist/{memory,awareness,neuron-api}.c (Windows/Linux build). Verified: no
forget handler calls engram_forget; a forget leaves the node present +
tombstone marker. Gate (neuron-ui verify-soul-contract.sh, new memory-forget
row): PRESENCE + IMMUTABILITY PASS.
|
||
|
|
f3034d23f7 |
Regenerate per-module dist/neuron-api.c for the immutability fix
soul.c (the macOS single-TU amalgamation) already carries the tombstone/supersede change, but the Windows/Linux build path compiles the per-module dist/*.c instead (build-soul-windows.sh excludes soul.c). That path was still pulling the stale, destructive dist/neuron-api.c — so without this the cross-compiled neuron.exe would keep hard-deleting engram nodes even though the source is fixed. Regenerated with `elc --emit-header neuron-api.el`: no engram_forget in memory_delete/node_delete (tombstone), supersedes edge in node_update. Portable C — identical for macOS/Windows/Linux; only the runtime it links against differs. |
||
|
|
d337fcb265 |
Make engram deletes/updates immutable (tombstone + supersede)
The soul was hard-deleting engram nodes: memory/delete and node/delete called engram_forget (frees the node and drops its incident edges), and node/update created a replacement then forgot the original with no link back. That violates the day-one rule that engram nodes are immutable — memory could be silently, irrecoverably destroyed via the API. Convert the three destructive handlers to Will's immutability semantics, mirroring the pattern memory/update and knowledge evolve/promote already use: - node/update -> create the new node, wire a "supersedes" edge new->old, KEEP the original. No engram_forget. (Was: create + forget old, no edge.) - memory/delete and node/delete -> TOMBSTONE: keep the node AND its edges, create a Tombstone marker node (content = target id, label "tombstone:<id>") wired with a "tombstones" edge. Never engram_forget. Default bounded list reads (handle_api_list_typed / the memory list) hide tombstoned nodes and the markers; ?include_deleted=1 returns them, and internal cognition + /api/graph/nodes still traverse them. memory/update was already correct and is unchanged. Full-graph hiding on /api/graph/nodes is deliberately NOT done at the el layer: json_array_get is O(index), so filtering that endpoint (called with limit up to 999999) would be O(n^2). That hide needs a runtime scan filter and is a separate follow-up; nodes there remain traversable, tagged status:deleted via the marker edge. Regenerated dist/soul.c from these sources (flat single-TU amalgamation, compiled under a 3GB physical-RSS watchdog, peak ~32MB). Verified with scripts/verify-soul-contract.sh in neuron-ui: PRESENCE passes (all 27 routes) and IMMUTABILITY passes (all four mutation routes KEPT; deletes produce a real tombstone marker and hide from the default list). |
||
|
|
1011d8e5be |
regen dist: rebuild soul.c from corrected sources (OOM gone, Track B compiled in)
Regenerates the combined dist/soul.c and per-module dist/*.c from the current El sources, on top of the elc-source-typo fixes (PR #77) and the Track B threat-to-others routing (PR #76), both already on this branch. Validated end to end under a physical-RSS watchdog (macOS silently ignores ulimit -v / RLIMIT_AS, so every elc/elb run was RSS-polled and kill -9'd at a 3GB ceiling, one module at a time): - OOM is GONE. The stale dist/soul-with-nlg.el (which still carries the malformed string literals) explodes to 3.3GB+ and is watchdog-killed at ~90%. With the typos fixed, every one of the 48 modules compiles at <=18MB peak RSS, and the full flat amalgamation compiles as a single translation unit at ~68MB. The 700GB pathology was purely the unbounded-parser-on-malformed-literal loop; no malformed construct means no loop. - The regenerated soul.c contains Track B: safety_classify_hard_bell -> threat_other -> safety_hard_directive routes credible threat-to-others to 911 and explicitly NOT to 988 / the safety contact. Verified in source, in the emitted C, and in the linked binary's strings. Track A (abuse / self_harm) is unchanged and still checked first. - The regenerated soul links to a working native arm64 binary and boots: serves on a throwaway port, /health returns 200, awareness loop runs. Also fixes one source blocker discovered during regen (unrelated to the typos or Track B): chat.el handle_chat_agentic left a void `if { println(...) }` in value position, which the current elc lowers to `_if_result = (println(...))` (assigning void) -> invalid C. Bound an explicit Bool so the branch is non-void; behavior unchanged (still only logs on persist failure). NOTE (runtime dependency, for controlled deploy): this branch's chat.el calls engram_get_node_by_label, which the canonical el-compiler/runtime does not yet declare/define (the release runtime v1.0.0-20260501 has it; the newest runtime has arena + http_serve_async but not this). Building the soul requires a runtime that has all three. Land engram_get_node_by_label into the runtime package before this soul.c can be built in CI. Do not merge — regen + Track B going live is a controlled-deploy call. |
||
|
|
c8cb425412 |
soul: per-tick arena bracketing in awareness_run + hand-patched dist/soul.c
awareness_run's while-loop ran outside any request arena, so every allocation in every 1s tick (search JSON, heartbeat payloads, curiosity activations) was treated as permanent by the runtime — 7.5GB RSS in under a minute. Bracket each iteration with el_arena_push/el_arena_pop (same pattern the compiler emits for scoped blocks; state_set/state_get persist separately via el_strdup_persist and are unaffected). dist/soul.c carries the same change hand-patched at the compiled awareness_run site — elc is currently unsafe to run locally (pathological memory on sessions.el), so the generated C was patched to match the source, verified line-for-line against the compiler's own conventions. MUST be paired with el repo PR #64 (el_strdup_persist for stored engram fields): per-tick arena reclamation widens the write-corruption window without it. Verified together: 5h live soak on the recovered production snapshot, flat RSS, write-field-integrity clean. Note: dist/soul.c still needs a full elc regen to pick up PR #73's source changes (consent tiers) — tracked separately; this patch does not regress that (those changes were never in dist). |
||
|
|
2688cb722a |
chore(dist): update soul.c with PR #63/#65/#66 + Task 1 chat.el changes
Manually adds compiled C equivalents for: - distill_transcript() — last-3-messages extractor; wires into handle_dharma_room_turn and handle_dharma_room_turn_agentic - current_engine_note() — appended to system prompt in handle_chat so Neuron can answer 'what model am I running on?' truthfully (PR #66) - llm_base_url / llm_wire_format / json_escape / openai_chat_complete — OpenAI-compatible provider path in handle_chat_agentic (PR #65) - flag_true() — tolerant agentic flag check (PR #63) Compile verified: 6 pre-existing warnings, 0 errors. |
||
|
|
c586ea5ef1 |
chore(dist): recompile neuron.c and elp-c-decls.h
Reflects session-start event pruning in emit_session_start_event (keep_n=10, prunes oldest beyond that) and updated forward declarations for connector routing (connectd_get, connectd_post, handle_connectors, rate_limit_check, handle_chat_plan) replacing the removed route_sessions helpers and flag_true. |
||
|
|
6819729429 |
fix(awareness): correct stale comment; add wm_top to curiosity_scan ISE
The hops=1 comment incorrectly claimed a semantic seed supplement (cosine-sim scan) was active — it was planned but never implemented. Corrected to accurately describe what the runtime does (istr_contains only). Also adds wm_top (top-3 WM nodes by weight) to the curiosity_scan ISE payload so activation patterns are visible without relying solely on the heartbeat's wm_active count. |
||
|
|
31dd93d5f4 |
fix(chat): add distill_transcript (was called but never defined)
handle_dharma_room_turn and handle_dharma_chat both called distill_transcript since June 30 but the function was never declared, causing a build failure. Implements last-3-messages extraction for JSON array transcripts and last-500-char truncation for plain text. |
||
|
|
9d266aac4c |
fix(sessions): extract session_search_entry to fix ELC OOM in session_search
The while loop in session_search had too many let bindings in scope; the ELC compiler's exponential rebinding accumulation caused OOM and truncation of dist/sessions.c since June 30. Moving the per-node logic into session_search_entry gives the compiler a clean scope boundary per call, restoring O(N) compile behaviour. |
||
|
|
76bd3afdf8 | feat(dist): Win32 POSIX shim for el_runtime.c cross-compilation | ||
|
|
51bea5507b |
prevent engram corruption: idempotent boot seeding, session-start event cap
Fix 1: mem_boot_count_inc prunes all existing soul:boot_count nodes before
inserting the new one — keeps exactly one boot counter node instead
of accumulating a new node per boot. Also fixes a latent ordering
bug where engram_search_json oldest-first results caused the counter
to read stale (low) values once >3 copies accumulated.
Fix 3: handle_api_node_delete comment clarified — the no-verify exception
is correct for deletes (not a write path); read-back-verify is for
writes only.
Fix 4: emit_session_start_event prunes old session-start InternalStateEvent
nodes after each boot, keeping the 10 most recent and forgetting
older ones. Prevents unbounded accumulation of ~120+ copies.
|
||
|
|
933547265e |
chore(dist): compile PRs #60/#61 into soul.c
- PR #60: inject operator home dir into system prompt (#30) Adds OPERATOR IDENTITY section so the LLM correctly resolves 'my files/notes/desktop' to the actual running user's $HOME. Prevents identity confusion between imprint author and operator. - PR #61: plan-mode endpoint POST /api/chat {mode:'plan'} (#27) Adds handle_chat_plan — returns {steps:[{id,title,detail}]} JSON. Wired into all three /api/chat route handlers. Grounds the plan via engram_compile (same as agentic path) for context awareness. dist changes: - soul.c: both PRs compiled in; build_system_prompt updated to 2-param signature (ctx, chat_mode); handle_chat_plan added - chat.c/routes.c/chat.elh: individual module outputs updated - elp-c-decls.h: remove stale 1-param build_system_prompt decl, add handle_chat_plan declaration - soul.elh.c: new soul header declarations file (from PR #60) Compile verified: cc -O2 -DHAVE_CURL soul.c el_runtime.c -lcurl Binary: 805K arm64, smoke test passes (port in use = expected). |
||
|
|
a77578e243 |
chore(dist): compile PRs #56/#57/#58 into soul.c
- PR #56: vision in agentic chat path (image content block) - PR #57: /api/connectors/call route — proxy connector tool calls - PR #58: /api/neuron/list/<type> off-by-one fix (str_slice 16->17) Live-verified: list/BacklogItem returns 50 nodes (was 0 before #58 fix). Binary size: 3.8MB. |
||
|
|
af594a9162 | Add .gitignore, untrack compiled binary from dist/ | ||
|
|
2589183775 | Expose node/create endpoint and respect label field in memory writes | ||
|
|
dcc0bf550a |
Add Ollama provider, portable memory, cultivation digest, refugee importer, GLM-OCR spike
- P0: unified soul binary with engram_node_full fix, read-back-verify, search fix - P0: move API keys from plaintext plists to macOS Keychain - P0: fix MCP backend URL (port 8742 → 7770) - P1.6: memory-export/import scripts (AES-256-CBC, versioned .neuronmem format) - P1.7: nightly cultivation digest with sharpness metric (launchd at 23:55) - P2.10: Ollama provider in agentic loop (SOUL_LLM_PROVIDER=ollama) - P3.12: refugee importer for ChatGPT/Screenpipe/generic formats - P3.13: GLM-OCR spike — SHIP IT (mlx-vlm, 1.59GB, photo-to-memory.sh) |
||
|
|
d4609c7baa |
chore(dist): update neuron.c and routes.c to 2-arg build_system_prompt
neuron.c and routes.c were compiled against the old 1-arg soul interface. chat.c already uses the 2-arg signature. The Windows cross-compile build generates elp-c-decls.h from all dist/*.c files, causing a conflicting-types error when both signatures appear. Recompile these modules against the current soul API to eliminate the conflict. |
||
|
|
98603f5ae8 |
self-review 2026-06-24: rebuild with goal_bias fix (Knowledge type boost)
Linked against dev runtime with is_knowledge fix that adds Knowledge node type. Engram goal_bias now gives Knowledge nodes +0.3 boost on technical queries, consistent with how Belief/DharmaSelf/Safety nodes are already treated. Same el_runtime source as concurrent foundation/el commit 16d62bd. |
||
|
|
bdc07be344 |
chore(dist): compile EL recall/dedup/session-continuity fixes to C
Updates soul.c and all per-module .c files with: - parse_float_x100() engram score fix - id_in_seen dedup wiring across session_preload - session-end summary hook + session-start recall - Emergency structural repair (no duplicate fns, all callsites wired) |