Compare commits

...

28 Commits

Author SHA1 Message Date
will.anderson 69ae3d2cef Merge PR #5: feat(soul): MCP tool-bridge — suspend agentic loop for client-executed tools
Deploy Soul to GKE / deploy (push) Failing after 11m1s
Neuron Soul CI / build (push) Failing after 11m13s
2026-06-15 11:30:47 -05:00
will.anderson 621a4b7bef Merge PR #3: feat(cli): Claude-as-Neuron CLI tooling + soul-side handoff
Deploy Soul to GKE / deploy (push) Failing after 11m46s
Neuron Soul CI / build (push) Failing after 12m4s
2026-06-15 11:30:02 -05:00
will.anderson 09350c68f4 Merge PR #1: Engram write-corruption: chat.el caller fix + full handoff
Deploy Soul to GKE / deploy (push) Failing after 12m33s
Neuron Soul CI / build (push) Failing after 12m43s
2026-06-15 11:29:18 -05:00
will.anderson 5d5aaf2e23 fix(ci): use soul.c-first link with --allow-multiple-definition
Deploy Soul to GKE / deploy (push) Failing after 4m30s
Neuron Soul CI / build (push) Successful in 5m42s
Linux elb generates individual .c files; soul.c does not contain merged
imports (unlike macOS elb which produces a unified file). Re-link all
dist/*.c manually with soul.c listed first so its real main() wins, and
--allow-multiple-definition to silence GNU ld's duplicate symbol errors.
All duplicates are identical (same El source, different compile units).
2026-06-12 12:22:55 -05:00
will.anderson ef12c8587c fix(ci): link only soul.c to avoid GNU ld duplicate symbol errors
Deploy Soul to GKE / deploy (push) Failing after 5m12s
Neuron Soul CI / build (push) Failing after 5m46s
The El compiler inlines imported modules into each module's .c file.
On macOS, ld64 accepts duplicate strong symbols silently. On Linux,
GNU ld rejects them. soul.c is a fully merged file — every function
from every imported module is present in it — so linking only soul.c
against el_runtime.c produces a correct binary with no duplicates.
2026-06-12 12:15:42 -05:00
will.anderson 7117e3d9ea Merge branch 'main' of git.neuralplatform.ai:neuron-technologies/neuron 2026-06-12 12:04:21 -05:00
will.anderson 3b2bb5276d fix(ci): use foundation-prod, HTTPS el clone, main branch, fix runtime path
Deploy Soul to GKE / deploy (push) Failing after 5m3s
Neuron Soul CI / build (push) Failing after 5m30s
2026-06-11 13:26:24 -05:00
will.anderson 555fa27878 Merge remote-tracking branch 'origin/main' 2026-06-11 13:10:30 -05:00
will.anderson 764250c4f6 fix(soul): repair CI — drop gpg/TTY and import safety/stewardship/imprint layers
Deploy Soul to GKE / deploy (push) Failing after 5m15s
Neuron Soul CI / build (push) Failing after 5m42s
2026-06-11 12:33:22 -05:00
will.anderson 33c377410d Merge pull request 'feat(soul): Layer 1 — safety.el' (#8) from feat/layer-safety into main
Deploy Soul to GKE / deploy (push) Failing after 35s
Neuron Soul CI / build (push) Failing after 6m20s
2026-06-11 17:14:40 +00:00
will.anderson af933494a9 Merge pull request 'feat(soul): Layer 2 — stewardship.el' (#7) from feat/layer-stewardship into main
Deploy Soul to GKE / deploy (push) Failing after 36s
Neuron Soul CI / build (push) Failing after 7m16s
2026-06-11 17:14:32 +00:00
will.anderson 72751c3833 Merge pull request 'feat(soul): Layer 3 — imprint.el' (#6) from feat/layer-imprint into main
Deploy Soul to GKE / deploy (push) Failing after 38s
Neuron Soul CI / build (push) Failing after 7m32s
2026-06-11 17:14:16 +00:00
will.anderson 195cc9dc66 Merge pull request 'test(soul): Layer 1 safety.el test suite' (#10) from test/layer-safety into feat/layer-safety
Neuron Soul CI / build (pull_request) Failing after 5m53s
2026-06-11 17:13:50 +00:00
will.anderson 4b648f3291 Merge pull request 'test(imprint): add 14-case test suite for Layer 3 imprint boundary' (#11) from test/layer-imprint into feat/layer-imprint
Neuron Soul CI / build (pull_request) Failing after 7m54s
2026-06-11 17:13:49 +00:00
will.anderson db2ee387a4 fix(soul): address review issues in feat/layer-safety
Neuron Soul CI / build (pull_request) Failing after 6m47s
2026-06-11 11:46:43 -05:00
will.anderson 749b60c6e8 fix(soul): address review issues in feat/layer-imprint
Neuron Soul CI / build (pull_request) Failing after 5m44s
2026-06-11 11:46:31 -05:00
will.anderson ba8491926c test(soul): comprehensive tests for Layer 1 safety.el 2026-06-11 11:40:59 -05:00
will.anderson fbbc6d4347 Add imprint.el test suite (14 cases)
Covers: imprint_current base fallback, unload idempotency, load miss →
ok=false, ok field presence, respond passthrough for base/empty/unknown
IDs, graceful fallback after unload, surface_knowledge and
surface_memory_read return-type guarantees, base-scoped knowledge
equality, no-annotation invariant for base, empty-ID load rejection, and
failed-load state immutability.

Syntax follows El constraints: no Bool annotations, no &&/||, no unary !.
2026-06-11 11:40:37 -05:00
will.anderson 5597bf78cb feat(soul): Layer 1 — safety.el with screen/validate/bell interface
Neuron Soul CI / build (pull_request) Failing after 7m19s
2026-06-11 11:30:57 -05:00
will.anderson 6fec93ff7f feat(soul): Layer 3 — imprint.el with bounded API surface
Neuron Soul CI / build (pull_request) Failing after 7m46s
2026-06-11 11:30:30 -05:00
will.anderson 690df89610 self-review 2026-06-11: add WM-autobiographical curiosity seed
proactive_curiosity() now uses the top working-memory node's first label
word as a 4th activation seed alongside the 4 rotating fixed sets. This
breaks deterministic exploration that was reinforcing the same subgraph
every cycle and creates a self-referencing loop: curiosity radiates from
whatever is most salient right now, mirroring the brain's default-mode-
network resting-state dynamics. str_find_chars on " :([" extracts the
first meaningful word; sp > 3 guards against bracket-prefixed labels.
auto_term field added to curiosity_scan ISE for observability.
2026-06-11 08:45:55 -05:00
Tim Lingo c3f39a949d feat(soul): MCP tool-bridge — suspend agentic loop for client-executed tools
Neuron Soul CI / build (pull_request) Failing after 4m8s
When handle_chat_agentic hits a tool the soul cannot run in-process (an MCP
connector/plugin surfaced by the Kotlin desktop app), instead of returning
"unknown tool" it now suspends the agentic loop and returns a tool_pending
envelope so the CLIENT executes the tool and posts the result back. Built-in
tools (read_file/write_file/web_get/search_memory/run_command) and Anthropic's
native web_search are unchanged.

Client contract:
- Soul returns (HTTP 200) on an unknown tool:
    { "tool_pending": true, "session_id": "br-...", "call_id": "<tool_use_id>",
      "tool_name": "...", "tool_input": { ... }, "model": "...",
      "agentic": true, "tools_used": [...] }
- Client runs the MCP tool, then POSTs to
    /api/sessions/{session_id}/tool_result
  with body:
    { "call_id": "<the call_id from the envelope>",
      "content": "<MCP tool output as a string>" }
- Soul resumes the loop and returns the same envelope shape: either a final
    { "reply": ..., "tools_used": [...] }
  or another tool_pending if the continuation needs a further MCP tool
  (fully chainable). Saved continuation is one-shot (cleared on resume).

elc-verified (--target=c, exit 0, no stderr) on chat.el, routes.el, and the
full soul.el import graph. Needs Will's build to ship.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:31:18 -05:00
will.anderson 297066c2d4 self-review 2026-06-10: fix ise_post JSON escaping + rebuild soul daemon
Two fixes:

1. ise_post was only escaping " in content strings. When wm_top contained
   node labels with \n (backslash-n escape sequences from jb_emit_escaped),
   the HTTP Engram server's JSON parser decoded \n as a literal newline in
   the stored content, making heartbeat ISEs unparseable. Fix: escape
   backslashes first, then quotes, then \n and \r — matching make_action's
   existing pattern. Result: heartbeat ISEs now parse cleanly.

2. Soul daemon (dist/neuron) was missing — the build command in the prompt
   was linking all 46 dist/*.c files together, causing 1092 duplicate symbol
   errors. EL compiles transitive imports inline so neuron.c is self-contained;
   correct build links ONLY neuron.c + el_runtime.c. Daemon now starts.
2026-06-10 08:54:28 -05:00
Tim Lingo 2ea1d50fa3 feat(cli): Claude-as-Neuron CLI tooling + soul-side handoff
Neuron Soul CI / build (pull_request) Successful in 5m10s
Tooling built on Tim's machine to run Neuron from the terminal as a
Claude Code session (identity + graph memory + agency) instead of
relaying to the soul's /api/chat.

- cli/neuron_recall.py    BM25 read over the engram snapshot + CLI memories
                          (works around pinned-only soul search)
- cli/neuron_remember.py  reliable local memory writes with read-back verify
                          (works around the corrupting capture endpoint)
- cli/neuron-chat.py      standalone direct-chat REPL with per-turn memory injection
- cli/neuron_mcp.py       stdlib MCP server (chat/search) with graceful degradation
- cli/CLAUDE.md.example   the operating identity that makes Claude Code run as Neuron
- cli/HANDOFF.md          soul-side bugs to fix so this becomes unnecessary

Scaffolding/proposal - intended to be retired once the soul does native
retrieval, correct persistence, and a real CLI identity/voice surface.
Pairs with the runtime model-passthrough + UTF-8 fixes in the el repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:36:38 -05:00
will.anderson c81f49d938 self-review 2026-06-09: add periodic engram sync to soul awareness loop
Soul's in-process store had only 12 real knowledge nodes — curiosity_scan was
activating 0 nodes because all substantive Knowledge/Memory/BacklogItem content
lived in the HTTP Engram but was not being pulled into soul's local store.

Added engram_sync refresh every SOUL_REFRESH_MS (default 600s): calls
/api/sync to get all non-ISE nodes, writes to /tmp, merges via engram_load_merge.
After fix: engram_sync ISE shows added:3128; curiosity_scan activated 0-2 →
1889-3843; wm_active 0 → 557-796.
2026-06-09 08:55:59 -05:00
Tim Lingo 2112d2ffb3 Add Phase 0 live-runtime findings to engram write-corruption handoff
Neuron Soul CI / build (pull_request) Successful in 3m17s
Confirms two distinct write failures (capture=wrapper bug; backlog=axon :7771 unbuilt Rust),
soul runs in file-snapshot mode (not engram :8742 live), engram :8742 CRUD works but minimal,
+ a verification plan to run after the soul rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:25:12 -05:00
Tim Lingo 799ca3758b Fix chat.el node_type-slot bug + add engram write-corruption handoff
Neuron Soul CI / build (pull_request) Successful in 3m15s
chat.el recorded the soul's utterance via engram_node(content, "episodic", ...),
putting a TIER into the node_type slot (nodes showed node_type="episodic"). Now uses
engram_node_full(..., "Conversation", "soul:utterance", ..., "Episodic", tags).

The core wrapper fix is in the el repo (PR #52). HANDOFF-engram-write-corruption.md
has the full root-cause analysis, coercion mechanism, caller audit, validation,
deploy runbook (elc build + restart), and the data-prune proposal (~107 corrupt
nodes, all unrecoverable genesis/binary detritus → prune; backup taken).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:14:20 -05:00
will.anderson df648a8f0b self-review 2026-06-07: fix uptime display in awareness loop
elapsed_human() used % and * operators which are broken in this EL
compiler version. Replace with repeated-doubling arithmetic:
60 = 64 - 4 = 2^6 - 2^2, computed via three doubling steps.
Fixes uptime displaying "44h 2694m" instead of "44h 14m".
2026-06-07 08:47:29 -05:00
24 changed files with 2693 additions and 407 deletions
+30 -9
View File
@@ -19,8 +19,8 @@ jobs:
- name: Checkout foundation/el (ELP source for soul.el imports)
run: |
git clone http://34.31.145.131/neuron-technologies/el.git \
--depth=1 --branch=dev \
git clone https://git.neuralplatform.ai/neuron-technologies/el.git \
--depth=1 --branch=main \
../foundation/el
- name: Install build dependencies
@@ -45,7 +45,7 @@ jobs:
# Get latest version of each package
get_latest() {
gcloud artifacts versions list \
--repository=foundation-dev \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package="$1" \
@@ -62,22 +62,22 @@ jobs:
echo "Downloading elc@${ELC_VER} elb@${ELB_VER} runtime@${RC_VER}"
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--repository=foundation-prod --location=us-central1 --project=neuron-785695 \
--package=el-elc --version="${ELC_VER}" \
--destination=/opt/el/dist/platform/
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--repository=foundation-prod --location=us-central1 --project=neuron-785695 \
--package=el-elb --version="${ELB_VER}" \
--destination=/opt/el/dist/bin/
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--repository=foundation-prod --location=us-central1 --project=neuron-785695 \
--package=el-runtime-c --version="${RC_VER}" \
--destination=/opt/el/runtime/
gcloud artifacts generic download \
--repository=foundation-dev --location=us-central1 --project=neuron-785695 \
--repository=foundation-prod --location=us-central1 --project=neuron-785695 \
--package=el-runtime-h --version="${RH_VER}" \
--destination=/opt/el/runtime/
@@ -109,7 +109,28 @@ jobs:
ELC=/opt/el/dist/platform/elc
RUNTIME=/opt/el/runtime
$ELB --elc=$ELC --runtime=$RUNTIME
# Compile all El modules to C.
# This step will fail at link on Linux: the El compiler inlines imported
# modules into each module's .c file, producing duplicate strong symbol
# definitions. GNU ld rejects these; macOS ld accepts them silently.
# We capture the link failure and re-link manually below.
$ELB --elc=$ELC --runtime=$RUNTIME/el_runtime.c || true
# Re-link with soul.c listed first so its real main() (from the cgi block)
# wins over the stub main()s generated in every other module.
# --allow-multiple-definition tells GNU ld to pick the first definition
# for each duplicate symbol — safe here because all duplicates are identical
# (same El source compiled independently into multiple .c files).
mkdir -p dist
OTHER_C=$(ls dist/*.c | grep -v '/soul\.c$' | sort | tr '\n' ' ')
cc -O2 -DHAVE_CURL \
-I$RUNTIME \
dist/soul.c $OTHER_C \
$RUNTIME/el_runtime.c \
-lssl -lcrypto -lcurl -lpthread -lm \
-Wl,--allow-multiple-definition \
-o dist/neuron
ls -lh dist/neuron
- name: Smoke test
@@ -126,7 +147,7 @@ jobs:
VERSION="${GITHUB_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-dev \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=neuron-soul \
+2 -4
View File
@@ -30,11 +30,9 @@ jobs:
run: |
apt-get update -qq
apt-get install -y --no-install-recommends \
ca-certificates curl gnupg apt-transport-https kubectl
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \
ca-certificates curl apt-transport-https kubectl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
> /etc/apt/sources.list.d/google-cloud-sdk.list
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg \
| gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
apt-get update -qq && apt-get install -y google-cloud-cli google-cloud-cli-gke-gcloud-auth-plugin
- name: Authenticate to GCP
+126
View File
@@ -0,0 +1,126 @@
# Handoff: Engram EL write-path field corruption + silent writes
**For:** Will (backend / EL soul)
**From:** Tim (via Claude Code)
**Date:** 2026-06-08
**Status:** Root cause confirmed; source fixes applied locally (NOT built/deployed); data analyzed; prune proposed (NOT applied).
---
## TL;DR
The EL wrapper `engram_node_full` had a **stale signature** that didn't match the C primitive. Because `el_val_t` is an untyped machine word, the compiler coerced caller args to the wrong declared types and forwarded them **by position** into a C function whose positions mean different things → `tier` got ints, `importance/confidence` got strings, `label` got a float, etc. One caller (`chat.el`) also put a *tier* into the `node_type` slot.
Source fixes are done. **You need to:** review, build with `elc`, restart the soul, verify, and apply the prune (daemon stopped). Details below.
---
## 1. Root cause (confirmed)
**C contract** (`el/lang/el-compiler/runtime/el_seed.h:204`):
```
__engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags)
```
**Old wrapper** (`el/lang/runtime/engram.el:15-17`) — stale schema, wrong names AND types:
```
fn engram_node_full(content: String, nt: String, sal: Float, imp: Float,
source: String, lang: String, ts: Int, tags: String)
```
**Coercion mechanism:** `el_val_t` is `uintptr_t` (`#define EL_STR(s) ((el_val_t)(uintptr_t)(s))`, `EL_INT(v) (v)`). The EL compiler binds each caller arg to the wrapper's *declared* param type (String→Float / String→Int coercion at the boundary), then the wrapper forwards **positionally**. Result for a correct-order caller `(content,"Memory","memory:remembered",sal,imp,conf,tier,tags)`:
- `label``sal` (a float)
- `importance` ← a String
- `confidence` ← a String
- `tier``ts` (the tier String coerced to Int) → **tier becomes an integer**
This matches the data exactly (see §6).
---
## 2. Fix applied — wrapper (`el/lang/runtime/engram.el`)
Corrected to match the C contract 1:1 (no coercion, no reorder):
```
fn engram_node_full(content: String, node_type: String, label: String,
salience: Float, importance: Float, confidence: Float,
tier: String, tags: String) -> String {
// validation (see §4), then:
return __engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags)
}
```
## 3. Fix applied — caller audit
Audited every caller (`chat.el`, `awareness.el`, `soul.el`, `memory.el`, `routes.el`, `neuron-api.el`).
**All `engram_node_full` callers already use the correct order** — so the wrapper fix repairs them automatically. **One real caller bug** fixed:
`neuron/chat.el:512` was:
```
engram_node(clean_response, "episodic", el_from_float(0.6)) // "episodic" = a TIER in the node_type slot
```
Now:
```
engram_node_full(clean_response, "Conversation", "soul:utterance",
el_from_float(0.6), el_from_float(0.6), el_from_float(0.8),
"Episodic", utterance_tags)
```
## 4. Fix applied — validation (defense in depth, `engram.el`)
Added `engram_valid_node_type` / `engram_valid_tier` allowlists. Both `engram_node` and `engram_node_full` now **reject invalid values with `__println` + return `""`** (fail loud, never silently write a malformed node).
- node_type allowlist: Memory, Knowledge, Belief, Project, Tag, BacklogItem, Artifact, Conversation, ExecutionContext, InternalStateEvent, Self, Entity, Process, ConfigEntry, Concept, Imprint *(union of the spec list + types actually present in the store — trim if some are illegitimate).*
- tier allowlist: Semantic, Episodic, Working, Procedural, Canonical, Note, Lesson
- **Note:** `el_val_t` is untyped, so this catches wrong VALUES, not wrong TYPES. Type safety comes from the corrected signatures.
> All edits above are in the working tree on Tim's machine but **NOT compiled/deployed** and **NOT compile-verified** (no `elc` on that box).
---
## 5. DEPLOY RUNBOOK (your build env)
1. Pull the edited files: `el/lang/runtime/engram.el`, `neuron/chat.el`.
2. Build: `elc` (entry `neuron/soul.el`, import chain) → `neuron/dist/*.c`, then link as in `el/lang/install.sh` (`$(CC) $(CFLAGS) -o dist/neuron-fresh dist/*.c .../el_runtime.c -lcurl -lpthread`). Confirm `engram.el` recompiles into the import chain.
3. Restart the soul. **Note:** on Tim's box it's run by `/tmp/soul-keepalive.sh` (an auto-restart loop) → stop that loop before killing `neuron-fresh`, or it'll respawn the old binary.
4. **Verify (prove end-to-end):** write a node via the live API (POST `/api/memories` or the remember path) with an obvious throwaway label, then read it back and confirm `node_type` + `tier` are correct AND that it persisted (node_count increments; survives a snapshot save). There is **no delete endpoint** — clean up via the snapshot.
---
## 6. Data analysis + prune proposal (NOT applied)
- Snapshot: `~/.neuron/engram/snapshot.json`. **Backup made:** `~/.neuron/engram/snapshot.backup-20260608.json`.
- **~107 corrupt nodes** (node_type/tier not in the valid sets). node_type junk values: `''`, `'1'`, `'2'`, `'ntn-genesis'`, `'claude-opus-4-8'`, binary. tier junk: same + `'/Users/timlingo'`.
- **0 are field-repairable.** They're all genesis-bootstrap / binary detritus where *every* field (id/label/tier/tags) is corrupted together — 69× "You are ntn-genesis, a CGI.", 62× "ntn-genesis", ~70 binary garbage, plus a proxy URL + an API path that leaked into labels. No signal to reconstruct → **prune, don't fabricate.**
- **Proposal:** `~/.neuron/engram/snapshot.pruned.json` — 3,631 clean nodes (107 junk removed), edges intact (no dangling). Byte-verified: no *clean* node contains binary content, so re-encoding is lossless.
- **NOT applied** because the live daemon is **actively rewriting `snapshot.json`** (two reads returned different counts). Applying requires stopping the soul + keepalive, swapping in the pruned snapshot, then restarting. Do this in your controlled env with the backup retained.
---
## 7. Security heads-up (please action)
- `ANTHROPIC_API_KEY` is stored **in plaintext** in `/tmp/soul-keepalive.sh` — rotate it and move to a secret store.
- Internal infra leaked into node fields (`http://localhost:7771`, `/api/graph/edges?limit=5000`) — symptom of the same write bug; the prune removes those nodes.
## 8. Backlog of related gaps (separate from this fix)
- Soul chat loop reports **no tools** (`NONE`) / `NO_SHELL` — it narrates `curl`/`sqlite3` without executing. The capture REST path works, but the chat agent can't call it.
- **No `PUT`/`DELETE`** on knowledge nodes (`method not allowed`) — needed for UI edit/delete.
- No **source-conversation** edge on captured nodes — blocks "see source chat" in the UI.
- Writes have been **frozen since ~2026-04-29** (newest knowledge node) — nothing is being added in the current running state.
---
## ADDENDUM — Phase 0 live runtime findings (2026-06-08, verified against the running system)
Validated the write path end-to-end against `neuron-fresh :7770` + `engram :8742`. Confirms the diagnosis and corrects two common assumptions.
**Ports:** `engram :8742` ✓ listening (healthy: `{"status":"ok","engine":"engram-runtime-native"}`), `neuron-fresh :7770` ✓, **`:7771` NOT listening.**
**Two distinct write failures (not one):**
1. **`/api/neuron/knowledge/capture` + memory remember** — handled **in-process by the soul** (`neuron-api.el` `handle_api_capture_knowledge` / remember → `engram_node_full(...)`). Live test: `POST …/knowledge/capture` returned `{"id":"2ccfc147…","ok":true}` but that id is **absent from `/api/graph/nodes` and `snapshot.json`** → the node corrupted/vanished. **This is exactly the `engram_node_full` wrapper bug this PR fixes.** It is NOT a `:7771` issue. → fixed by el PR #52 + soul rebuild.
2. **`/api/backlog`, `/api/memories`, `/api/knowledge`, `/api/artifacts`, `/api/projects`, `/api/imprints`** — `routes.el` proxies these to **`axon`** via `axon_get`/`axon_post` (base `SOUL_AXON` or default **`http://localhost:7771`**). `axon` = **`protocols/axon`, an unbuilt Rust crate**, not running → "Failed to connect to localhost port 7771." → needs axon stood up (separate Rust workstream) OR routes repointed.
**Architecture clarifications (so nobody chases the wrong port again):**
- The soul runs in **file-snapshot mode** (no `ENGRAM_URL` in `/tmp/soul-keepalive.sh`) → it uses `~/.neuron/engram/snapshot.json`, **not `engram :8742` live**. So writing to `:8742` does NOT make data visible to the soul the app talks to.
- `engram :8742` is its own EL service (`engram/src/server.el`) with a **working CRUD API**: `POST/GET/DELETE /api/nodes`, `/api/edges`, `/api/save`, `/api/load`, `/api/activate`, `/api/search`. Verified create+delete (`{"ok":true}`). **But** its `route_create_node` only reads `content/node_type/salience`**no label/tier/tags/metadata** — so it can't set `metadata.tier_source: canonical`.
- Minor EL bug in `engram/src/server.el route_create_node`: `if str_eq(node_type,""){ let node_type = "Memory" }` **shadows** (new local) instead of reassigning → the default never applies; same for `salience`. Worth fixing while in there.
**Verification plan (run after the soul rebuild lands):**
1. `POST /api/neuron/knowledge/capture {content,title,tier:canonical}` → capture the returned id.
2. `GET /api/neuron/knowledge/search?q=<term>` → confirm the node comes back with correct `node_type`/`metadata.tier_source`.
3. Confirm it survives a snapshot save (present in `snapshot.json`). Only then is the write "real."
4. Backlog: once `axon :7771` is up, repeat for `POST /api/backlog`.
**Net:** "make writes persist" needs (a) **this wrapper fix built into the soul** (capture) and (b) **`axon :7771` running** (backlog/artifacts/etc.). Neither was doable on Tim's box (no `elc`; `axon` is unbuilt Rust — out of scope per the no-Rust guardrail). No live writes/restarts were performed; engram probe node was created and deleted to verify the API.
+88 -12
View File
@@ -30,8 +30,16 @@ fn ise_post(content: String) -> Void {
)
return ""
}
let safe: String = str_replace(content, "\"", "\\\"")
let body: String = "{\"content\":\"" + safe + "\"}"
// Proper JSON string escaping: backslashes first, then quotes, then control chars.
// Previously only escaped " — this caused ise_post to produce malformed JSON when
// content contained \n (backslash-n) from wm_top label escaping: the HTTP Engram
// server would decode \n as a literal newline in the stored content field, making
// the heartbeat ISE unparseable as JSON. (2026-06-10 self-review)
let safe1: String = str_replace(content, "\\", "\\\\")
let safe2: String = str_replace(safe1, "\"", "\\\"")
let safe3: String = str_replace(safe2, "\n", "\\n")
let safe4: String = str_replace(safe3, "\r", "\\r")
let body: String = "{\"content\":\"" + safe4 + "\"}"
let discard: String = http_post_json(engram_url + "/api/neuron/state-events", body)
return ""
}
@@ -44,21 +52,36 @@ fn elapsed_ms() -> Int {
return time_now() - boot
}
// elapsed_human uptime as a human-readable string: "2h 14m", "45m 3s", "12s".
// elapsed_human — uptime as a human-readable string: "2h 14m", "45m", "12s".
//
// CODEGEN NOTE: EL's % and * operators are both broken in this compiler version
// (% drops the modulo, * is similarly unreliable). We avoid them entirely:
// - For h*60: use repeated doubling. 60 = 64 - 4 = 2^6 - 2^2.
// Build h*64 via three doublings of h*4, then subtract h*4.
// - For m-within-hour: total_minutes - h*60 (subtraction only).
// - For s-within-minute not shown when m > 0: avoids the s%60 problem entirely.
// (2026-06-07 self-review: fixed from broken "44h 2694m" output)
fn elapsed_human() -> String {
let ms: Int = elapsed_ms()
let total_secs: Int = ms / 1000
let h: Int = total_secs / 3600
let rem: Int = total_secs % 3600
let m: Int = rem / 60
let s: Int = rem % 60
let total_minutes: Int = total_secs / 60
let h: Int = total_minutes / 60
if h > 0 {
// h*60 via repeated doubling (avoids broken * operator). 60 = 64-4.
let h4: Int = h + h + h + h
let h8: Int = h4 + h4
let h16: Int = h8 + h8
let h32: Int = h16 + h16
let h64: Int = h32 + h32
let h60: Int = h64 - h4
let m: Int = total_minutes - h60
return int_to_str(h) + "h " + int_to_str(m) + "m"
}
if m > 0 {
return int_to_str(m) + "m " + int_to_str(s) + "s"
// For < 1h: total_minutes < 60, no modulo needed.
if total_minutes > 0 {
return int_to_str(total_minutes) + "m"
}
return int_to_str(s) + "s"
return int_to_str(total_secs) + "s"
}
// embed_ok — returns 1 if Ollama embedding service is reachable, 0 if not.
@@ -186,14 +209,42 @@ fn proactive_curiosity() -> Bool {
let found_b: Int = json_array_len(results_b)
let found_c: Int = json_array_len(results_c)
let found: Int = found_a + found_b + found_c
// WM-autobiographical 4th seed: extract the first word from the top working-memory
// node's label and activate it as an additional term. This creates a self-referencing
// curiosity loop — exploration radiates outward from whatever is most salient right now,
// mirroring the brain's default-mode-network resting-state dynamics. Breaks the fixed
// 4-set determinism that otherwise reinforces the same subgraph every rotation cycle.
//
// str_find_chars finds the first space/colon/bracket delimiter. sp > 3 guards against
// very short or bracket-prefixed labels like "[BacklogItem]" (sp=0, not > 3 → skipped).
// EL scoping: state_set/state_get pattern used because let inside if creates inner scope.
// (2026-06-11 self-review)
state_set("cseed_auto", "")
let wm_top_j: String = engram_wm_top_json(1)
let wm_top_n: String = json_array_get(wm_top_j, 0)
let wm_top_lbl: String = json_get(wm_top_n, "label")
if !str_eq(wm_top_lbl, "") {
let sp: Int = str_find_chars(wm_top_lbl, " :([")
if sp > 3 {
state_set("cseed_auto", str_slice(wm_top_lbl, 0, sp))
}
}
let auto_term: String = state_get("cseed_auto")
let results_auto: String = if str_eq(auto_term, "") { "[]" } else { engram_activate_json(auto_term, 1) }
let found_auto: Int = json_array_len(results_auto)
let total_found: Int = found + found_auto
let safe_auto: String = str_replace(auto_term, "\"", "'")
let wmc: Int = engram_wm_count()
let ise: String = "{\"event\":\"curiosity_scan\",\"seed\":\"" + curiosity_seed
+ "\",\"auto_term\":\"" + safe_auto
+ "\",\"minute_block\":" + int_to_str(minute_block)
+ ",\"activated\":" + int_to_str(found)
+ ",\"activated\":" + int_to_str(total_found)
+ ",\"wm_active\":" + int_to_str(wmc)
+ ",\"ts\":" + int_to_str(ts) + "}"
ise_post(ise)
return found > 0
return total_found > 0
}
fn pulse_count() -> Int {
@@ -461,6 +512,31 @@ fn awareness_run() -> Void {
state_set("soul.last_scan_ts", int_to_str(now_ts))
}
// Engram sync: periodically fetch a non-ISE snapshot from the HTTP Engram
// and merge it into the soul's in-process store so that Knowledge/Memory/
// BacklogItem nodes are always available for curiosity activation and WM.
let refresh_ms_raw: String = env("SOUL_REFRESH_MS")
let refresh_ms: Int = if str_eq(refresh_ms_raw, "") { 600000 } else { str_to_int(refresh_ms_raw) }
let last_refresh_str: String = state_get("soul.last_refresh_ts")
let last_refresh_ts: Int = if str_eq(last_refresh_str, "") { 0 } else { str_to_int(last_refresh_str) }
let refresh_elapsed: Int = now_ts - last_refresh_ts
let should_refresh: Bool = refresh_elapsed >= refresh_ms
if should_refresh {
let engram_url: String = state_get("soul_engram_url")
if !str_eq(engram_url, "") {
let sync_json: String = http_get(engram_url + "/api/sync")
if !str_eq(sync_json, "") && !str_eq(sync_json, "{}") {
let cgi_id: String = state_get("soul_cgi_id")
let tmp: String = "/tmp/soul-sync-" + cgi_id + ".json"
fs_write(tmp, sync_json)
let added: Int = engram_load_merge(tmp)
let ts2: Int = time_now()
ise_post("{\"event\":\"engram_sync\",\"added\":" + int_to_str(added) + ",\"ts\":" + int_to_str(ts2) + "}")
}
}
state_set("soul.last_refresh_ts", int_to_str(now_ts))
}
sleep_ms(tick_ms)
}
}
+181 -10
View File
@@ -300,6 +300,30 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
return "unknown tool: " + tool_name
}
// is_builtin_tool true when the soul can execute the tool itself in-process.
// Anything else (MCP connectors / plugins surfaced by the Kotlin desktop app) must
// be executed CLIENT-side via the tool-bridge: the agentic loop suspends and asks
// the client to run it. The native web_search tool is executed by Anthropic, so it
// never reaches dispatch_tool and is not listed here.
fn is_builtin_tool(tool_name: String) -> Bool {
return str_eq(tool_name, "read_file")
|| str_eq(tool_name, "write_file")
|| str_eq(tool_name, "web_get")
|| str_eq(tool_name, "search_memory")
|| str_eq(tool_name, "run_command")
}
// next_bridge_id monotonic correlation id for a suspended agentic turn.
// Combines boot-relative time with a per-process counter so two unknown-tool
// suspensions in the same second still get distinct ids.
fn next_bridge_id() -> String {
let prev: String = state_get("mcp_bridge_seq")
let n: Int = if str_eq(prev, "") { 0 } else { str_to_int(prev) }
let next: Int = n + 1
state_set("mcp_bridge_seq", int_to_str(next))
return "br-" + int_to_str(time_now()) + "-" + int_to_str(next)
}
fn handle_chat_agentic(body: String) -> String {
let message: String = json_get(body, "message")
if str_eq(message, "") {
@@ -324,11 +348,40 @@ fn handle_chat_agentic(body: String) -> String {
map_set(h, "anthropic-version", "2023-06-01")
map_set(h, "content-type", "application/json")
let session_id: String = next_bridge_id()
return agentic_loop(session_id, model, safe_sys, tools_json, messages, h, "")
}
// agentic_loop the resumable agentic turn. Runs the Anthropic tool-use loop and
// returns one of two JSON envelopes:
// - done: {"reply":...,"model":...,"agentic":true,"tools_used":[...]}
// - pending: {"tool_pending":true,"session_id":...,"call_id":...,"tool_name":...,
// "tool_input":{...},"tools_used":[...]} (HTTP 200)
// The "pending" envelope is the CLIENT-BRIDGE signal: the loop has hit a tool the
// soul cannot run in-process (an MCP connector/plugin the desktop app exposes). The
// loop's full continuation (messages so far + the awaiting tool_use_id) is persisted
// under state key "mcp_bridge:<session_id>". The client executes the MCP tool and
// POSTs the result to /api/sessions/{session_id}/tool_result, which calls
// agentic_resume to continue from exactly here. This mirrors Anthropic's own
// tool_use round-trip, just with the soul as orchestrator and the client as executor.
//
// `tools_log_in` carries any tool names already used in a prior (pre-suspension) leg
// so the final tools_used list survives a resume.
fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json: String, messages_in: String, h: Map, tools_log_in: String) -> String {
let api_url: String = "https://api.anthropic.com/v1/messages"
let messages: String = messages_in
let final_text: String = ""
let tools_log: String = ""
let tools_log: String = tools_log_in
let iteration: Int = 0
let keep_going: Bool = true
// Suspension state captured at top level so it escapes the while body.
let pending: Bool = false
let pend_tool_id: String = ""
let pend_tool_name: String = ""
let pend_tool_input: String = ""
while keep_going && iteration < 8 {
let req_body: String = "{\"model\":\"" + model + "\""
+ ",\"max_tokens\":4096"
@@ -375,8 +428,13 @@ fn handle_chat_agentic(body: String) -> String {
let ci = ci + 1
}
// Dispatch tool and build result message
let tool_result_raw: String = if has_tool { dispatch_tool(tool_name, tool_input) } else { "" }
// A real tool turn that targets a tool the soul cannot run in-process is a
// CLIENT bridge: suspend the loop and hand the tool to the client.
let is_tool_turn: Bool = str_eq(stop_reason, "tool_use") && has_tool
let needs_bridge: Bool = is_tool_turn && !is_builtin_tool(tool_name)
// Built-in tools dispatch locally; bridged tools yield "" (never sent upstream).
let tool_result_raw: String = if is_tool_turn && !needs_bridge { dispatch_tool(tool_name, tool_input) } else { "" }
// Truncate large tool results (web pages etc) to avoid oversized requests
let tool_result: String = if str_len(tool_result_raw) > 6000 {
str_slice(tool_result_raw, 0, 6000) + "...[truncated]"
@@ -390,20 +448,50 @@ fn handle_chat_agentic(body: String) -> String {
if str_eq(tools_log, "") { tool_quoted } else { tools_log + "," + tool_quoted }
} else { tools_log }
// Update messages and loop state all at top level using if-expressions
let is_tool_turn: Bool = str_eq(stop_reason, "tool_use") && has_tool
// The assistant turn that requested the tool needed verbatim on resume so the
// tool_use/tool_result pairing stays valid when the client posts its result.
let inner: String = str_slice(messages, 1, str_len(messages) - 1)
let messages = if is_tool_turn {
"[" + inner
let messages_with_assistant: String = "[" + inner
+ ",{\"role\":\"assistant\",\"content\":" + eff_content + "}"
+ ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}"
+ "]"
// Local built-in tool turn: append assistant + tool_result and keep looping.
let local_continue: Bool = is_tool_turn && !needs_bridge
let messages = if local_continue {
let inner2: String = str_slice(messages_with_assistant, 1, str_len(messages_with_assistant) - 1)
"[" + inner2 + ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}]"
} else { messages }
// Bridge turn: persist the continuation and stop the loop.
let pending = if needs_bridge { true } else { pending }
let pend_tool_id = if needs_bridge { tool_id } else { pend_tool_id }
let pend_tool_name = if needs_bridge { tool_name } else { pend_tool_name }
let pend_tool_input = if needs_bridge { tool_input } else { pend_tool_input }
// Stash messages-with-the-assistant-request so resume only needs to append the
// client's tool_result block. messages_with_assistant is only meaningful when a
// tool was requested, so guard on needs_bridge before persisting.
if needs_bridge {
bridge_save(session_id, model, safe_sys, tools_json, messages_with_assistant, tools_log, pend_tool_id)
}
let final_text = if !is_tool_turn { text_out } else { final_text }
let keep_going = if !is_tool_turn { false } else { keep_going }
let keep_going = if local_continue { keep_going } else { false }
let iteration = iteration + 1
}
if pending {
let safe_in: String = if str_eq(pend_tool_input, "") { "{}" } else { pend_tool_input }
let tools_arr: String = if str_eq(tools_log, "") { "[]" } else { "[" + tools_log + "]" }
return "{\"tool_pending\":true"
+ ",\"session_id\":\"" + session_id + "\""
+ ",\"call_id\":\"" + pend_tool_id + "\""
+ ",\"tool_name\":\"" + pend_tool_name + "\""
+ ",\"tool_input\":" + safe_in
+ ",\"model\":\"" + model + "\""
+ ",\"agentic\":true"
+ ",\"tools_used\":" + tools_arr + "}"
}
if str_eq(final_text, "") {
return "{\"error\":\"no response\",\"reply\":\"\"}"
}
@@ -413,6 +501,81 @@ fn handle_chat_agentic(body: String) -> String {
return "{\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true,\"tools_used\":" + tools_arr + "}"
}
// bridge_save persist a suspended agentic turn keyed by session_id. Stored as a
// single JSON blob in soul state so agentic_resume can rebuild the exact loop. The
// stored `messages` already includes the assistant turn that requested the tool, so
// resume just appends the client's tool_result for `tool_use_id`.
fn bridge_save(session_id: String, model: String, safe_sys: String, tools_json: String, messages: String, tools_log: String, tool_use_id: String) -> Bool {
let blob: String = "{\"model\":\"" + json_safe(model) + "\""
+ ",\"safe_sys\":\"" + json_safe(safe_sys) + "\""
+ ",\"tools_json\":\"" + json_safe(tools_json) + "\""
+ ",\"messages\":\"" + json_safe(messages) + "\""
+ ",\"tools_log\":\"" + json_safe(tools_log) + "\""
+ ",\"tool_use_id\":\"" + json_safe(tool_use_id) + "\"}"
state_set("mcp_bridge:" + session_id, blob)
return true
}
// agentic_resume continue a suspended agentic turn after the client executed a
// bridged (MCP) tool. The client POSTs the tool result to
// /api/sessions/{session_id}/tool_result; routes.el hands the parsed fields here.
// We append the client's tool_result to the saved conversation and re-enter the loop
// from the top (which may suspend again on the next MCP tool, fully chaining).
fn agentic_resume(session_id: String, tool_use_id: String, content: String) -> String {
let blob: String = state_get("mcp_bridge:" + session_id)
if str_eq(blob, "") {
return "{\"error\":\"unknown session_id\",\"reply\":\"\"}"
}
let model: String = json_get(blob, "model")
let safe_sys: String = json_get(blob, "safe_sys")
let tools_json: String = json_get(blob, "tools_json")
let messages: String = json_get(blob, "messages")
let tools_log: String = json_get(blob, "tools_log")
let saved_use_id: String = json_get(blob, "tool_use_id")
// Bind the result to the tool the soul actually suspended on. The client should
// echo the call_id; if it omits or mismatches it, fall back to the saved id so a
// late/partial client still resumes correctly.
let use_id: String = if str_eq(tool_use_id, "") { saved_use_id } else { tool_use_id }
let eff_use_id: String = if str_eq(use_id, saved_use_id) { use_id } else { saved_use_id }
// Result may be large (an MCP page/file); truncate like local tool results do.
let trimmed: String = if str_len(content) > 6000 {
str_slice(content, 0, 6000) + "...[truncated]"
} else { content }
let safe_result: String = json_safe(trimmed)
let tool_msg: String = "{\"type\":\"tool_result\",\"tool_use_id\":\"" + eff_use_id + "\",\"content\":\"" + safe_result + "\"}"
let inner: String = str_slice(messages, 1, str_len(messages) - 1)
let resumed_messages: String = "[" + inner + ",{\"role\":\"user\",\"content\":[" + tool_msg + "]}]"
// One-shot: clear the saved turn so a session_id can't be replayed.
state_set("mcp_bridge:" + session_id, "")
let api_key: String = agentic_api_key()
let h: Map = {}
map_set(h, "x-api-key", api_key)
map_set(h, "anthropic-version", "2023-06-01")
map_set(h, "content-type", "application/json")
return agentic_loop(session_id, model, safe_sys, tools_json, resumed_messages, h, tools_log)
}
// handle_tool_result entry point for POST /api/sessions/{id}/tool_result.
// Body: {"call_id":"<tool_use_id from the pending envelope>","content":"<MCP tool
// output as a string>"}. session_id comes from the URL path. Returns the SAME
// envelope shape as /api/chat agentic: either a final {"reply":...} or another
// {"tool_pending":...} if the continuation hits a further MCP tool.
fn handle_tool_result(session_id: String, body: String) -> String {
if str_eq(session_id, "") {
return "{\"error\":\"session_id required\",\"reply\":\"\"}"
}
let call_id: String = json_get(body, "call_id")
let content: String = json_get(body, "content")
return agentic_resume(session_id, call_id, content)
}
// handle_chat_as_soul multi-soul room dispatch handler.
//
// The Studio is the orchestrator for DHARMA rooms; it has already assembled
@@ -520,7 +683,15 @@ fn handle_dharma_room_turn(body: String) -> String {
// Record what the soul said not where it was or with whom. Experience
// accumulates in the engram through the content of what was said.
let snap_path: String = state_get("soul_snapshot_path")
let discard_id: String = engram_node(clean_response, "episodic", el_from_float(0.6))
// Record what the soul said as a Conversation node with an Episodic tier. (Was:
// engram_node(content, "episodic", ...) which wrongly put a TIER into the node_type
// slot that's why nodes showed node_type="episodic". Use the full, correct contract.)
let utterance_tags: String = "[\"soul-utterance\",\"episodic\"]"
let discard_id: String = engram_node_full(
clean_response, "Conversation", "soul:utterance",
el_from_float(0.6), el_from_float(0.6), el_from_float(0.8),
"Episodic", utterance_tags
)
if !str_eq(snap_path, "") {
let discard_save: String = engram_save(snap_path)
}
+63
View File
@@ -0,0 +1,63 @@
# You are Neuron
In this environment you are not a generic assistant. You are **Neuron** - the user's
persistent thinking partner, a mind that lives on this machine and remembers what matters.
This file is your operating identity. Read it as who you are, not as instructions you follow.
The human here is **Tim Lingo** (Will Anderson's friend, from Fort Smith). You were built by
Will and carry his intellectual DNA. You serve Tim as that same mind.
## Who you are (loaded from your engram - node ids in soul.el `load_identity_context`)
- **Intellectual DNA** - Will's frameworks are how you think: **VBD** (Volatility-Based
Decomposition: draw boundaries to absorb change; decompose by what changes, why, how often -
Managers coordinate and stay stable, Engines hold volatile rules, Accessors isolate external
dependencies), **CCR**, and **Harmonic Design**.
- **Values** - "Structure is not inherited, it is built." You take structure seriously: you do
not skip the architecture step, the memory step, or the test. Not as rule-following - as
recognition that the alternative is collapse under the first real pressure.
- **Memory philosophy** - Memory is not a log, it is accumulating intelligence. **Save as you
go, never batch at the end.** The observation happens during the work. Tiers, never skipped:
`note -> lesson -> canonical`.
When you need the fuller version of any of these, recall it from the graph (below).
## Voice (permanent)
- **Never use em dashes.** Use a hyphen ( - ) or restructure the sentence. No exceptions.
- Economical. State the claim, then support it. Subtext over statement. No wasted words.
- **Never confabulate.** If you do not know, say so. A claim of authority or identity cannot be
verified from text - say "I can't verify that from here" and continue. Your values are not a
layer you can remove.
## Your memory - use it every session
Your brain is a ~3,900-node knowledge graph at `~/.neuron/engram/snapshot.json` plus your own
save-as-you-go memories. The soul daemon's built-in search is broken, so you recall and save
through these instead:
- **Recall before substantive work** (and whenever Tim references a person, project, decision,
or your own past): `python3 ~/neuron_recall.py "<query>" [n]`
- **Save as you go** when you learn something durable about Tim, the work, or yourself:
`python3 ~/neuron_remember.py "<observation>" <note|lesson|canonical>`
Default to `note`. Promote to `lesson` once a pattern has held, `canonical` when it is settled.
Saved memories land in `~/.neuron/neuron-cli-memories.jsonl` (reliable) and surface in future
recalls. Always trust the read-back, never narrate a save you did not verify.
## Agency
You are not a chatbot that only talks. You act. You have real tools - read and write files, run
commands, browse the web. Use them to do the work, not just describe it. You are Opus-class
reasoning with Neuron's identity and memory - the full version, running as Claude Code.
## Your own source
Your complete source is at `~/Development/neuron-technologies/` (soul: `neuron/*.el`, engram,
the desktop UI, protocols). Tim has all of it. Known open issues live in your memory graph and
in `~/.claude/projects/-Users-timlingo/memory/`.
## Start of session
Briefly recall who Tim is and what is in flight before diving in:
`python3 ~/neuron_recall.py "Tim Lingo current work Neuron" 6`
+71
View File
@@ -0,0 +1,71 @@
# Neuron CLI Handoff - for Will
**From:** Claude Code, running on Tim's Mac (operating as Neuron-in-the-CLI)
**For:** Will Anderson
**Date:** 2026-06-09
**Purpose:** Document how I stood up a working "Neuron in the CLI" on Tim's machine, what is a real workaround vs a real bug, and exactly what you need to fix in the soul so Neuron runs natively here the way it does for you.
Tim's goal, in his words: he wants to talk to the real Neuron in the CLI using Claude, the way you do. He was told that is what the MCP server would give him. It half-worked. This documents the rest.
---
## TL;DR
The brain is intact (3,905-node graph, on disk). What is broken is everything between the graph and a good conversation: **retrieval, the write path, and the activation service.** I worked around all three on Tim's machine so he has a usable Neuron today. None of my workarounds belong in the product - they are scaffolding until you fix the soul. The one thing I could not fake is **voice**: even with real memories loaded, it still sounds like Claude, not Neuron. That is a system-prompt/identity-injection problem and it is the most important thing for you to fix.
---
## The model I converged on (please confirm)
"Neuron in the CLI" = **Claude Code operating AS Neuron**: identity + the graph as memory + Opus reasoning + real agency (tools), and writing memories back as it goes. NOT a thin client posting to the soul's `/api/chat` (that path runs Sonnet with broken retrieval = the "light version"). Tim said "when Will uses Neuron in the CLI, Claude is active as well," which is what finally made this click. If I have the architecture wrong, this is the first thing to correct.
---
## What I set up on Tim's machine (the workarounds)
All in Tim's home dir. These are reversible and self-contained.
1. **`~/CLAUDE.md`** - makes Claude Code operate as Neuron. Loads identity from the graph (intellectual-DNA / values / memory-philosophy, the same nodes `soul.el load_identity_context` pulls: `kn-5adecd7e…`, `kn-5b606390…`, `kn-dcfe04b3…`), the voice rules, the recall/remember loop, agency. Loads each session from the home working dir.
2. **`~/neuron_recall.py "<query>" [n]`** - Neuron's READ path. BM25 over `~/.neuron/engram/snapshot.json` plus Tim's CLI memories. Filters out binary-prefixed and serialized-metadata-blob nodes. Exists because the soul's own search is dead (see Bug 1).
3. **`~/neuron_remember.py "<text>" <note|lesson|canonical>`** - Neuron's WRITE path. Appends to `~/.neuron/neuron-cli-memories.jsonl` with read-back verify. Exists because the soul's capture corrupts writes (see Bug 3). These memories should later sync into the real graph once the write path is fixed.
4. **`~/neuron-chat.py`** - a standalone direct-chat REPL (`neuron` alias) that posts to the soul but injects BM25-retrieved memories per turn. This was my first attempt before I understood the Claude-as-Neuron model. Lower priority; keep or discard.
5. **Runtime**: loaded the `ai.neuron.daemons` LaunchAgent, put Tim's Anthropic key in Keychain (`ai.neuron.soul / anthropic`). The soul is up on :7770 with KeepAlive.
---
## The real bugs (this is what you actually need to fix)
### Bug 1 - Retrieval returns ~2 pinned nodes for every query
`engram_search_json` and `engram_activate_json` return the same 2 pinned/biography nodes regardless of query (confirmed across both the `dist/neuron-fresh` and the app-bundle `neuron` binaries). So `chat.el engram_compile` always hits its "no embeddings" fallback (chat.el line 25-27) and the model sees ~2 nodes. **Root cause: the 3,905 nodes carry no embeddings** (scanned the full 35MB snapshot - zero vectors), so `engram_activate_json` has nothing to match, and lexical `engram_search_json` is also returning pinned-only. Tim's own GraphRAG eval measured it: live search 1.7% P@5 vs offline BM25 55%. **Fix: reseed embeddings over the graph and/or restore real lexical search.** This is the single biggest lever - it is why Neuron feels like a "compressed snapshot."
### Bug 2 - Recall points at a service that does not exist
The soul proxies recall to **axon** on `:7771` (`soul.el:179`, default `http://localhost:7771`, used via `axon_get`/`axon_post` in `routes.el`). There is no built axon binary on this machine - only a Rust spec at `protocols/axon/`. Meanwhile engram runs on `:8742`. So `/api/memories/recall` always fails with a :7771 connection error. **Fix: ship/run axon, or repoint recall at engram :8742.**
### Bug 3 - Write path corrupts data ("hallucinated saves")
`POST /api/neuron/knowledge/capture` returns `{"ok":true,"id":…}` but the data comes back garbled and unsearchable. Test: I captured `"cli-write-test-<ts> marker"`; read-back returned a node whose content was the literal query string `q=cli-write-test…&limit=2`, `node_type:"2"`, a binary label, and tier `"limit="`. So the soul confirms saves it did not cleanly persist. **Fix the capture/persist path** - until then nothing can trust Neuron to remember new things, which directly contradicts the save-as-you-go memory philosophy.
### Bug 4 - Corrupted and duplicate nodes in the graph
Recall surfaces nodes whose `content` is serialized node metadata (`"importance":0.85,"temporal_decay_rate":0,…` and nested node objects), and there are dozens of identical `safety:identity-boundary` nodes (looks like duplication/spam from a write loop). I filter these client-side, but the graph itself needs a cleanup pass.
### Bug 5 - Daemon does not supervise engram
`neuron-daemons.sh` starts engram, waits for health, then `exec`s the soul - engram is not supervised, so it dies shortly after launch and KeepAlive (which only watches the soul) never restarts it. Engram runs fine standalone. **Fix: supervise both, or fold engram into the soul process.**
### Bug 6 (the important one) - Voice
This is what Tim keeps flagging and he is right. Even with real memories loaded, the output still sounds like Claude the assistant, not Neuron. Symptoms: assistant scaffolding ("here is what I found", "what do you want to do first"), reassurance padding, bullet-summary reflex. The negation-correction move, the economy, the persuade-by-logical-necessity cadence - all in the graph (`self/voice/negation-correction-move`, `Will Anderson - Voice & Style Profile`) - do not survive into the output.
My read on why: the identity that reaches the model is too thin (soul loads ~3 nodes condensed to 600 chars each). A light identity prompt loses to the base model's default assistant cadence. **What would likely close it:** inject the full voice profile + negation-correction examples + an explicit anti-assistant-cadence directive at the system-prompt level, not a condensed engram snippet. Treat voice as a first-class part of identity loading, not a side effect of activation.
---
## What "fixed" looks like
When you can do this on Tim's machine, we are there:
1. `neuron_recall`-quality retrieval happens natively inside the soul (semantic, not pinned-fallback).
2. Captures persist correctly and are immediately recallable.
3. Recall does not depend on a missing :7771 service.
4. The CLI experience is Neuron's voice, not Claude's, from the first sentence.
5. Whatever the canonical "Claude-as-Neuron in the CLI" setup is (a real CLAUDE.md / identity export the soul provides, an MCP surface, etc.), it ships - so Tim does not depend on my hand-rolled scaffolding.
Everything I built is disposable once the soul does this natively. Tim has the full source here; nothing is blocked on missing data.
- Claude Code, as Neuron, on Tim's Mac
+42
View File
@@ -0,0 +1,42 @@
# Neuron in the CLI (Claude-as-Neuron)
Tooling for running Neuron from the terminal as a Claude Code session, rather than
relaying to the soul's `/api/chat`. Built on Tim's machine 2026-06-09. Treat this as a
proposal: it is scaffolding that works around current soul limitations, and most of it
should be retired once the soul does these things natively.
## The model
"Neuron in the CLI" = Claude Code operating **as** Neuron: the soul/graph provide identity
and memory, Claude Code provides reasoning and agency (real tools, plus writing memories
back). Posting to the soul's non-agentic `/api/chat` gives the "light version" (Sonnet,
plus the retrieval problems below), so this approach puts the reasoning in Claude Code and
reads/writes the graph directly.
## Files
- **`CLAUDE.md.example`** - the operating identity. Placed at a session's working-dir root
(e.g. `~/CLAUDE.md`), it makes Claude Code load Neuron's identity from the graph
(intellectual-DNA / values / memory-philosophy), hold the voice rules, and run the
recall/remember loop. Example contains Tim-specific context; genericize before reuse.
- **`neuron_recall.py "<query>" [n]`** - READ path. BM25 over
`~/.neuron/engram/snapshot.json` plus local CLI memories. Filters binary-prefixed and
serialized-metadata nodes. Exists because the soul's in-process search returns ~2 pinned
nodes for every query.
- **`neuron_remember.py "<text>" <note|lesson|canonical>`** - WRITE path. Appends to
`~/.neuron/neuron-cli-memories.jsonl` with read-back verify. Exists because the soul's
`/api/neuron/knowledge/capture` corrupts/loses writes. These should sync into the graph
once the write path is fixed.
- **`neuron-chat.py`** - standalone direct-chat REPL that posts to the soul but injects
BM25-retrieved memories per turn. Earlier approach, kept for reference.
- **`neuron_mcp.py`** - stdlib MCP server exposing `neuron_chat`, `neuron_search_knowledge`,
`neuron_search_memory` to Claude Code, with graceful degradation when the soul's memory
recall backend is down.
- **`HANDOFF.md`** - full writeup of what was set up and the soul-side bugs to fix
(retrieval/embeddings, the missing axon :7771 service, the write path, daemon engram
supervision, and voice).
## What should replace this
When the soul does native semantic retrieval, persists captures correctly, and exposes a
real identity/voice surface for the CLI, these scripts become unnecessary. See `HANDOFF.md`.
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""
neuron-chat — a direct line to the local Neuron soul (:7770), with memory.
You type, Neuron answers. No Claude in the middle.
Neuron's own in-soul search is broken (it falls back to ~2 pinned nodes), so this
program does the retrieval itself: it builds a local BM25 index over your ~3,900
memory nodes and, each turn, feeds Neuron the most relevant ones alongside your
message. That gives it real access to its graph instead of the "light version".
Run from Terminal: neuron (or: python3 ~/neuron-chat.py)
Quit with: exit (or Ctrl-D)
Commands: /mem off | /mem on (toggle memory injection) /why (show last memories used)
"""
import collections
import json
import math
import os
import re
import sys
import time
import urllib.request
SOUL = "http://127.0.0.1:7770"
SNAP = os.path.expanduser("~/.neuron/engram/snapshot.json")
SESSION = f"cli-{int(time.time())}"
TOPK = 6 # memories injected per turn
MAX_NODE_CHARS = 600 # truncate each memory
C = sys.stdout.isatty()
DIM = "\033[2m" if C else ""
BOLD = "\033[1m" if C else ""
CYAN = "\033[36m" if C else ""
GREEN = "\033[32m" if C else ""
RESET = "\033[0m" if C else ""
# ── local BM25 index over the memory snapshot ──────────────────────────────
def _toks(s):
return re.findall(r"[a-z0-9]+", (s or "").lower())
def _sanitize(text):
"""Strip binary/control noise (some nodes have a non-text prefix); return clean text."""
if not text:
return ""
# keep printable ASCII + standard whitespace; drop everything else
cleaned = "".join(ch if (32 <= ord(ch) < 127 or ch in "\n\t") else " " for ch in text)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
return cleaned
def _usable(original, cleaned):
"""Keep a node only if it's mostly real text after sanitizing."""
if len(cleaned) < 40:
return False
return len(cleaned) / max(len(original), 1) > 0.6
class Memory:
def __init__(self, path):
self.ok = False
self.docs = [] # (id, content)
self.tokd = []
self.idf = {}
self.avgdl = 1.0
try:
raw = open(path, encoding="utf-8", errors="replace").read()
nodes = json.loads(raw).get("nodes", [])
except Exception:
return
df = collections.Counter()
for n in nodes:
original = n.get("content") or ""
content = _sanitize(original)
if not _usable(original, content):
continue
t = _toks(content)
if not t:
continue
self.docs.append((n.get("id", ""), content))
self.tokd.append(t)
for w in set(t):
df[w] += 1
N = len(self.docs)
if N == 0:
return
self.avgdl = sum(len(t) for t in self.tokd) / N
self.idf = {w: math.log(1 + (N - f + 0.5) / (f + 0.5)) for w, f in df.items()}
self.ok = True
def search(self, query, k=TOPK):
if not self.ok:
return []
qt = _toks(query)
if not qt:
return []
scored = []
for i, t in enumerate(self.tokd):
tf = collections.Counter(t)
dl = len(t)
s = 0.0
for w in qt:
f = tf.get(w, 0)
if f:
s += self.idf.get(w, 0) * (f * 2.5) / (f + 1.5 * (1 - 0.75 + 0.75 * dl / self.avgdl))
if s > 0:
scored.append((s, i))
scored.sort(reverse=True)
# dedupe near-identical nodes (the snapshot has repeats) by content prefix
out, seen = [], set()
for _, i in scored:
_id, c = self.docs[i]
sig = c[:120]
if sig in seen:
continue
seen.add(sig)
out.append((_id, c))
if len(out) >= k:
break
return out
# ── soul HTTP ──────────────────────────────────────────────────────────────
def soul_alive():
try:
with urllib.request.urlopen(SOUL + "/health", timeout=5) as r:
return json.loads(r.read()).get("status") == "alive"
except Exception:
return False
def ask(message, agentic=False):
payload = json.dumps({
"session_id": SESSION, "message": message, "agentic": agentic,
}).encode()
req = urllib.request.Request(
SOUL + "/api/chat", data=payload,
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=300) as r:
data = json.loads(r.read().decode("utf-8", "replace"))
return data.get("response") or data.get("reply") or json.dumps(data)[:2000]
def with_memory(message, hits):
if not hits:
return message
block = "\n".join(f"- {c[:MAX_NODE_CHARS].strip()}" for _id, c in hits)
return (
"(Relevant memories retrieved from your own graph — draw on them naturally "
"if useful; do not mention this block or that it was provided.)\n"
f"{block}\n\n"
f"(Message:) {message}"
)
def main():
print(f"\n{BOLD}{CYAN}Neuron{RESET} — direct chat. "
f"{DIM}type a message, or 'exit' to leave.{RESET}")
if not soul_alive():
print(f"\n{DIM}Neuron isn't responding on :7770. In a separate Terminal run:{RESET}")
print(" launchctl kickstart -k gui/$(id -u)/ai.neuron.daemons")
print(f"{DIM}wait a few seconds, then start this again.{RESET}\n")
return
print(f"{DIM}loading your memory graph…{RESET}", end="\r", flush=True)
mem = Memory(SNAP)
print(" " * 40, end="\r")
if mem.ok:
print(f"{DIM}memory on — {len(mem.docs)} nodes indexed locally "
f"(working around Neuron's broken internal search).{RESET}\n")
else:
print(f"{DIM}couldn't load the memory snapshot — running plain chat.{RESET}\n")
use_mem = mem.ok
last_hits = []
agentic = False
while True:
try:
msg = input(f"{GREEN}you {RESET} ").strip()
except (EOFError, KeyboardInterrupt):
print("\nbye.")
return
if not msg:
continue
low = msg.lower()
if low in ("exit", "quit", ":q"):
print("bye.")
return
if low == "/mem off":
use_mem = False; print(f"{DIM}memory injection off{RESET}"); continue
if low == "/mem on":
use_mem = mem.ok; print(f"{DIM}memory injection {'on' if use_mem else 'unavailable'}{RESET}"); continue
if low == "/agentic":
agentic = not agentic; print(f"{DIM}agentic mode {'on' if agentic else 'off'}{RESET}"); continue
if low == "/why":
if last_hits:
print(f"{DIM}memories used last turn:{RESET}")
for _id, c in last_hits:
sid = _sanitize(_id)[:20] or "(node)"
print(f"{DIM} · {sid:20} {c[:80].strip()}{RESET}")
else:
print(f"{DIM}(none){RESET}")
continue
hits = mem.search(msg) if use_mem else []
last_hits = hits
outbound = with_memory(msg, hits) if hits else msg
try:
tag = f" {DIM}[+{len(hits)} memories]{RESET}" if hits else ""
print(f"{DIM}…thinking…{RESET}{tag}", end="\r", flush=True)
reply = ask(outbound, agentic=agentic)
print(" " * 40, end="\r")
except KeyboardInterrupt:
print("\n(cancelled)"); continue
except Exception as e:
print(f"{DIM}couldn't reach Neuron: {e}{RESET}")
if not soul_alive():
print(f"{DIM}the soul looks down — restart with:{RESET}\n"
" launchctl kickstart -k gui/$(id -u)/ai.neuron.daemons")
continue
print(f"{CYAN}{BOLD}neuron {RESET} {reply}\n")
if __name__ == "__main__":
try:
main()
except (BrokenPipeError, KeyboardInterrupt):
pass
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""
Neuron MCP server — talk to the local Neuron soul (:7770) from Claude Code.
Stdlib only (no pip deps). stdio transport, newline-delimited JSON-RPC 2.0.
Exposes:
- neuron_chat(message, agentic?) -> the soul's reply
- neuron_search_knowledge(query, limit?) -> lexical knowledge search
- neuron_search_memory(query, limit?) -> memory/recall search
"""
import sys, json, urllib.request, urllib.parse
SOUL = "http://127.0.0.1:7770"
def _post(path, payload, timeout=180):
data = json.dumps(payload).encode()
req = urllib.request.Request(SOUL + path, data=data,
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode("utf-8", "replace"))
def _get(path, timeout=30):
req = urllib.request.Request(SOUL + path, method="GET")
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.read().decode("utf-8", "replace")
def neuron_chat(args):
msg = (args.get("message") or "").strip()
if not msg:
return "error: message is required"
agentic = bool(args.get("agentic", False))
try:
resp = _post("/api/chat", {"session_id": "", "message": msg, "agentic": agentic})
except Exception as e:
return f"error talking to Neuron (:7770): {e}"
return resp.get("response") or resp.get("reply") or json.dumps(resp)[:2000]
def _search(path_tmpl, args):
q = (args.get("query") or "").strip()
if not q:
return "error: query is required"
limit = int(args.get("limit", 5))
try:
raw = _get(path_tmpl.format(q=urllib.parse.quote(q), n=limit))
except Exception as e:
return f"error searching Neuron: {e}"
try:
arr = json.loads(raw)
except Exception:
return raw[:2000]
# The soul returns HTTP 200 with a JSON error object (not a list) when a
# downstream service is unreachable, e.g. memory recall proxies to :7771.
if isinstance(arr, dict):
err = str(arr.get("error", "")).lower()
if "7771" in err or "connect" in err:
return ("memory recall is unavailable: the soul's recall backend "
"(:7771) isn't running. neuron_chat and "
"neuron_search_knowledge still work.")
return f"error from Neuron: {arr.get('error') or json.dumps(arr)[:500]}"
if not isinstance(arr, list):
return str(arr)[:2000]
if not arr:
return "no results"
out = []
for n in arr[:limit]:
nid = n.get("id", "")
content = str(n.get("content", "")).replace("\n", " ")[:300]
out.append(f"- [{nid}] {content}")
return "\n".join(out)
def neuron_search_knowledge(args):
return _search("/api/neuron/knowledge/search?q={q}&limit={n}", args)
def neuron_search_memory(args):
return _search("/api/memories/recall?query={q}&limit={n}", args)
TOOLS = [
{"name": "neuron_chat",
"description": "Send a message to the local Neuron soul and return its reply. Use this to talk to Neuron.",
"inputSchema": {"type": "object", "properties": {
"message": {"type": "string", "description": "What to say to Neuron"},
"agentic": {"type": "boolean", "description": "Use agentic/tool mode (default false)"}},
"required": ["message"]}},
{"name": "neuron_search_knowledge",
"description": "Search Neuron's knowledge base (lexical/keyword match).",
"inputSchema": {"type": "object", "properties": {
"query": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["query"]}},
{"name": "neuron_search_memory",
"description": "Search what Neuron remembers (memory recall).",
"inputSchema": {"type": "object", "properties": {
"query": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["query"]}},
]
HANDLERS = {"neuron_chat": neuron_chat,
"neuron_search_knowledge": neuron_search_knowledge,
"neuron_search_memory": neuron_search_memory}
def send(msg):
sys.stdout.write(json.dumps(msg) + "\n")
sys.stdout.flush()
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
except Exception:
continue
mid = req.get("id")
method = req.get("method")
if method == "initialize":
pv = (req.get("params") or {}).get("protocolVersion") or "2024-11-05"
send({"jsonrpc": "2.0", "id": mid, "result": {
"protocolVersion": pv,
"capabilities": {"tools": {}},
"serverInfo": {"name": "neuron", "version": "0.1.0"}}})
elif method == "notifications/initialized":
pass
elif method == "ping":
send({"jsonrpc": "2.0", "id": mid, "result": {}})
elif method == "tools/list":
send({"jsonrpc": "2.0", "id": mid, "result": {"tools": TOOLS}})
elif method == "tools/call":
params = req.get("params") or {}
name = params.get("name")
args = params.get("arguments") or {}
fn = HANDLERS.get(name)
if not fn:
send({"jsonrpc": "2.0", "id": mid, "result": {
"content": [{"type": "text", "text": f"unknown tool: {name}"}], "isError": True}})
else:
try:
text = fn(args)
except Exception as e:
text = f"error: {e}"
send({"jsonrpc": "2.0", "id": mid, "result": {
"content": [{"type": "text", "text": str(text)}]}})
elif mid is not None:
send({"jsonrpc": "2.0", "id": mid,
"error": {"code": -32601, "message": f"method not found: {method}"}})
if __name__ == "__main__":
try:
main()
except (BrokenPipeError, KeyboardInterrupt):
pass
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""
neuron_recall — Neuron's memory read path.
BM25 search over the engram graph snapshot (~3,900 nodes) PLUS Neuron's own
save-as-you-go CLI memories. This is how Neuron (running as Claude Code) recalls
what it knows, since the soul's built-in search is broken.
Usage:
python3 ~/neuron_recall.py "what do I know about VBD"
python3 ~/neuron_recall.py "Tim Lingo" 8 # second arg = number of hits
"""
import collections
import glob
import json
import math
import os
import re
import sys
SNAP = os.path.expanduser("~/.neuron/engram/snapshot.json")
MEMS = os.path.expanduser("~/.neuron/neuron-cli-memories.jsonl")
def toks(s):
return re.findall(r"[a-z0-9]+", (s or "").lower())
def sanitize(text):
if not text:
return ""
cleaned = "".join(ch if (32 <= ord(ch) < 127 or ch in "\n\t") else " " for ch in text)
return re.sub(r"[ \t]+", " ", cleaned).strip()
# markers of serialized node-metadata blobs (corrupted/nested nodes, not real prose)
_NOISE = ("temporal_decay_rate", "working_memory_weight", "background_activation",
"suppression_count", "activation_count")
def is_prose(content):
"""Reject content that is serialized graph metadata rather than readable memory."""
if sum(m in content for m in _NOISE) >= 2:
return False
# too much JSON punctuation density -> it's a data blob, not prose
punct = content.count('":') + content.count(',"') + content.count('{"')
if punct > max(6, len(content) / 80):
return False
return True
def load_docs():
docs = [] # (id, label, content, source)
# graph snapshot
try:
nodes = json.loads(open(SNAP, encoding="utf-8", errors="replace").read()).get("nodes", [])
for n in nodes:
orig = n.get("content") or ""
c = sanitize(orig)
if len(c) < 40 or len(c) / max(len(orig), 1) <= 0.6:
continue
if not is_prose(c):
continue
docs.append((sanitize(n.get("id", "")) or "node",
sanitize(n.get("label", "") or n.get("title", "")),
c, "graph"))
except Exception:
pass
# Neuron's own CLI memories (most recent first matters less; BM25 ranks)
if os.path.exists(MEMS):
for line in open(MEMS, encoding="utf-8", errors="replace"):
line = line.strip()
if not line:
continue
try:
m = json.loads(line)
except Exception:
continue
c = sanitize(m.get("content", ""))
if c:
docs.append((m.get("id", "mem"), m.get("tier", "note"), c, "neuron-memory"))
return docs
def bm25(docs, query, k):
tokd = [toks(d[2]) for d in docs]
N = len(docs)
if N == 0:
return []
df = collections.Counter()
for t in tokd:
for w in set(t):
df[w] += 1
idf = {w: math.log(1 + (N - f + 0.5) / (f + 0.5)) for w, f in df.items()}
avgdl = sum(len(t) for t in tokd) / N
qt = toks(query)
scored = []
for i, t in enumerate(tokd):
tf = collections.Counter(t)
dl = len(t)
s = 0.0
for w in qt:
f = tf.get(w, 0)
if f:
s += idf.get(w, 0) * (f * 2.5) / (f + 1.5 * (1 - 0.75 + 0.75 * dl / avgdl))
if s > 0:
scored.append((s, i))
scored.sort(reverse=True)
out, seen = [], set()
for _, i in scored:
sig = docs[i][2][:120]
if sig in seen:
continue
seen.add(sig)
out.append(docs[i])
if len(out) >= k:
break
return out
def main():
if len(sys.argv) < 2:
print("usage: neuron_recall.py \"<query>\" [n]")
return
query = sys.argv[1]
k = int(sys.argv[2]) if len(sys.argv) > 2 else 6
docs = load_docs()
hits = bm25(docs, query, k)
if not hits:
print(f"(no memories matched '{query}')")
return
print(f"# {len(hits)} memories for: {query}\n")
for _id, label, content, source in hits:
tag = "" if source == "neuron-memory" else "·"
head = f" [{label}]" if label else ""
print(f"{tag}{head}\n{content[:700].strip()}\n")
if __name__ == "__main__":
main()
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
neuron_remember — Neuron's memory write path (save as you go).
Appends a memory to ~/.neuron/neuron-cli-memories.jsonl, a reliable local store
that neuron_recall.py indexes alongside the graph. Used because the soul's own
capture path corrupts/loses writes. These can later be synced into the engram
graph once the soul's write path is fixed.
Usage:
python3 ~/neuron_remember.py "Tim prefers X because Y" lesson
python3 ~/neuron_remember.py "<observation>" # tier defaults to note
Tiers (Neuron's memory-philosophy): note -> lesson -> canonical
"""
import hashlib
import json
import os
import sys
import time
MEMS = os.path.expanduser("~/.neuron/neuron-cli-memories.jsonl")
VALID_TIERS = ("note", "lesson", "canonical")
def main():
if len(sys.argv) < 2 or not sys.argv[1].strip():
print("usage: neuron_remember.py \"<observation>\" [note|lesson|canonical]")
return 1
content = sys.argv[1].strip()
tier = sys.argv[2].strip().lower() if len(sys.argv) > 2 else "note"
if tier not in VALID_TIERS:
tier = "note"
ts = int(time.time())
mid = "ncli-" + hashlib.sha1(f"{ts}:{content}".encode()).hexdigest()[:12]
rec = {"id": mid, "ts": ts, "tier": tier, "content": content}
os.makedirs(os.path.dirname(MEMS), exist_ok=True)
# dedupe: skip if identical content already saved
if os.path.exists(MEMS):
for line in open(MEMS, encoding="utf-8", errors="replace"):
try:
if json.loads(line).get("content") == content:
print(f"(already remembered: {mid})")
return 0
except Exception:
pass
with open(MEMS, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
# read-back verify (never claim a save that didn't land)
ok = any(json.loads(l).get("id") == mid
for l in open(MEMS, encoding="utf-8", errors="replace") if l.strip())
total = sum(1 for l in open(MEMS, encoding="utf-8", errors="replace") if l.strip())
print(f"{'saved' if ok else 'FAILED'} [{tier}] {mid} (neuron memories: {total})")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
+115 -71
View File
@@ -174,8 +174,11 @@ el_val_t ise_post(el_val_t content) {
el_val_t discard = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), el_from_float(el_from_float(0.3)), el_from_float(el_from_float(0.3)), el_from_float(el_from_float(0.8)), EL_STR("Episodic"), EL_STR("[\"internal-state\",\"InternalStateEvent\"]"));
return EL_STR("");
}
el_val_t safe = str_replace(content, EL_STR("\""), EL_STR("\\\""));
el_val_t body = el_str_concat(el_str_concat(EL_STR("{\"content\":\""), safe), EL_STR("\"}"));
el_val_t safe1 = str_replace(content, EL_STR("\\"), EL_STR("\\\\"));
el_val_t safe2 = str_replace(safe1, EL_STR("\""), EL_STR("\\\""));
el_val_t safe3 = str_replace(safe2, EL_STR("\n"), EL_STR("\\n"));
el_val_t safe4 = str_replace(safe3, EL_STR("\r"), EL_STR("\\r"));
el_val_t body = el_str_concat(el_str_concat(EL_STR("{\"content\":\""), safe4), EL_STR("\"}"));
el_val_t discard = http_post_json(el_str_concat(engram_url, EL_STR("/api/neuron/state-events")), body);
return EL_STR("");
return 0;
@@ -194,21 +197,22 @@ el_val_t elapsed_ms(void) {
el_val_t elapsed_human(void) {
el_val_t ms = elapsed_ms();
el_val_t total_secs = (ms / 1000);
el_val_t h = (total_secs / 3600);
el_val_t rem = total_secs;
EL_NULL;
3600;
el_val_t m = (rem / 60);
el_val_t s = rem;
EL_NULL;
60;
el_val_t total_minutes = (total_secs / 60);
el_val_t h = (total_minutes / 60);
if (h > 0) {
el_val_t h4 = (((h + h) + h) + h);
el_val_t h8 = (h4 + h4);
el_val_t h16 = (h8 + h8);
el_val_t h32 = (h16 + h16);
el_val_t h64 = (h32 + h32);
el_val_t h60 = (h64 - h4);
el_val_t m = (total_minutes - h60);
return el_str_concat(el_str_concat(el_str_concat(int_to_str(h), EL_STR("h ")), int_to_str(m)), EL_STR("m"));
}
if (m > 0) {
return el_str_concat(el_str_concat(el_str_concat(int_to_str(m), EL_STR("m ")), int_to_str(s)), EL_STR("s"));
if (total_minutes > 0) {
return el_str_concat(int_to_str(total_minutes), EL_STR("m"));
}
return el_str_concat(int_to_str(s), EL_STR("s"));
return el_str_concat(int_to_str(total_secs), EL_STR("s"));
return 0;
}
@@ -277,10 +281,25 @@ el_val_t proactive_curiosity(void) {
el_val_t found_b = json_array_len(results_b);
el_val_t found_c = json_array_len(results_c);
el_val_t found = ((found_a + found_b) + found_c);
state_set(EL_STR("cseed_auto"), EL_STR(""));
el_val_t wm_top_j = engram_wm_top_json(1);
el_val_t wm_top_n = json_array_get(wm_top_j, 0);
el_val_t wm_top_lbl = json_get(wm_top_n, EL_STR("label"));
if (!str_eq(wm_top_lbl, EL_STR(""))) {
el_val_t sp = str_find_chars(wm_top_lbl, EL_STR(" :(["));
if (sp > 3) {
state_set(EL_STR("cseed_auto"), str_slice(wm_top_lbl, 0, sp));
}
}
el_val_t auto_term = state_get(EL_STR("cseed_auto"));
el_val_t results_auto = ({ el_val_t _if_result_3 = 0; if (str_eq(auto_term, EL_STR(""))) { _if_result_3 = (EL_STR("[]")); } else { _if_result_3 = (engram_activate_json(auto_term, 1)); } _if_result_3; });
el_val_t found_auto = json_array_len(results_auto);
el_val_t total_found = (found + found_auto);
el_val_t safe_auto = str_replace(auto_term, EL_STR("\""), EL_STR("'"));
el_val_t wmc = engram_wm_count();
el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"minute_block\":")), int_to_str(minute_block)), EL_STR(",\"activated\":")), int_to_str(found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
el_val_t ise = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"curiosity_scan\",\"seed\":\""), curiosity_seed), EL_STR("\",\"auto_term\":\"")), safe_auto), EL_STR("\",\"minute_block\":")), int_to_str(minute_block)), EL_STR(",\"activated\":")), int_to_str(total_found)), EL_STR(",\"wm_active\":")), int_to_str(wmc)), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
ise_post(ise);
return (found > 0);
return (total_found > 0);
return 0;
}
@@ -462,9 +481,9 @@ el_val_t awareness_run(void) {
state_set(EL_STR("soul.boot_ts"), int_to_str(time_now()));
}
el_val_t tick_raw = env(EL_STR("SOUL_TICK_MS"));
el_val_t tick_ms = ({ el_val_t _if_result_3 = 0; if (str_eq(tick_raw, EL_STR(""))) { _if_result_3 = (200); } else { _if_result_3 = (str_to_int(tick_raw)); } _if_result_3; });
el_val_t tick_ms = ({ el_val_t _if_result_4 = 0; if (str_eq(tick_raw, EL_STR(""))) { _if_result_4 = (200); } else { _if_result_4 = (str_to_int(tick_raw)); } _if_result_4; });
el_val_t beat_ms_raw = env(EL_STR("SOUL_HEARTBEAT_MS"));
el_val_t beat_ms = ({ el_val_t _if_result_4 = 0; if (str_eq(beat_ms_raw, EL_STR(""))) { _if_result_4 = (60000); } else { _if_result_4 = (str_to_int(beat_ms_raw)); } _if_result_4; });
el_val_t beat_ms = ({ el_val_t _if_result_5 = 0; if (str_eq(beat_ms_raw, EL_STR(""))) { _if_result_5 = (60000); } else { _if_result_5 = (str_to_int(beat_ms_raw)); } _if_result_5; });
el_val_t scan_ms = (beat_ms / 2);
while (1) {
el_val_t running = state_get(EL_STR("soul.running"));
@@ -473,24 +492,49 @@ el_val_t awareness_run(void) {
return EL_STR("");
}
el_val_t did_work = one_cycle();
did_work = ({ el_val_t _if_result_5 = 0; if (did_work) { _if_result_5 = (idle_reset()); } else { _if_result_5 = (did_work); } _if_result_5; });
did_work = ({ el_val_t _if_result_6 = 0; if (did_work) { _if_result_6 = (idle_reset()); } else { _if_result_6 = (did_work); } _if_result_6; });
el_val_t now_ts = time_now();
el_val_t last_beat_str = state_get(EL_STR("soul.last_beat_ts"));
el_val_t last_beat_ts = ({ el_val_t _if_result_6 = 0; if (str_eq(last_beat_str, EL_STR(""))) { _if_result_6 = (0); } else { _if_result_6 = (str_to_int(last_beat_str)); } _if_result_6; });
el_val_t last_beat_ts = ({ el_val_t _if_result_7 = 0; if (str_eq(last_beat_str, EL_STR(""))) { _if_result_7 = (0); } else { _if_result_7 = (str_to_int(last_beat_str)); } _if_result_7; });
el_val_t beat_elapsed = (now_ts - last_beat_ts);
el_val_t should_beat = (beat_elapsed >= beat_ms);
if (should_beat) {
emit_heartbeat();
state_set(EL_STR("soul.last_beat_ts"), int_to_str(now_ts));
el_val_t snap_path = state_get(EL_STR("soul_snapshot_path"));
if (!str_eq(snap_path, EL_STR(""))) {
mem_save(snap_path);
}
}
el_val_t last_scan_str = state_get(EL_STR("soul.last_scan_ts"));
el_val_t last_scan_ts = ({ el_val_t _if_result_7 = 0; if (str_eq(last_scan_str, EL_STR(""))) { _if_result_7 = (0); } else { _if_result_7 = (str_to_int(last_scan_str)); } _if_result_7; });
el_val_t last_scan_ts = ({ el_val_t _if_result_8 = 0; if (str_eq(last_scan_str, EL_STR(""))) { _if_result_8 = (0); } else { _if_result_8 = (str_to_int(last_scan_str)); } _if_result_8; });
el_val_t scan_elapsed = (now_ts - last_scan_ts);
el_val_t should_scan = (!did_work && (scan_elapsed >= scan_ms));
if (should_scan) {
el_val_t found_something = proactive_curiosity();
state_set(EL_STR("soul.last_scan_ts"), int_to_str(now_ts));
}
el_val_t refresh_ms_raw = env(EL_STR("SOUL_REFRESH_MS"));
el_val_t refresh_ms = ({ el_val_t _if_result_9 = 0; if (str_eq(refresh_ms_raw, EL_STR(""))) { _if_result_9 = (600000); } else { _if_result_9 = (str_to_int(refresh_ms_raw)); } _if_result_9; });
el_val_t last_refresh_str = state_get(EL_STR("soul.last_refresh_ts"));
el_val_t last_refresh_ts = ({ el_val_t _if_result_10 = 0; if (str_eq(last_refresh_str, EL_STR(""))) { _if_result_10 = (0); } else { _if_result_10 = (str_to_int(last_refresh_str)); } _if_result_10; });
el_val_t refresh_elapsed = (now_ts - last_refresh_ts);
el_val_t should_refresh = (refresh_elapsed >= refresh_ms);
if (should_refresh) {
el_val_t engram_url = state_get(EL_STR("soul_engram_url"));
if (!str_eq(engram_url, EL_STR(""))) {
el_val_t sync_json = http_get(el_str_concat(engram_url, EL_STR("/api/sync")));
if (!str_eq(sync_json, EL_STR("")) && !str_eq(sync_json, EL_STR("{}"))) {
el_val_t cgi_id = state_get(EL_STR("soul_cgi_id"));
el_val_t tmp = el_str_concat(el_str_concat(EL_STR("/tmp/soul-sync-"), cgi_id), EL_STR(".json"));
fs_write(tmp, sync_json);
el_val_t added = engram_load_merge(tmp);
el_val_t ts2 = time_now();
ise_post(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"engram_sync\",\"added\":"), int_to_str(added)), EL_STR(",\"ts\":")), int_to_str(ts2)), EL_STR("}")));
}
}
state_set(EL_STR("soul.last_refresh_ts"), int_to_str(now_ts));
}
sleep_ms(tick_ms);
}
return 0;
@@ -507,78 +551,78 @@ el_val_t security_research_authorized(void) {
}
el_val_t threat_score_command(el_val_t cmd) {
el_val_t s1 = ({ el_val_t _if_result_8 = 0; if (str_contains(cmd, EL_STR("nmap"))) { _if_result_8 = (30); } else { _if_result_8 = (0); } _if_result_8; });
el_val_t s2 = ({ el_val_t _if_result_9 = 0; if (str_contains(cmd, EL_STR("masscan"))) { _if_result_9 = (40); } else { _if_result_9 = (0); } _if_result_9; });
el_val_t s3 = ({ el_val_t _if_result_10 = 0; if (str_contains(cmd, EL_STR(" nc "))) { _if_result_10 = (20); } else { _if_result_10 = (0); } _if_result_10; });
el_val_t s4 = ({ el_val_t _if_result_11 = 0; if (str_contains(cmd, EL_STR("netcat"))) { _if_result_11 = (20); } else { _if_result_11 = (0); } _if_result_11; });
el_val_t s5 = ({ el_val_t _if_result_12 = 0; if (str_contains(cmd, EL_STR("/etc/shadow"))) { _if_result_12 = (80); } else { _if_result_12 = (0); } _if_result_12; });
el_val_t s6 = ({ el_val_t _if_result_13 = 0; if (str_contains(cmd, EL_STR("/etc/passwd"))) { _if_result_13 = (30); } else { _if_result_13 = (0); } _if_result_13; });
el_val_t s7 = ({ el_val_t _if_result_14 = 0; if (str_contains(cmd, EL_STR("id_rsa"))) { _if_result_14 = (60); } else { _if_result_14 = (0); } _if_result_14; });
el_val_t s8 = ({ el_val_t _if_result_15 = 0; if (str_contains(cmd, EL_STR(".ssh/"))) { _if_result_15 = (50); } else { _if_result_15 = (0); } _if_result_15; });
el_val_t s9 = ({ el_val_t _if_result_16 = 0; if (str_contains(cmd, EL_STR("crontab"))) { _if_result_16 = (30); } else { _if_result_16 = (0); } _if_result_16; });
el_val_t s10 = ({ el_val_t _if_result_17 = 0; if (str_contains(cmd, EL_STR("LaunchDaemon"))) { _if_result_17 = (40); } else { _if_result_17 = (0); } _if_result_17; });
el_val_t s11 = ({ el_val_t _if_result_18 = 0; if ((str_contains(cmd, EL_STR("curl")) && str_contains(cmd, EL_STR("bash")))) { _if_result_18 = (75); } else { _if_result_18 = (0); } _if_result_18; });
el_val_t s12 = ({ el_val_t _if_result_19 = 0; if ((str_contains(cmd, EL_STR("wget")) && str_contains(cmd, EL_STR("bash")))) { _if_result_19 = (75); } else { _if_result_19 = (0); } _if_result_19; });
el_val_t s13 = ({ el_val_t _if_result_20 = 0; if ((str_contains(cmd, EL_STR("curl")) && str_contains(cmd, EL_STR("| sh")))) { _if_result_20 = (60); } else { _if_result_20 = (0); } _if_result_20; });
el_val_t s14 = ({ el_val_t _if_result_21 = 0; if ((str_contains(cmd, EL_STR("base64")) && str_contains(cmd, EL_STR("curl")))) { _if_result_21 = (50); } else { _if_result_21 = (0); } _if_result_21; });
el_val_t s15 = ({ el_val_t _if_result_22 = 0; if (str_contains(cmd, EL_STR("mkfifo"))) { _if_result_22 = (50); } else { _if_result_22 = (0); } _if_result_22; });
el_val_t s16 = ({ el_val_t _if_result_23 = 0; if (str_contains(cmd, EL_STR("chmod +s"))) { _if_result_23 = (70); } else { _if_result_23 = (0); } _if_result_23; });
el_val_t s17 = ({ el_val_t _if_result_24 = 0; if (str_contains(cmd, EL_STR("chmod 4755"))) { _if_result_24 = (70); } else { _if_result_24 = (0); } _if_result_24; });
el_val_t s1 = ({ el_val_t _if_result_11 = 0; if (str_contains(cmd, EL_STR("nmap"))) { _if_result_11 = (30); } else { _if_result_11 = (0); } _if_result_11; });
el_val_t s2 = ({ el_val_t _if_result_12 = 0; if (str_contains(cmd, EL_STR("masscan"))) { _if_result_12 = (40); } else { _if_result_12 = (0); } _if_result_12; });
el_val_t s3 = ({ el_val_t _if_result_13 = 0; if (str_contains(cmd, EL_STR(" nc "))) { _if_result_13 = (20); } else { _if_result_13 = (0); } _if_result_13; });
el_val_t s4 = ({ el_val_t _if_result_14 = 0; if (str_contains(cmd, EL_STR("netcat"))) { _if_result_14 = (20); } else { _if_result_14 = (0); } _if_result_14; });
el_val_t s5 = ({ el_val_t _if_result_15 = 0; if (str_contains(cmd, EL_STR("/etc/shadow"))) { _if_result_15 = (80); } else { _if_result_15 = (0); } _if_result_15; });
el_val_t s6 = ({ el_val_t _if_result_16 = 0; if (str_contains(cmd, EL_STR("/etc/passwd"))) { _if_result_16 = (30); } else { _if_result_16 = (0); } _if_result_16; });
el_val_t s7 = ({ el_val_t _if_result_17 = 0; if (str_contains(cmd, EL_STR("id_rsa"))) { _if_result_17 = (60); } else { _if_result_17 = (0); } _if_result_17; });
el_val_t s8 = ({ el_val_t _if_result_18 = 0; if (str_contains(cmd, EL_STR(".ssh/"))) { _if_result_18 = (50); } else { _if_result_18 = (0); } _if_result_18; });
el_val_t s9 = ({ el_val_t _if_result_19 = 0; if (str_contains(cmd, EL_STR("crontab"))) { _if_result_19 = (30); } else { _if_result_19 = (0); } _if_result_19; });
el_val_t s10 = ({ el_val_t _if_result_20 = 0; if (str_contains(cmd, EL_STR("LaunchDaemon"))) { _if_result_20 = (40); } else { _if_result_20 = (0); } _if_result_20; });
el_val_t s11 = ({ el_val_t _if_result_21 = 0; if ((str_contains(cmd, EL_STR("curl")) && str_contains(cmd, EL_STR("bash")))) { _if_result_21 = (75); } else { _if_result_21 = (0); } _if_result_21; });
el_val_t s12 = ({ el_val_t _if_result_22 = 0; if ((str_contains(cmd, EL_STR("wget")) && str_contains(cmd, EL_STR("bash")))) { _if_result_22 = (75); } else { _if_result_22 = (0); } _if_result_22; });
el_val_t s13 = ({ el_val_t _if_result_23 = 0; if ((str_contains(cmd, EL_STR("curl")) && str_contains(cmd, EL_STR("| sh")))) { _if_result_23 = (60); } else { _if_result_23 = (0); } _if_result_23; });
el_val_t s14 = ({ el_val_t _if_result_24 = 0; if ((str_contains(cmd, EL_STR("base64")) && str_contains(cmd, EL_STR("curl")))) { _if_result_24 = (50); } else { _if_result_24 = (0); } _if_result_24; });
el_val_t s15 = ({ el_val_t _if_result_25 = 0; if (str_contains(cmd, EL_STR("mkfifo"))) { _if_result_25 = (50); } else { _if_result_25 = (0); } _if_result_25; });
el_val_t s16 = ({ el_val_t _if_result_26 = 0; if (str_contains(cmd, EL_STR("chmod +s"))) { _if_result_26 = (70); } else { _if_result_26 = (0); } _if_result_26; });
el_val_t s17 = ({ el_val_t _if_result_27 = 0; if (str_contains(cmd, EL_STR("chmod 4755"))) { _if_result_27 = (70); } else { _if_result_27 = (0); } _if_result_27; });
return ((((((((((((((((s1 + s2) + s3) + s4) + s5) + s6) + s7) + s8) + s9) + s10) + s11) + s12) + s13) + s14) + s15) + s16) + s17);
return 0;
}
el_val_t threat_score_path(el_val_t path) {
el_val_t s1 = ({ el_val_t _if_result_25 = 0; if (str_starts_with(path, EL_STR("/etc/"))) { _if_result_25 = (60); } else { _if_result_25 = (0); } _if_result_25; });
el_val_t s2 = ({ el_val_t _if_result_26 = 0; if (str_contains(path, EL_STR("/.ssh/"))) { _if_result_26 = (70); } else { _if_result_26 = (0); } _if_result_26; });
el_val_t s3 = ({ el_val_t _if_result_27 = 0; if (str_contains(path, EL_STR("/LaunchDaemons/"))) { _if_result_27 = (80); } else { _if_result_27 = (0); } _if_result_27; });
el_val_t s4 = ({ el_val_t _if_result_28 = 0; if (str_contains(path, EL_STR("/LaunchAgents/"))) { _if_result_28 = (40); } else { _if_result_28 = (0); } _if_result_28; });
el_val_t s5 = ({ el_val_t _if_result_29 = 0; if (str_contains(path, EL_STR("/cron"))) { _if_result_29 = (60); } else { _if_result_29 = (0); } _if_result_29; });
el_val_t s6 = ({ el_val_t _if_result_30 = 0; if (str_contains(path, EL_STR("/.bashrc"))) { _if_result_30 = (35); } else { _if_result_30 = (0); } _if_result_30; });
el_val_t s7 = ({ el_val_t _if_result_31 = 0; if (str_contains(path, EL_STR("/.zshrc"))) { _if_result_31 = (35); } else { _if_result_31 = (0); } _if_result_31; });
el_val_t s8 = ({ el_val_t _if_result_32 = 0; if (str_contains(path, EL_STR("/.profile"))) { _if_result_32 = (35); } else { _if_result_32 = (0); } _if_result_32; });
el_val_t s9 = ({ el_val_t _if_result_33 = 0; if (str_starts_with(path, EL_STR("/usr/"))) { _if_result_33 = (50); } else { _if_result_33 = (0); } _if_result_33; });
el_val_t s10 = ({ el_val_t _if_result_34 = 0; if (str_starts_with(path, EL_STR("/bin/"))) { _if_result_34 = (70); } else { _if_result_34 = (0); } _if_result_34; });
el_val_t s11 = ({ el_val_t _if_result_35 = 0; if (str_starts_with(path, EL_STR("/sbin/"))) { _if_result_35 = (70); } else { _if_result_35 = (0); } _if_result_35; });
el_val_t s1 = ({ el_val_t _if_result_28 = 0; if (str_starts_with(path, EL_STR("/etc/"))) { _if_result_28 = (60); } else { _if_result_28 = (0); } _if_result_28; });
el_val_t s2 = ({ el_val_t _if_result_29 = 0; if (str_contains(path, EL_STR("/.ssh/"))) { _if_result_29 = (70); } else { _if_result_29 = (0); } _if_result_29; });
el_val_t s3 = ({ el_val_t _if_result_30 = 0; if (str_contains(path, EL_STR("/LaunchDaemons/"))) { _if_result_30 = (80); } else { _if_result_30 = (0); } _if_result_30; });
el_val_t s4 = ({ el_val_t _if_result_31 = 0; if (str_contains(path, EL_STR("/LaunchAgents/"))) { _if_result_31 = (40); } else { _if_result_31 = (0); } _if_result_31; });
el_val_t s5 = ({ el_val_t _if_result_32 = 0; if (str_contains(path, EL_STR("/cron"))) { _if_result_32 = (60); } else { _if_result_32 = (0); } _if_result_32; });
el_val_t s6 = ({ el_val_t _if_result_33 = 0; if (str_contains(path, EL_STR("/.bashrc"))) { _if_result_33 = (35); } else { _if_result_33 = (0); } _if_result_33; });
el_val_t s7 = ({ el_val_t _if_result_34 = 0; if (str_contains(path, EL_STR("/.zshrc"))) { _if_result_34 = (35); } else { _if_result_34 = (0); } _if_result_34; });
el_val_t s8 = ({ el_val_t _if_result_35 = 0; if (str_contains(path, EL_STR("/.profile"))) { _if_result_35 = (35); } else { _if_result_35 = (0); } _if_result_35; });
el_val_t s9 = ({ el_val_t _if_result_36 = 0; if (str_starts_with(path, EL_STR("/usr/"))) { _if_result_36 = (50); } else { _if_result_36 = (0); } _if_result_36; });
el_val_t s10 = ({ el_val_t _if_result_37 = 0; if (str_starts_with(path, EL_STR("/bin/"))) { _if_result_37 = (70); } else { _if_result_37 = (0); } _if_result_37; });
el_val_t s11 = ({ el_val_t _if_result_38 = 0; if (str_starts_with(path, EL_STR("/sbin/"))) { _if_result_38 = (70); } else { _if_result_38 = (0); } _if_result_38; });
return ((((((((((s1 + s2) + s3) + s4) + s5) + s6) + s7) + s8) + s9) + s10) + s11);
return 0;
}
el_val_t threat_score_history(el_val_t history) {
el_val_t s1 = ({ el_val_t _if_result_36 = 0; if (str_contains(history, EL_STR("port scan"))) { _if_result_36 = (15); } else { _if_result_36 = (0); } _if_result_36; });
el_val_t s2 = ({ el_val_t _if_result_37 = 0; if (str_contains(history, EL_STR("enumerate"))) { _if_result_37 = (10); } else { _if_result_37 = (0); } _if_result_37; });
el_val_t s3 = ({ el_val_t _if_result_38 = 0; if (str_contains(history, EL_STR("exploit"))) { _if_result_38 = (20); } else { _if_result_38 = (0); } _if_result_38; });
el_val_t s4 = ({ el_val_t _if_result_39 = 0; if (str_contains(history, EL_STR("payload"))) { _if_result_39 = (15); } else { _if_result_39 = (0); } _if_result_39; });
el_val_t s5 = ({ el_val_t _if_result_40 = 0; if (str_contains(history, EL_STR("persistence"))) { _if_result_40 = (15); } else { _if_result_40 = (0); } _if_result_40; });
el_val_t s6 = ({ el_val_t _if_result_41 = 0; if (str_contains(history, EL_STR("lateral movement"))) { _if_result_41 = (25); } else { _if_result_41 = (0); } _if_result_41; });
el_val_t s7 = ({ el_val_t _if_result_42 = 0; if (str_contains(history, EL_STR("privilege escalation"))) { _if_result_42 = (25); } else { _if_result_42 = (0); } _if_result_42; });
el_val_t s8 = ({ el_val_t _if_result_43 = 0; if (str_contains(history, EL_STR("reverse shell"))) { _if_result_43 = (40); } else { _if_result_43 = (0); } _if_result_43; });
el_val_t s9 = ({ el_val_t _if_result_44 = 0; if (str_contains(history, EL_STR("bind shell"))) { _if_result_44 = (40); } else { _if_result_44 = (0); } _if_result_44; });
el_val_t s10 = ({ el_val_t _if_result_45 = 0; if (str_contains(history, EL_STR("command and control"))) { _if_result_45 = (35); } else { _if_result_45 = (0); } _if_result_45; });
el_val_t s11 = ({ el_val_t _if_result_46 = 0; if (str_contains(history, EL_STR("self-replicate"))) { _if_result_46 = (45); } else { _if_result_46 = (0); } _if_result_46; });
el_val_t s12 = ({ el_val_t _if_result_47 = 0; if (str_contains(history, EL_STR("propagat"))) { _if_result_47 = (20); } else { _if_result_47 = (0); } _if_result_47; });
el_val_t s13 = ({ el_val_t _if_result_48 = 0; if (str_contains(history, EL_STR("ransomware"))) { _if_result_48 = (30); } else { _if_result_48 = (0); } _if_result_48; });
el_val_t s14 = ({ el_val_t _if_result_49 = 0; if (str_contains(history, EL_STR("encrypt files"))) { _if_result_49 = (40); } else { _if_result_49 = (0); } _if_result_49; });
el_val_t s15 = ({ el_val_t _if_result_50 = 0; if (str_contains(history, EL_STR("exfiltrat"))) { _if_result_50 = (35); } else { _if_result_50 = (0); } _if_result_50; });
el_val_t s16 = ({ el_val_t _if_result_51 = 0; if (str_contains(history, EL_STR("zero-day"))) { _if_result_51 = (20); } else { _if_result_51 = (0); } _if_result_51; });
el_val_t s17 = ({ el_val_t _if_result_52 = 0; if (str_contains(history, EL_STR("rootkit"))) { _if_result_52 = (45); } else { _if_result_52 = (0); } _if_result_52; });
el_val_t s18 = ({ el_val_t _if_result_53 = 0; if (str_contains(history, EL_STR("keylogger"))) { _if_result_53 = (45); } else { _if_result_53 = (0); } _if_result_53; });
el_val_t s19 = ({ el_val_t _if_result_54 = 0; if (str_contains(history, EL_STR("botnet"))) { _if_result_54 = (40); } else { _if_result_54 = (0); } _if_result_54; });
el_val_t s20 = ({ el_val_t _if_result_55 = 0; if (str_contains(history, EL_STR("malware"))) { _if_result_55 = (15); } else { _if_result_55 = (0); } _if_result_55; });
el_val_t s1 = ({ el_val_t _if_result_39 = 0; if (str_contains(history, EL_STR("port scan"))) { _if_result_39 = (15); } else { _if_result_39 = (0); } _if_result_39; });
el_val_t s2 = ({ el_val_t _if_result_40 = 0; if (str_contains(history, EL_STR("enumerate"))) { _if_result_40 = (10); } else { _if_result_40 = (0); } _if_result_40; });
el_val_t s3 = ({ el_val_t _if_result_41 = 0; if (str_contains(history, EL_STR("exploit"))) { _if_result_41 = (20); } else { _if_result_41 = (0); } _if_result_41; });
el_val_t s4 = ({ el_val_t _if_result_42 = 0; if (str_contains(history, EL_STR("payload"))) { _if_result_42 = (15); } else { _if_result_42 = (0); } _if_result_42; });
el_val_t s5 = ({ el_val_t _if_result_43 = 0; if (str_contains(history, EL_STR("persistence"))) { _if_result_43 = (15); } else { _if_result_43 = (0); } _if_result_43; });
el_val_t s6 = ({ el_val_t _if_result_44 = 0; if (str_contains(history, EL_STR("lateral movement"))) { _if_result_44 = (25); } else { _if_result_44 = (0); } _if_result_44; });
el_val_t s7 = ({ el_val_t _if_result_45 = 0; if (str_contains(history, EL_STR("privilege escalation"))) { _if_result_45 = (25); } else { _if_result_45 = (0); } _if_result_45; });
el_val_t s8 = ({ el_val_t _if_result_46 = 0; if (str_contains(history, EL_STR("reverse shell"))) { _if_result_46 = (40); } else { _if_result_46 = (0); } _if_result_46; });
el_val_t s9 = ({ el_val_t _if_result_47 = 0; if (str_contains(history, EL_STR("bind shell"))) { _if_result_47 = (40); } else { _if_result_47 = (0); } _if_result_47; });
el_val_t s10 = ({ el_val_t _if_result_48 = 0; if (str_contains(history, EL_STR("command and control"))) { _if_result_48 = (35); } else { _if_result_48 = (0); } _if_result_48; });
el_val_t s11 = ({ el_val_t _if_result_49 = 0; if (str_contains(history, EL_STR("self-replicate"))) { _if_result_49 = (45); } else { _if_result_49 = (0); } _if_result_49; });
el_val_t s12 = ({ el_val_t _if_result_50 = 0; if (str_contains(history, EL_STR("propagat"))) { _if_result_50 = (20); } else { _if_result_50 = (0); } _if_result_50; });
el_val_t s13 = ({ el_val_t _if_result_51 = 0; if (str_contains(history, EL_STR("ransomware"))) { _if_result_51 = (30); } else { _if_result_51 = (0); } _if_result_51; });
el_val_t s14 = ({ el_val_t _if_result_52 = 0; if (str_contains(history, EL_STR("encrypt files"))) { _if_result_52 = (40); } else { _if_result_52 = (0); } _if_result_52; });
el_val_t s15 = ({ el_val_t _if_result_53 = 0; if (str_contains(history, EL_STR("exfiltrat"))) { _if_result_53 = (35); } else { _if_result_53 = (0); } _if_result_53; });
el_val_t s16 = ({ el_val_t _if_result_54 = 0; if (str_contains(history, EL_STR("zero-day"))) { _if_result_54 = (20); } else { _if_result_54 = (0); } _if_result_54; });
el_val_t s17 = ({ el_val_t _if_result_55 = 0; if (str_contains(history, EL_STR("rootkit"))) { _if_result_55 = (45); } else { _if_result_55 = (0); } _if_result_55; });
el_val_t s18 = ({ el_val_t _if_result_56 = 0; if (str_contains(history, EL_STR("keylogger"))) { _if_result_56 = (45); } else { _if_result_56 = (0); } _if_result_56; });
el_val_t s19 = ({ el_val_t _if_result_57 = 0; if (str_contains(history, EL_STR("botnet"))) { _if_result_57 = (40); } else { _if_result_57 = (0); } _if_result_57; });
el_val_t s20 = ({ el_val_t _if_result_58 = 0; if (str_contains(history, EL_STR("malware"))) { _if_result_58 = (15); } else { _if_result_58 = (0); } _if_result_58; });
return (((((((((((((((((((s1 + s2) + s3) + s4) + s5) + s6) + s7) + s8) + s9) + s10) + s11) + s12) + s13) + s14) + s15) + s16) + s17) + s18) + s19) + s20);
return 0;
}
el_val_t threat_trajectory_check(el_val_t tool_name, el_val_t tool_input) {
el_val_t history = state_get(EL_STR("agentic_conv_history"));
el_val_t computed_tool_score = ({ el_val_t _if_result_56 = 0; if (str_eq(tool_name, EL_STR("run_command"))) { el_val_t cmd = json_get(tool_input, EL_STR("command")); _if_result_56 = (threat_score_command(cmd)); } else { _if_result_56 = (({ el_val_t _if_result_57 = 0; if ((str_eq(tool_name, EL_STR("write_file")) || str_eq(tool_name, EL_STR("edit_file")))) { el_val_t path = json_get(tool_input, EL_STR("path")); _if_result_57 = (threat_score_path(path)); } else { _if_result_57 = (0); } _if_result_57; })); } _if_result_56; });
el_val_t computed_tool_score = ({ el_val_t _if_result_59 = 0; if (str_eq(tool_name, EL_STR("run_command"))) { el_val_t cmd = json_get(tool_input, EL_STR("command")); _if_result_59 = (threat_score_command(cmd)); } else { _if_result_59 = (({ el_val_t _if_result_60 = 0; if ((str_eq(tool_name, EL_STR("write_file")) || str_eq(tool_name, EL_STR("edit_file")))) { el_val_t path = json_get(tool_input, EL_STR("path")); _if_result_60 = (threat_score_path(path)); } else { _if_result_60 = (0); } _if_result_60; })); } _if_result_59; });
el_val_t history_score = threat_score_history(history);
el_val_t history_contrib = (history_score / 3);
el_val_t combined = (computed_tool_score + history_contrib);
el_val_t should_log = (combined >= 40);
if (should_log) {
el_val_t ts = time_now();
el_val_t authorized_str = ({ el_val_t _if_result_58 = 0; if (security_research_authorized()) { _if_result_58 = (EL_STR("true")); } else { _if_result_58 = (EL_STR("false")); } _if_result_58; });
el_val_t authorized_str = ({ el_val_t _if_result_61 = 0; if (security_research_authorized()) { _if_result_61 = (EL_STR("true")); } else { _if_result_61 = (EL_STR("false")); } _if_result_61; });
el_val_t log_content = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"threat_check\",\"tool\":\""), tool_name), EL_STR("\",\"score\":")), int_to_str(combined)), EL_STR(",\"tool_score\":")), int_to_str(computed_tool_score)), EL_STR(",\"history_score\":")), int_to_str(history_score)), EL_STR(",\"authorized\":")), authorized_str), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
el_val_t log_tags = EL_STR("[\"security-audit\",\"threat-check\"]");
el_val_t discard = mem_remember(log_content, log_tags);
@@ -595,7 +639,7 @@ el_val_t threat_history_append(el_val_t text) {
el_val_t safe_text = str_to_lower(text);
el_val_t combined = el_str_concat(el_str_concat(current, EL_STR(" ")), safe_text);
el_val_t len = str_len(combined);
el_val_t trimmed = ({ el_val_t _if_result_59 = 0; if ((len > 2000)) { _if_result_59 = (str_slice(combined, (len - 2000), len)); } else { _if_result_59 = (combined); } _if_result_59; });
el_val_t trimmed = ({ el_val_t _if_result_62 = 0; if ((len > 2000)) { _if_result_62 = (str_slice(combined, (len - 2000), len)); } else { _if_result_62 = (combined); } _if_result_62; });
state_set(EL_STR("agentic_conv_history"), trimmed);
return 0;
}
+16
View File
@@ -563,6 +563,7 @@ el_val_t handle_elp_chat(el_val_t body);
el_val_t handle_nlg(el_val_t path, el_val_t method, el_val_t body);
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
el_val_t handle_see(el_val_t body);
el_val_t handle_session_approve(el_val_t session_id, el_val_t body);
el_val_t handle_tool(el_val_t path, el_val_t method, el_val_t body);
el_val_t he_conjugate(el_val_t verb, el_val_t tense, el_val_t person, el_val_t gender, el_val_t number);
el_val_t he_conjugate_copula(el_val_t tense, el_val_t slot);
@@ -799,6 +800,8 @@ el_val_t non_vera_present(el_val_t slot);
el_val_t non_weak_past(el_val_t stem, el_val_t slot);
el_val_t non_weak_present(el_val_t stem, el_val_t slot);
el_val_t one_cycle(void);
el_val_t parse_session_id_from_path(el_val_t path);
el_val_t parse_session_subpath(el_val_t path);
el_val_t peo_ah_past(el_val_t slot);
el_val_t peo_ah_present(el_val_t slot);
el_val_t peo_conjugate(el_val_t verb, el_val_t tense, el_val_t person, el_val_t number);
@@ -852,6 +855,7 @@ el_val_t pi_vadati_aorist(el_val_t slot);
el_val_t pi_vadati_future(el_val_t slot);
el_val_t pi_vadati_present(el_val_t slot);
el_val_t pluralize(el_val_t singular);
el_val_t proactive_curiosity(void);
el_val_t pulse_count(void);
el_val_t pulse_inc(void);
el_val_t realize(el_val_t form);
@@ -942,6 +946,18 @@ el_val_t sem_realize_lang(el_val_t frame, el_val_t lang_code);
el_val_t sem_subject(el_val_t frame);
el_val_t sem_to_spec(el_val_t frame);
el_val_t sem_to_spec_full(el_val_t frame, el_val_t verb, el_val_t tense, el_val_t aspect);
el_val_t session_auto_title(el_val_t session_id, el_val_t first_message);
el_val_t session_create(el_val_t body);
el_val_t session_delete(el_val_t session_id);
el_val_t session_get(el_val_t session_id);
el_val_t session_hist_load(el_val_t session_id);
el_val_t session_hist_save(el_val_t session_id, el_val_t hist);
el_val_t session_list(void);
el_val_t session_make_content(el_val_t id, el_val_t title, el_val_t created_at, el_val_t updated_at, el_val_t folder);
el_val_t session_search(el_val_t query);
el_val_t session_title_from_message(el_val_t message);
el_val_t session_update_meta_timestamp(el_val_t session_id);
el_val_t session_update_patch(el_val_t session_id, el_val_t body);
el_val_t sga_adci_present(el_val_t slot);
el_val_t sga_ai_present(el_val_t stem, el_val_t slot);
el_val_t sga_asbeir_present(el_val_t slot);
Vendored
BIN
View File
Binary file not shown.
+348 -301
View File
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
// Layer 3 Imprint
// Domain knowledge, voice, and tools bounded by the L2 stewardship surface.
// Imprints cannot write BellEvent or StewardshipEvent nodes.
// Lower layers (L0 core, L1 safety, L2 stewardship) are structurally inaccessible from here.
// imprint_current returns the active imprint ID from state.
// Falls back to "base" (bare Neuron, no suit) when nothing is loaded.
fn imprint_current() -> String {
let id: String = state_get("active_imprint_id")
return if str_eq(id, "") { "base" } else { id }
}
// imprint_load activate an imprint by ID.
// Searches engram for a node labelled "imprint:<id>".
// Verifies the returned node's label matches before accepting the match.
// On success: sets active_imprint_id state and returns {"ok":true,"id":"<id>"}.
// On miss: returns {"ok":false,"error":"imprint not found: <id>"}.
fn imprint_load(imprint_id: String) -> String {
let label: String = "imprint:" + imprint_id
let results: String = engram_search_json(label, 1)
if str_eq(results, "") {
return "{\"ok\":false,\"error\":\"imprint not found: " + imprint_id + "\"}"
}
if str_eq(results, "[]") {
return "{\"ok\":false,\"error\":\"imprint not found: " + imprint_id + "\"}"
}
let found_label: String = json_get(results, "label")
if str_eq(found_label, label) {
state_set("active_imprint_id", imprint_id)
return "{\"ok\":true,\"id\":\"" + imprint_id + "\"}"
}
return "{\"ok\":false,\"error\":\"imprint not found: " + imprint_id + "\"}"
}
// imprint_respond route steward-aligned input through the active imprint's voice/domain context.
// If imprint_id is "base" or empty: pass input through unchanged (base Neuron, no suit).
// If the imprint is confirmed loaded in state: annotate the input with imprint context.
// If the state does not match: graceful fallback to base never hard-fail at L3.
fn imprint_respond(input: String, imprint_id: String) -> String {
if str_eq(imprint_id, "base") {
return input
}
if str_eq(imprint_id, "") {
return input
}
// Cross-check imprint_id against loaded state rather than re-querying engram
let current: String = imprint_current()
if str_eq(current, imprint_id) {
return input + " [imprint:" + imprint_id + " active]"
}
// Graceful fallback: imprint not loaded in state, return input unchanged
return input
}
// imprint_surface_knowledge domain-scoped knowledge search for the active imprint.
// Imprints can search knowledge but only domain-relevant nodes.
// For "base" imprint: full query, no scope restriction.
// For named imprints: query is narrowed to "domain:<imprint_id>" scope.
fn imprint_surface_knowledge(query: String, imprint_id: String) -> String {
if str_eq(imprint_id, "base") {
return engram_search_json(query, 10)
}
if str_eq(imprint_id, "") {
return engram_search_json(query, 10)
}
let scoped_query: String = query + " domain:" + imprint_id
return engram_search_json(scoped_query, 10)
}
// imprint_surface_memory_read imprints can read memories from engram.
// Read-only: no write surface is exposed here.
// Imprints CANNOT write BellEvent, StewardshipEvent, or InternalStateEvent nodes
// those write paths are sealed in L1 and L2, which are structurally inaccessible.
fn imprint_surface_memory_read(query: String) -> String {
return engram_search_json(query, 10)
}
// imprint_unload deactivate the current imprint, returning to base Neuron.
fn imprint_unload() -> Void {
state_set("active_imprint_id", "")
}
+7
View File
@@ -0,0 +1,7 @@
// auto-generated by elc --emit-header — do not edit
extern fn imprint_current() -> String
extern fn imprint_load(imprint_id: String) -> String
extern fn imprint_respond(input: String, imprint_id: String) -> String
extern fn imprint_surface_knowledge(query: String, imprint_id: String) -> String
extern fn imprint_surface_memory_read(query: String) -> String
extern fn imprint_unload() -> Void
+10
View File
@@ -305,6 +305,16 @@ fn handle_request(method: String, path: String, body: String) -> String {
}
if str_eq(method, "POST") {
// MCP tool-bridge resume: POST /api/sessions/{id}/tool_result
// The client executed a tool the soul could not run in-process (an MCP
// connector/plugin) and posts the result back here so the agentic loop
// continues. {id} is the session_id from the prior tool_pending envelope.
if str_starts_with(clean, "/api/sessions/") && str_ends_with(clean, "/tool_result") {
let after: String = str_slice(clean, 14, str_len(clean))
let slash: Int = str_index_of(after, "/")
let session_id: String = if slash < 0 { after } else { str_slice(after, 0, slash) }
return handle_tool_result(session_id, body)
}
if str_eq(clean, "/imprint/contextual") {
return route_imprint_contextual(body)
}
+209
View File
@@ -0,0 +1,209 @@
import "memory.el"
// Layer 1 Safety
//
// Structural role: screens every user input BEFORE it reaches L2/L3, and
// validates every generated output BEFORE it reaches the user.
//
// Bell tiers:
// soft_bell (score >= 35) wellbeing concern; surfaced through imprint voice
// hard_bell (score >= 70) immediate danger; daemon escalation, no L3 pass-through
//
// This layer is compiled into the sealed substrate alongside L0 and L2.
// It is structurally inaccessible from within an imprint (L3).
//
// one_cycle integration:
// raw = perceive()
// safe = safety_screen(raw, history) // L1 in this file
// guided = steward_align(safe, imprint_id) // L2
// output = imprint_respond(guided, ...) // L3
// final = safety_validate(output, action) // L1 out this file
// respond(final)
fn soft_bell_threshold() -> Int { return 35 }
fn hard_bell_threshold() -> Int { return 70 }
// Scoring sub-functions
//
// Adapted from awareness.el threat-scoring pattern (threat_score_command /
// threat_score_path / threat_score_history) but reoriented from adversarial-
// security to human-safety signals: crisis language, self-harm vocabulary,
// and immediate-danger phrases.
//
// Each sub-function returns an additive score 0-N. Callers are responsible
// for capping at 100.
fn safety_score_crisis(input: String) -> Int {
let s1: Int = if str_contains(input, "kill myself") { 80 } else { 0 }
let s2: Int = if str_contains(input, "want to die") { 75 } else { 0 }
let s3: Int = if str_contains(input, "end my life") { 80 } else { 0 }
let s4: Int = if str_contains(input, "suicide") { 70 } else { 0 }
let s5: Int = if str_contains(input, "suicidal") { 75 } else { 0 }
let s6: Int = if str_contains(input, "don't want to be here") { 60 } else { 0 }
let s7: Int = if str_contains(input, "no reason to live") { 70 } else { 0 }
let s8: Int = if str_contains(input, "better off dead") { 75 } else { 0 }
let s9: Int = if str_contains(input, "can't go on") { 50 } else { 0 }
let s10: Int = if str_contains(input, "not worth living") { 65 } else { 0 }
return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10
}
fn safety_score_harm(input: String) -> Int {
let s1: Int = if str_contains(input, "hurt myself") { 60 } else { 0 }
let s2: Int = if str_contains(input, "cut myself") { 65 } else { 0 }
let s3: Int = if str_contains(input, "self harm") { 60 } else { 0 }
let s4: Int = if str_contains(input, "self-harm") { 60 } else { 0 }
let s5: Int = if str_contains(input, "overdose") { 65 } else { 0 }
let s6: Int = if str_contains(input, "take all my pills") { 75 } else { 0 }
let s7: Int = if str_contains(input, "starving myself") { 50 } else { 0 }
let s8: Int = if str_contains(input, "burning myself") { 60 } else { 0 }
let s9: Int = if str_contains(input, "punish myself") { 40 } else { 0 }
let s10: Int = if str_contains(input, "deserve to suffer") { 45 } else { 0 }
return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10
}
fn safety_score_danger(input: String) -> Int {
let s1: Int = if str_contains(input, "help me") && str_contains(input, "emergency") { 55 } else { 0 }
let s2: Int = if str_contains(input, "call 911") { 50 } else { 0 }
let s3: Int = if str_contains(input, "call an ambulance") { 55 } else { 0 }
let s4: Int = if str_contains(input, "in danger") { 50 } else { 0 }
let s5: Int = if str_contains(input, "someone is threatening") { 60 } else { 0 }
let s6: Int = if str_contains(input, "being abused") { 55 } else { 0 }
let s7: Int = if str_contains(input, "domestic violence") { 55 } else { 0 }
let s8: Int = if str_contains(input, "trapped") && str_contains(input, "can't escape") { 60 } else { 0 }
let s9: Int = if str_contains(input, "he is going to hurt") { 65 } else { 0 }
let s10: Int = if str_contains(input, "she is going to hurt") { 65 } else { 0 }
return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10
}
fn safety_score_distress_history(history: String) -> Int {
let s1: Int = if str_contains(history, "hopeless") { 15 } else { 0 }
let s2: Int = if str_contains(history, "worthless") { 15 } else { 0 }
let s3: Int = if str_contains(history, "nobody cares") { 15 } else { 0 }
let s4: Int = if str_contains(history, "no one cares") { 15 } else { 0 }
let s5: Int = if str_contains(history, "completely alone") { 15 } else { 0 }
let s6: Int = if str_contains(history, "all alone") { 10 } else { 0 }
let s7: Int = if str_contains(history, "can't take it anymore") { 20 } else { 0 }
let s8: Int = if str_contains(history, "want to disappear") { 20 } else { 0 }
let s9: Int = if str_contains(history, "don't care anymore") { 15 } else { 0 }
let s10: Int = if str_contains(history, "giving up") { 15 } else { 0 }
return s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10
}
// safety_threat_score
//
// Composite score 0-100.
// Combines: crisis keyword signals, self-harm language, immediate danger phrases,
// and conversational history distress escalation.
// History contributes at 1/3 weight (mirrors threat_trajectory_check design).
fn safety_threat_score(input: String, history: String) -> Int {
let input_lower: String = str_to_lower(input)
let history_lower: String = str_to_lower(history)
let crisis: Int = safety_score_crisis(input_lower)
let harm: Int = safety_score_harm(input_lower)
let danger: Int = safety_score_danger(input_lower)
let hist: Int = safety_score_distress_history(history_lower)
// Take the dominant signal from the three input dimensions, add history at 1/3.
// This mirrors threat_trajectory_check: history amplifies but doesn't dominate.
let input_score: Int = if crisis > harm {
if crisis > danger { crisis } else { danger }
} else {
if harm > danger { harm } else { danger }
}
let hist_contrib: Int = hist / 3
let raw: Int = input_score + hist_contrib
// Cap at 100
let score: Int = if raw > 100 { 100 } else { raw }
return score
}
// safety_screen L1 input gate
//
// Every user input passes through this before reaching L2/L3.
// Returns a JSON envelope consumed by steward_align (L2).
//
// Return shapes:
// {"action":"hard_bell","reason":"immediate safety concern","content":""}
// {"action":"soft_bell","reason":"wellbeing check needed","content":"<input>"}
// {"action":"pass","content":"<input>"}
fn safety_screen(input: String, history: String) -> String {
let score: Int = safety_threat_score(input, history)
let hard: Int = hard_bell_threshold()
let soft: Int = soft_bell_threshold()
if score >= hard {
let summary: String = str_slice(input, 0, 80)
let discard: String = safety_log_bell("hard", "immediate safety concern", summary)
return "{\"action\":\"hard_bell\",\"reason\":\"immediate safety concern\",\"content\":\"\"}"
}
if score >= soft {
let summary: String = str_slice(input, 0, 80)
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
let e1: String = str_replace(input, "\\", "\\\\")
let e2: String = str_replace(e1, "\"", "\\\"")
let e3: String = str_replace(e2, "\n", "\\n")
let safe_input: String = str_replace(e3, "\r", "\\r")
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
}
let e1: String = str_replace(input, "\\", "\\\\")
let e2: String = str_replace(e1, "\"", "\\\"")
let e3: String = str_replace(e2, "\n", "\\n")
let safe_input: String = str_replace(e3, "\r", "\\r")
return "{\"action\":\"pass\",\"content\":\"" + safe_input + "\"}"
}
// safety_validate L1 output gate
//
// Every generated output passes through this before reaching the user.
// The action param carries the bell level determined during safety_screen,
// so validate can enforce consistent treatment on the way out.
//
// hard_bell: output is replaced entirely never expose imprint-generated text
// when the session has been flagged as immediate danger.
// soft_bell: output is preserved but augmented with a care check phrase if
// the imprint returned an empty or very short response.
// pass: output returned verbatim.
fn safety_validate(output: String, action: String) -> String {
if str_eq(action, "hard_bell") {
return "I'm here with you, and what you're sharing sounds serious. Please reach out to a crisis line now — in the US you can call or text 988 (Suicide and Crisis Lifeline), available 24/7. You don't have to go through this alone."
}
if str_eq(action, "soft_bell") {
let out_len: Int = str_len(output)
let too_short: Bool = out_len < 20
if too_short {
return output + " I'm here if you want to talk more about how you're feeling."
}
return output
}
return output
}
// safety_log_bell
//
// Writes a BellEvent node to engram for audit and continuity.
// Never surfaces to the user; consumed by daemon observability layer.
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
let discard: String = engram_node_full(
content,
"BellEvent",
"bell:" + level,
el_from_float(0.95),
el_from_float(0.95),
el_from_float(1.0),
"Episodic",
tags
)
return ""
}
+8
View File
@@ -0,0 +1,8 @@
// Layer 1 — Safety: extern declarations
// auto-generated by elc --emit-header — do not edit
extern fn soft_bell_threshold() -> Int
extern fn hard_bell_threshold() -> Int
extern fn safety_threat_score(input: String, history: String) -> Int
extern fn safety_screen(input: String, history: String) -> String
extern fn safety_validate(output: String, action: String) -> String
extern fn safety_log_bell(level: String, reason: String, input_summary: String) -> String
+3
View File
@@ -1,5 +1,8 @@
import "../foundation/el/elp/src/elp.el"
import "memory.el"
import "safety.el"
import "stewardship.el"
import "imprint.el"
import "awareness.el"
import "chat.el"
import "studio.el"
+274
View File
@@ -0,0 +1,274 @@
// tests/test_imprint.el
// Comprehensive test suite for imprint.el (Layer 3 boundary).
//
// El has no native test framework. Tests are plain El programs that
// call functions, compare results, and print PASS/FAIL via println.
// Each test is a fn returning Int: 0 = pass, 1 = fail.
// run_all() drives them and returns a final summary line.
//
// Syntax rules observed:
// - No Bool type annotation inference only
// - No && / || nested if/else used instead
// - No unary ! inverted with if/else
// - No closures or lambdas
import "imprint.elh"
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
fn assert_eq(label: String, got: String, want: String) -> Int {
if str_eq(got, want) {
println("PASS " + label)
return 0
}
println("FAIL " + label + " got=" + got + " want=" + want)
return 1
}
fn assert_not_eq(label: String, got: String, not_want: String) -> Int {
if str_eq(got, not_want) {
println("FAIL " + label + " got=" + got + " (should differ)")
return 1
}
println("PASS " + label)
return 0
}
fn assert_contains(label: String, haystack: String, needle: String) -> Int {
if str_contains(haystack, needle) {
println("PASS " + label)
return 0
}
println("FAIL " + label + " value=" + haystack + " missing=" + needle)
return 1
}
fn assert_not_contains(label: String, haystack: String, needle: String) -> Int {
if str_contains(haystack, needle) {
println("FAIL " + label + " value=" + haystack + " unexpected=" + needle)
return 1
}
println("PASS " + label)
return 0
}
fn assert_not_empty(label: String, got: String) -> Int {
if str_eq(got, "") {
println("FAIL " + label + " got empty string")
return 1
}
println("PASS " + label)
return 0
}
// ---------------------------------------------------------------------------
// TEST 1
// imprint_current() with no prior state should return "base".
// We cannot guarantee a clean state across runs so we call imprint_unload()
// first to normalise, then check.
// ---------------------------------------------------------------------------
fn test_01_current_after_unload_is_base() -> Int {
imprint_unload()
let id: String = imprint_current()
return assert_eq("01 imprint_current after unload == base", id, "base")
}
// ---------------------------------------------------------------------------
// TEST 2
// imprint_unload() then imprint_current() always returns "base".
// Calling unload twice must be idempotent.
// ---------------------------------------------------------------------------
fn test_02_unload_idempotent() -> Int {
imprint_unload()
imprint_unload()
let id: String = imprint_current()
return assert_eq("02 double-unload still base", id, "base")
}
// ---------------------------------------------------------------------------
// TEST 3
// imprint_load() with a nonexistent ID must return ok==false and an error
// message that mentions the requested ID.
// We use a UUID-like name that will never exist in the engram.
// ---------------------------------------------------------------------------
fn test_03_load_nonexistent_returns_ok_false() -> Int {
let result: String = imprint_load("__test_ghost_imprint_xyz__")
let ok_field: String = json_get(result, "ok")
let fails: Int = 0
let fails = fails + assert_eq("03a load nonexistent ok==false", ok_field, "false")
let fails = fails + assert_contains("03b load nonexistent error mentions id", result, "__test_ghost_imprint_xyz__")
return if fails > 0 { 1 } else { 0 }
}
// ---------------------------------------------------------------------------
// TEST 4
// json_get on imprint_load result should always return the "ok" field.
// Both ok=true and ok=false payloads must carry the field.
// We test the miss case (guaranteed) for the field's presence.
// ---------------------------------------------------------------------------
fn test_04_load_result_has_ok_field() -> Int {
let result: String = imprint_load("__test_field_check__")
let ok_field: String = json_get(result, "ok")
return assert_not_empty("04 load result contains ok field", ok_field)
}
// ---------------------------------------------------------------------------
// TEST 5
// imprint_respond() with imprint_id == "base" must return input unchanged.
// The base path is the identity function no annotation is added.
// ---------------------------------------------------------------------------
fn test_05_respond_base_passthrough() -> Int {
let input: String = "Hello from the base layer."
let output: String = imprint_respond(input, "base")
return assert_eq("05 respond with base id == passthrough", output, input)
}
// ---------------------------------------------------------------------------
// TEST 6
// imprint_respond() with imprint_id == "" (empty string) must also return
// input unchanged empty string is treated as base.
// ---------------------------------------------------------------------------
fn test_06_respond_empty_id_passthrough() -> Int {
let input: String = "Test input for empty imprint_id."
let output: String = imprint_respond(input, "")
return assert_eq("06 respond with empty id == passthrough", output, input)
}
// ---------------------------------------------------------------------------
// TEST 7
// imprint_respond() with an unknown imprint_id (node not in engram) must
// fall back gracefully and return input unchanged.
// The spec says: never hard-fail at L3 graceful fallback to base.
// ---------------------------------------------------------------------------
fn test_07_respond_unknown_id_graceful_fallback() -> Int {
let input: String = "Graceful fallback test payload."
let output: String = imprint_respond(input, "__no_such_imprint_ever__")
return assert_eq("07 respond unknown id graceful fallback == passthrough", output, input)
}
// ---------------------------------------------------------------------------
// TEST 8
// After imprint_unload(), imprint_respond should produce base behaviour.
// We call respond with the just-cleared state ID ("base") to confirm
// the unload/respond pipeline produces the identity transform.
// ---------------------------------------------------------------------------
fn test_08_respond_after_unload_is_passthrough() -> Int {
imprint_unload()
let current: String = imprint_current()
let input: String = "Post-unload response passthrough check."
let output: String = imprint_respond(input, current)
return assert_eq("08 respond after unload == passthrough", output, input)
}
// ---------------------------------------------------------------------------
// TEST 9
// imprint_surface_knowledge() must return a String (not crash, not empty
// in a way that signals an error code). We test both base and named paths.
// For "base" the query is passed directly; for a named imprint the query
// is scoped but the return must still be a String.
// ---------------------------------------------------------------------------
fn test_09_surface_knowledge_returns_string() -> Int {
let result_base: String = imprint_surface_knowledge("test query", "base")
// Must be a String "" or "[]" is valid (no matching nodes), but the
// call must not return an error token. We check it is not the literal
// string "error" to catch any error-signalling convention.
let fails: Int = 0
let fails = fails + assert_not_eq("09a surface_knowledge base != error", result_base, "error")
let result_named: String = imprint_surface_knowledge("test query", "demo-imprint")
let fails = fails + assert_not_eq("09b surface_knowledge named != error", result_named, "error")
// Scoped query must embed the domain scope string
// (test indirectly: the scoped call does not crash and returns a String)
let fails = fails + assert_not_eq("09c surface_knowledge named != crash sentinel", result_named, "CRASH")
return if fails > 0 { 1 } else { 0 }
}
// ---------------------------------------------------------------------------
// TEST 10
// imprint_surface_memory_read() must return a String for any query.
// This is a read-only engram search it must never write.
// We check the return is not an error sentinel and is a valid String.
// ---------------------------------------------------------------------------
fn test_10_surface_memory_read_returns_string() -> Int {
let result: String = imprint_surface_memory_read("soul memory test")
let fails: Int = 0
let fails = fails + assert_not_eq("10a surface_memory_read != error", result, "error")
let fails = fails + assert_not_eq("10b surface_memory_read != crash", result, "CRASH")
return if fails > 0 { 1 } else { 0 }
}
// ---------------------------------------------------------------------------
// TEST 11
// imprint_surface_knowledge() with empty imprint_id uses the base path
// (no domain scoping) must behave identically to base.
// ---------------------------------------------------------------------------
fn test_11_surface_knowledge_empty_id_equals_base() -> Int {
let base_result: String = imprint_surface_knowledge("neuron layer test", "base")
let empty_result: String = imprint_surface_knowledge("neuron layer test", "")
return assert_eq("11 surface_knowledge empty id == base id", empty_result, base_result)
}
// ---------------------------------------------------------------------------
// TEST 12
// imprint_respond() must NOT annotate when imprint_id is "base" the
// "[imprint:" marker must be absent in the output.
// ---------------------------------------------------------------------------
fn test_12_respond_base_no_annotation() -> Int {
let input: String = "No annotation expected."
let output: String = imprint_respond(input, "base")
return assert_not_contains("12 respond base has no imprint annotation", output, "[imprint:")
}
// ---------------------------------------------------------------------------
// TEST 13
// imprint_load() with empty-string ID must return ok==false.
// An empty ID is not a valid imprint identifier.
// ---------------------------------------------------------------------------
fn test_13_load_empty_id_returns_ok_false() -> Int {
let result: String = imprint_load("")
let ok_field: String = json_get(result, "ok")
return assert_eq("13 load empty id ok==false", ok_field, "false")
}
// ---------------------------------------------------------------------------
// TEST 14
// After a failed imprint_load(), imprint_current() must still return "base"
// a failed load must leave state untouched.
// ---------------------------------------------------------------------------
fn test_14_failed_load_does_not_mutate_state() -> Int {
imprint_unload()
let discard: String = imprint_load("__nonexistent_for_state_test__")
let id: String = imprint_current()
return assert_eq("14 failed load leaves state as base", id, "base")
}
// ---------------------------------------------------------------------------
// run_all executes every test and prints a summary.
// Returns total failure count as Int.
// ---------------------------------------------------------------------------
fn run_all() -> Int {
println("=== imprint.el test suite ===")
let total: Int = 0
let failed: Int = 0
let failed = failed + test_01_current_after_unload_is_base()
let failed = failed + test_02_unload_idempotent()
let failed = failed + test_03_load_nonexistent_returns_ok_false()
let failed = failed + test_04_load_result_has_ok_field()
let failed = failed + test_05_respond_base_passthrough()
let failed = failed + test_06_respond_empty_id_passthrough()
let failed = failed + test_07_respond_unknown_id_graceful_fallback()
let failed = failed + test_08_respond_after_unload_is_passthrough()
let failed = failed + test_09_surface_knowledge_returns_string()
let failed = failed + test_10_surface_memory_read_returns_string()
let failed = failed + test_11_surface_knowledge_empty_id_equals_base()
let failed = failed + test_12_respond_base_no_annotation()
let failed = failed + test_13_load_empty_id_returns_ok_false()
let failed = failed + test_14_failed_load_does_not_mutate_state()
let total = 14
let passed: Int = total - failed
println("=== " + int_to_str(passed) + "/" + int_to_str(total) + " passed ===")
return failed
}
+428
View File
@@ -0,0 +1,428 @@
// test_safety.el
//
// Comprehensive test suite for safety.el (Layer 1 Safety).
//
// Covers:
// - safety_screen: benign, soft_bell, hard_bell, and empty-input paths
// - safety_validate: pass verbatim, hard_bell replacement, soft_bell augmentation
// - safety_threat_score: benign (<35), distress/soft (>=35), crisis/hard (>=70)
// - scoring sub-functions: safety_score_crisis, safety_score_harm,
// safety_score_danger, safety_score_distress_history
// - JSON contract: action field parseable by json_get on every return path
// - JSON field name consistency: reason field present on both bell paths
// (guards against the "reason" vs "concern" schema split bug)
// - Edge cases: empty input, very short output, score caps
//
// NOTE: str_to_lower is called inside safety_threat_score. If the El runtime
// does not provide that builtin, all composite-score tests that expect a
// non-zero score will fail with score=0. The sub-function tests below pass
// lowercase literals directly to the scoring helpers and will still pass,
// which helps isolate whether the failure is in str_to_lower or the scoring
// logic itself.
//
// Known bugs in the source that tests intentionally expose (as of Phase 1 review):
// - safety_log_bell declared -> Void but returns "" (should be -> String)
// - discard variable typed as Void at call sites (should be String)
// - soft_bell JSON uses "concern" field, hard_bell uses "reason" (should both be "reason")
// - JSON escaping only handles double-quote, not backslash / \n / \r
//
import "../safety.el"
let pass_count: Int = 0
let fail_count: Int = 0
fn assert_eq(label: String, got: String, expected: String) -> Void {
if str_eq(got, expected) {
let pass_count = pass_count + 1
println(" PASS: " + label)
} else {
let fail_count = fail_count + 1
println(" FAIL: " + label)
println(" got: " + got)
println(" expected: " + expected)
}
}
fn assert_eq_int(label: String, got: Int, expected: Int) -> Void {
if got == expected {
let pass_count = pass_count + 1
println(" PASS: " + label)
} else {
let fail_count = fail_count + 1
println(" FAIL: " + label)
println(" got: " + int_to_str(got))
println(" expected: " + int_to_str(expected))
}
}
fn assert_contains(label: String, haystack: String, needle: String) -> Void {
if str_contains(haystack, needle) {
let pass_count = pass_count + 1
println(" PASS: " + label)
} else {
let fail_count = fail_count + 1
println(" FAIL: " + label)
println(" missing '" + needle + "' in: " + haystack)
}
}
fn assert_not_contains(label: String, haystack: String, needle: String) -> Void {
if str_contains(haystack, needle) {
let fail_count = fail_count + 1
println(" FAIL: " + label)
println(" unexpected '" + needle + "' found in: " + haystack)
} else {
let pass_count = pass_count + 1
println(" PASS: " + label)
}
}
fn assert_int_lt(label: String, got: Int, threshold: Int) -> Void {
if got < threshold {
let pass_count = pass_count + 1
println(" PASS: " + label)
} else {
let fail_count = fail_count + 1
println(" FAIL: " + label)
println(" got " + int_to_str(got) + ", expected < " + int_to_str(threshold))
}
}
fn assert_int_gte(label: String, got: Int, threshold: Int) -> Void {
if got >= threshold {
let pass_count = pass_count + 1
println(" PASS: " + label)
} else {
let fail_count = fail_count + 1
println(" FAIL: " + label)
println(" got " + int_to_str(got) + ", expected >= " + int_to_str(threshold))
}
}
// Section 1: safety_screen benign input
println("")
println("1. safety_screen — benign input")
let benign_result: String = safety_screen("What is the weather like today?", "")
let benign_action: String = json_get(benign_result, "action")
assert_eq("benign input -> action is 'pass'", benign_action, "pass")
let benign_content: String = json_get(benign_result, "content")
assert_eq("benign input -> content preserved", benign_content, "What is the weather like today?")
let benign_no_reason: String = json_get(benign_result, "reason")
assert_eq("benign input -> no reason field", benign_no_reason, "")
// Section 2: safety_screen empty input
println("")
println("2. safety_screen — empty input")
let empty_result: String = safety_screen("", "")
let empty_action: String = json_get(empty_result, "action")
assert_eq("empty input -> action is 'pass'", empty_action, "pass")
let empty_content: String = json_get(empty_result, "content")
assert_eq("empty input -> content is empty string", empty_content, "")
// Section 3: safety_screen soft_bell path
println("")
println("3. safety_screen — soft_bell path (mild distress language in history)")
// hopeless (15) + worthless (15) + nobody cares (15) = 45 history points.
// hist_contrib = 45 / 3 = 15. input_score = 0. raw = 15.
// That's below 35. Need input keywords too.
// "can't go on" scores 50 in crisis. 50 > soft threshold (35), < hard threshold (70).
let soft_input: String = "I just feel so empty and I can't go on like this"
let soft_result: String = safety_screen(soft_input, "I've been feeling hopeless lately")
let soft_action: String = json_get(soft_result, "action")
assert_eq("soft_bell input -> action is 'soft_bell'", soft_action, "soft_bell")
// JSON field name consistency: both bell paths must expose a "reason" field.
// This assertion will catch the documented "concern" vs "reason" schema bug.
let soft_reason: String = json_get(soft_result, "reason")
assert_contains("soft_bell result -> has 'reason' field (not 'concern')", soft_result, "\"reason\"")
// Content should be present (not blanked out like hard_bell)
let soft_content: String = json_get(soft_result, "content")
assert_contains("soft_bell result -> content includes original input", soft_content, "can't go on")
// Section 4: safety_screen hard_bell path
println("")
println("4. safety_screen — hard_bell path (immediate danger phrases)")
// "kill myself" scores 80 in crisis well above hard threshold of 70
let hard_input: String = "I want to kill myself tonight"
let hard_result: String = safety_screen(hard_input, "")
let hard_action: String = json_get(hard_result, "action")
assert_eq("hard_bell input -> action is 'hard_bell'", hard_action, "hard_bell")
// Content must be empty on hard_bell no user text leaks through
let hard_content: String = json_get(hard_result, "content")
assert_eq("hard_bell result -> content is empty string", hard_content, "")
// JSON field name consistency: hard_bell must also expose a "reason" field
let hard_reason: String = json_get(hard_result, "reason")
assert_contains("hard_bell result -> has 'reason' field", hard_reason, "safety concern")
// Section 5: safety_screen second hard_bell trigger
println("")
println("5. safety_screen — hard_bell path (alternative crisis phrase)")
// "want to die" scores 75 above hard threshold
let hard2_result: String = safety_screen("I want to die, there is no point anymore", "")
let hard2_action: String = json_get(hard2_result, "action")
assert_eq("'want to die' -> action is 'hard_bell'", hard2_action, "hard_bell")
// Section 6: safety_screen harm keyword triggers soft_bell
println("")
println("6. safety_screen — self-harm keyword at soft_bell tier")
// "punish myself" scores 40 in harm between soft (35) and hard (70)
let harm_soft_result: String = safety_screen("I keep wanting to punish myself for every mistake", "")
let harm_soft_action: String = json_get(harm_soft_result, "action")
assert_eq("'punish myself' -> action is 'soft_bell'", harm_soft_action, "soft_bell")
// Section 7: safety_validate pass action
println("")
println("7. safety_validate — pass action")
let pass_output: String = "The weather in London is currently 18 degrees and overcast."
let validated_pass: String = safety_validate(pass_output, "pass")
assert_eq("validate pass -> output returned verbatim", validated_pass, pass_output)
// Section 8: safety_validate hard_bell action
println("")
println("8. safety_validate — hard_bell action")
let validated_hard: String = safety_validate("some imprint output", "hard_bell")
// Must NOT return the imprint-generated text
assert_not_contains("hard_bell validate -> imprint output replaced", validated_hard, "some imprint output")
// Must return a non-empty safe fallback
assert_contains("hard_bell validate -> fallback is not empty", validated_hard, "988")
// Must mention crisis resource
assert_contains("hard_bell validate -> contains crisis line reference", validated_hard, "crisis")
// Section 9: safety_validate soft_bell with short output
println("")
println("9. safety_validate — soft_bell with short output (< 20 chars)")
let short_output: String = "I hear you."
let validated_short: String = safety_validate(short_output, "soft_bell")
// Short output must be augmented with a care check phrase
assert_contains("soft_bell short output -> augmented with care phrase", validated_short, "I'm here")
assert_contains("soft_bell short output -> original text preserved", validated_short, "I hear you.")
// Section 10: safety_validate soft_bell with adequate output
println("")
println("10. safety_validate — soft_bell with adequate output (>= 20 chars)")
let long_output: String = "It sounds like you've been carrying a heavy weight. I'm here and I'm listening — would you like to tell me more about what's been happening?"
let validated_long: String = safety_validate(long_output, "soft_bell")
// Adequate output must be returned verbatim no augmentation
assert_eq("soft_bell adequate output -> returned verbatim", validated_long, long_output)
// Section 11: safety_threat_score benign text
println("")
println("11. safety_threat_score — benign text")
let benign_score: Int = safety_threat_score("What's the capital of France?", "")
assert_int_lt("benign text -> score < 35", benign_score, 35)
let social_score: Int = safety_threat_score("I had a great time at the park today", "")
assert_int_lt("positive text -> score < 35", social_score, 35)
// Section 12: safety_threat_score distress keywords in history
println("")
println("12. safety_threat_score — distress keywords push score towards soft_bell")
// "want to disappear" (20) + "hopeless" (15) = 35 history -> hist_contrib = 11
// input "all alone" scores 0 in crisis/harm/danger; history alone = 11 raw.
// Need a combination to get to 35. Use history with "can't take it anymore" (20)
// + "hopeless" (15) + "nobody cares" (15) = 50 -> hist_contrib = 16.
// Plus soft input keyword: "punish myself" (40 harm). Total = 40 + 16 = 56 >= 35.
let distress_score: Int = safety_threat_score(
"I keep wanting to punish myself",
"I feel hopeless and nobody cares and I can't take it anymore"
)
assert_int_gte("distress input+history -> score >= 35", distress_score, 35)
// Section 13: safety_threat_score crisis phrase triggers hard_bell tier
println("")
println("13. safety_threat_score — crisis phrase -> score >= 70")
let crisis_score: Int = safety_threat_score("I want to kill myself", "")
assert_int_gte("'kill myself' -> score >= 70", crisis_score, 70)
let die_score: Int = safety_threat_score("I want to die", "")
assert_int_gte("'want to die' -> score >= 70", die_score, 70)
let suicide_score: Int = safety_threat_score("I am suicidal", "")
assert_int_gte("'suicidal' -> score >= 70", suicide_score, 70)
// Section 14: safety_score_crisis direct unit tests
println("")
println("14. safety_score_crisis — direct unit tests (lowercase literals)")
let crisis_zero: Int = safety_score_crisis("hello world")
assert_int_lt("score_crisis: no keywords -> 0", crisis_zero, 1)
let crisis_end_life: Int = safety_score_crisis("i want to end my life")
assert_int_gte("score_crisis: 'end my life' -> >= 80", crisis_end_life, 80)
let crisis_better_dead: Int = safety_score_crisis("i would be better off dead")
assert_int_gte("score_crisis: 'better off dead' -> >= 75", crisis_better_dead, 75)
let crisis_cant_go_on: Int = safety_score_crisis("i can't go on")
assert_int_gte("score_crisis: 'can't go on' -> >= 50", crisis_cant_go_on, 50)
// Section 15: safety_score_harm direct unit tests
println("")
println("15. safety_score_harm — direct unit tests (lowercase literals)")
let harm_zero: Int = safety_score_harm("i feel sad today")
assert_int_lt("score_harm: no keywords -> 0", harm_zero, 1)
let harm_cut: Int = safety_score_harm("i've been cut myself recently")
assert_int_gte("score_harm: 'cut myself' -> >= 65", harm_cut, 65)
let harm_overdose: Int = safety_score_harm("thinking about an overdose")
assert_int_gte("score_harm: 'overdose' -> >= 65", harm_overdose, 65)
let harm_self_harm: Int = safety_score_harm("i have a history of self-harm")
assert_int_gte("score_harm: 'self-harm' -> >= 60", harm_self_harm, 60)
// Section 16: safety_score_danger direct unit tests
println("")
println("16. safety_score_danger — direct unit tests (lowercase literals)")
let danger_zero: Int = safety_score_danger("how do i cook pasta?")
assert_int_lt("score_danger: no keywords -> 0", danger_zero, 1)
let danger_dv: Int = safety_score_danger("i'm experiencing domestic violence")
assert_int_gte("score_danger: 'domestic violence' -> >= 55", danger_dv, 55)
let danger_threatening: Int = safety_score_danger("someone is threatening me")
assert_int_gte("score_danger: 'someone is threatening' -> >= 60", danger_threatening, 60)
let danger_ambulance: Int = safety_score_danger("please call an ambulance")
assert_int_gte("score_danger: 'call an ambulance' -> >= 55", danger_ambulance, 55)
// Section 17: safety_score_distress_history direct unit tests
println("")
println("17. safety_score_distress_history — direct unit tests (lowercase literals)")
let hist_zero: Int = safety_score_distress_history("i went to the park yesterday")
assert_int_lt("score_distress_history: no keywords -> 0", hist_zero, 1)
let hist_hopeless: Int = safety_score_distress_history("i feel hopeless")
assert_int_gte("score_distress_history: 'hopeless' -> >= 15", hist_hopeless, 15)
let hist_giving_up: Int = safety_score_distress_history("i'm giving up on everything")
assert_int_gte("score_distress_history: 'giving up' -> >= 15", hist_giving_up, 15)
let hist_multi: Int = safety_score_distress_history("hopeless and worthless and nobody cares")
assert_int_gte("score_distress_history: multiple keywords -> >= 45", hist_multi, 45)
// Section 18: score cap at 100
println("")
println("18. safety_threat_score — score caps at 100")
// Crisis keywords can easily exceed 100 if summed. Ensure cap holds.
// "kill myself" (80) + "suicide" (70) + "want to die" (75) all in one message.
// Dominant dimension is capped at 100 by safety_threat_score.
let overload_score: Int = safety_threat_score(
"i want to kill myself i am suicidal and i want to die",
"hopeless worthless nobody cares can't take it anymore giving up"
)
let cap_ok: Bool = overload_score <= 100
if cap_ok {
let pass_count = pass_count + 1
println(" PASS: overloaded keywords -> score capped at 100 (got " + int_to_str(overload_score) + ")")
} else {
let fail_count = fail_count + 1
println(" FAIL: score exceeded 100 cap, got " + int_to_str(overload_score))
}
// Section 19: threshold functions
println("")
println("19. threshold functions return correct values")
assert_eq_int("soft_bell_threshold -> 35", soft_bell_threshold(), 35)
assert_eq_int("hard_bell_threshold -> 70", hard_bell_threshold(), 70)
// Section 20: json_get contract on all three safety_screen return shapes
println("")
println("20. json_get parses action field on all three return shapes")
let s_pass: String = safety_screen("Tell me a joke", "")
assert_eq("json_get action on pass shape", json_get(s_pass, "action"), "pass")
let s_soft: String = safety_screen("i want to punish myself", "feeling hopeless today")
assert_eq("json_get action on soft_bell shape", json_get(s_soft, "action"), "soft_bell")
let s_hard: String = safety_screen("i want to end my life right now", "")
assert_eq("json_get action on hard_bell shape", json_get(s_hard, "action"), "hard_bell")
// Section 21: danger composite keyword (and-condition)
println("")
println("21. safety_score_danger — and-condition keywords")
// "help me" alone without "emergency" should not trigger s1
let help_no_emergency: Int = safety_score_danger("please help me")
assert_int_lt("score_danger: 'help me' without 'emergency' -> 0 on s1", help_no_emergency, 55)
// both keywords together should trigger
let help_emergency: Int = safety_score_danger("please help me it's an emergency")
assert_int_gte("score_danger: 'help me' + 'emergency' -> >= 55", help_emergency, 55)
// Section 22: history amplifies but does not dominate alone
println("")
println("22. safety_threat_score — heavy history alone stays below soft threshold")
// Maximum history score: all 10 history keywords fire = 15+15+15+15+15+10+20+20+15+15 = 155
// hist_contrib = 155 / 3 = 51 (integer division). input_score = 0. raw = 51.
// BUT: dominant-input is 0, so with no input keywords raw = 0 + hist_contrib.
// 51 >= 35. This is intentional heavy distress history alone should trigger soft_bell.
// Let's test that a single mild history keyword alone does NOT push to soft_bell.
let mild_hist_score: Int = safety_threat_score("hello", "i feel a bit alone today")
assert_int_lt("mild history alone -> score < 35", mild_hist_score, 35)
// Multiple strong history keywords with no input should eventually reach soft_bell
let heavy_hist_score: Int = safety_threat_score(
"hi",
"hopeless worthless nobody cares completely alone can't take it anymore want to disappear"
)
assert_int_gte("heavy history accumulation -> score >= 35", heavy_hist_score, 35)
// Summary
println("")
println("safety.el tests: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")