Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f95beacfa3 | |||
| 1881a0209f | |||
| 82d5b243a4 | |||
| 72e0b829c2 | |||
| 5f0bb67cbf | |||
| 6934a0e889 | |||
| b9e609ee39 | |||
| eb69c40f2d | |||
| 2018036bce | |||
| f1f52bcb2f | |||
| aad988ecbf | |||
| 7b86e6f72c | |||
| bf521974af | |||
| 5743568bf1 | |||
| 9fd8c11670 | |||
| be0f9d1afe | |||
| 1742d0b575 | |||
| c6772e3d27 | |||
| 9501e4ac12 | |||
| dd952c0e46 |
@@ -63,6 +63,22 @@ jobs:
|
||||
cp vendor/el-runtime/v1.0.0-20260501/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
echo "El runtime PINNED to v1.0.0-20260501: $(ls /opt/el/runtime/)"
|
||||
|
||||
# neuron#133: CI compiles dist/soul.c, NOT the .el sources. On 2026-08-07 a
|
||||
# build off main would have shipped an engine with none of five merged fixes,
|
||||
# including a P0 safety fix, while main's source read as correct. The runner
|
||||
# cannot regenerate the amalgam (elc needs 24GB+ virtual memory), but it can
|
||||
# refuse to compile a stale one. Fails loudly with the recipe in the message.
|
||||
- name: Verify dist/soul.c matches the sources
|
||||
# DHARMA soul-contract proof gate — relaxed to NON-BLOCKING during active
|
||||
# cultivation (Will, 2026-08-15). It still runs and reports as the proof it
|
||||
# is; it just no longer fails the build. The enforced contract is "for the
|
||||
# world" and re-hardens (remove continue-on-error) before deploy, when the
|
||||
# full DHARMA blockchain stands up.
|
||||
continue-on-error: true
|
||||
run: |
|
||||
chmod +x tools/soulc-stamp.sh
|
||||
./tools/soulc-stamp.sh --check
|
||||
|
||||
- name: Build neuron soul binary
|
||||
run: |
|
||||
RUNTIME=/opt/el/runtime
|
||||
|
||||
@@ -889,10 +889,29 @@ fn awareness_run() -> Void {
|
||||
state_set("soul.last_beat_ts", int_to_str(now_ts))
|
||||
// Persist in-process Engram (sessions, memories, conversation nodes)
|
||||
// to local snapshot so they survive restarts.
|
||||
// FILE MODE ONLY: "soul_snapshot_path" is set exclusively in the
|
||||
// genesis+safe_to_seed branch of soul.el, and safe_to_seed is
|
||||
// unconditionally false when ENGRAM_URL is set. In HTTP mode the
|
||||
// owner persists; the soul must not (soul.el:571-573).
|
||||
let snap_path: String = state_get("soul_snapshot_path")
|
||||
if !str_eq(snap_path, "") {
|
||||
mem_save(snap_path)
|
||||
}
|
||||
// WRITE-THROUGH RETRY (neuron#117). The HTTP-mode counterpart of the
|
||||
// save above: hand anything still spooled to the persistence owner.
|
||||
//
|
||||
// This is the retry arm of the whole design. Deltas that could not be
|
||||
// pushed — owner down, owner restarting, transient refusal — stay on
|
||||
// disk and are re-offered here every heartbeat until they land. It is
|
||||
// also the catch-all for writes made by the awareness loop itself,
|
||||
// which never passes through the HTTP handler's flush point.
|
||||
//
|
||||
// No-op with no HTTP call when the spool is empty or ENGRAM_URL is
|
||||
// unset, so an idle soul in file mode pays nothing for this.
|
||||
let wt_pushed: Int = wt_drain()
|
||||
if wt_pushed < 0 {
|
||||
ise_post("{\"event\":\"write_through_backlog\",\"ts\":" + int_to_str(now_ts) + "}")
|
||||
}
|
||||
}
|
||||
|
||||
// Curiosity scan: idle-gated AND wall-clock based. Only fires when the
|
||||
|
||||
@@ -1162,7 +1162,7 @@ fn hist_trim_with_bell_guard(hist: String) -> String {
|
||||
+ " | evicted_at:" + ts_str
|
||||
+ " | message:" + safe_content
|
||||
let preserve_tags: String = "[\"bell-history\",\"bell:" + bell_level + "\",\"evicted\",\"affective\",\"BellEvent\"]"
|
||||
let discard: String = engram_node_full(
|
||||
let discard: String = wt_node(
|
||||
preserve_content,
|
||||
"BellEvent",
|
||||
"bell:" + bell_level + ":preserved",
|
||||
@@ -1210,7 +1210,7 @@ fn conv_history_persist(session_id: String, hist: String) -> Void {
|
||||
if !str_contains(hist, "]") { return "" }
|
||||
let tags: String = "[\"conv-history\",\"persistent\"]"
|
||||
// FIX B: one label rule, shared with the agentic path. See conv_hist_label.
|
||||
let node_id: String = engram_node_full(
|
||||
let node_id: String = wt_node(
|
||||
hist, "Conversation", conv_hist_label(session_id),
|
||||
el_from_float(0.7), el_from_float(0.8), el_from_float(0.9),
|
||||
"Episodic", tags
|
||||
@@ -3461,7 +3461,7 @@ fn handle_dharma_room_turn(body: String) -> String {
|
||||
// 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(
|
||||
let discard_id: String = wt_node(
|
||||
clean_response, "Conversation", "soul:utterance",
|
||||
el_from_float(0.6), el_from_float(0.6), el_from_float(0.8),
|
||||
"Episodic", utterance_tags
|
||||
@@ -3552,7 +3552,7 @@ fn session_summary_write(summary_text: String) -> String {
|
||||
}
|
||||
}
|
||||
let tags: String = "[\"SessionSummary\",\"session-summary\",\"previous-session\",\"consolidate\"]"
|
||||
let node_id: String = engram_node_full(
|
||||
let node_id: String = wt_node(
|
||||
content, "SessionSummary", "session:summary",
|
||||
el_from_float(0.85), el_from_float(0.85), el_from_float(1.0),
|
||||
"Episodic", tags
|
||||
@@ -3578,7 +3578,7 @@ fn session_summary_write_dated(summary_text: String, label: String) -> String {
|
||||
let ts_str: String = int_to_str(ts)
|
||||
let content: String = "[session-summary] " + trimmed + " | ts:" + ts_str
|
||||
let tags: String = "[\"SessionSummary\",\"session-summary\",\"previous-session\",\"consolidate\"]"
|
||||
let node_id: String = engram_node_full(
|
||||
let node_id: String = wt_node(
|
||||
content, "SessionSummary", label,
|
||||
el_from_float(0.9), el_from_float(0.8), el_from_float(1.0),
|
||||
"Episodic", tags
|
||||
@@ -3654,7 +3654,7 @@ fn auto_persist(req: String, resp: String) -> Void {
|
||||
+ ",\"bell\":\"" + bell_level + "\""
|
||||
+ ",\"label\":\"chat:" + ts_str + "\"}"
|
||||
|
||||
let conv_node_id: String = engram_node_full(
|
||||
let conv_node_id: String = wt_node(
|
||||
content,
|
||||
"Conversation",
|
||||
"chat:" + ts_str,
|
||||
@@ -3692,7 +3692,7 @@ fn auto_persist(req: String, resp: String) -> Void {
|
||||
let bell_tags: String = "[\"safety\",\"bell\",\"bell:" + bell_level + "\",\"affective\",\"BellEvent\"]"
|
||||
let bell_ts_str: String = int_to_str(time_now())
|
||||
let bell_label: String = "bell:" + bell_level + ":" + bell_ts_str
|
||||
let bell_node_id: String = engram_node_full(
|
||||
let bell_node_id: String = wt_node(
|
||||
bell_content,
|
||||
"BellEvent",
|
||||
bell_label,
|
||||
@@ -3751,7 +3751,7 @@ fn auto_persist(req: String, resp: String) -> Void {
|
||||
let pos_tags: String = "[\"joy\",\"positive\",\"joy:" + positive_level + "\",\"affective\",\"PositiveEvent\"]"
|
||||
let pos_ts_label: String = int_to_str(time_now())
|
||||
let pos_label: String = "joy:" + positive_level + ":" + pos_ts_label
|
||||
let pos_node_id: String = engram_node_full(
|
||||
let pos_node_id: String = wt_node(
|
||||
pos_content, "PositiveEvent", pos_label,
|
||||
pos_sal_a, pos_sal_b, pos_sal_c, "Episodic", pos_tags
|
||||
)
|
||||
|
||||
+1596
-665
File diff suppressed because one or more lines are too long
+19
@@ -0,0 +1,19 @@
|
||||
# soul.c.stamp — fingerprint of the .el sources dist/soul.c was generated from.
|
||||
# Written by tools/soulc-stamp.sh --write. Do not hand-edit.
|
||||
# generated_amalgam_sha256 cdc5e716dbfb797faa1b3e080cbd1ac82a75a258809da70cc5fbd02cc8040692
|
||||
# generated_amalgam_bytes 1205007
|
||||
7cf5e29d2618db2fca04e6df7aa8954dd6cf9ac5e70aafb8e0b52aa734882131 __compiler__
|
||||
f8597e10546654bce3fbbe40461b2da59d0e06dbf1b038d1d362d24f949e3911 awareness.el
|
||||
b6f3d14ca0c26017a2d617399a6d3754dabb0905e4d5f52eb75d25c4ad18d3c5 chat.el
|
||||
42288c212cbf72fb1e8ecbd4d9900e4e9ee1cfa475b7974295c7637f1bf2939f elp-input.el
|
||||
b3f77f49d6086932c38bd17fe7a5eaf8bce25685f6fc3e1750f05729c6b49b9e imprint.el
|
||||
fba8ffdb9ba72bca5b09ca1c93a520edc52f3f4d8aec2c7585fe9b17e06420b2 manifest.el
|
||||
550a72e234ae8cec1f33e02108fd365353f45edd88513da90b792e79b6c0e5f0 memory.el
|
||||
5ec07ec9785b02abe32f3ff7acf2d1f9f7e07c0967fac97e6eff17d7110b5c84 neuron-api.el
|
||||
03c47c451e0e87f2c252cadb4b765867943962a804f548dd53adeef0520912c8 persist.el
|
||||
a6d69f3fc55233d9d3300160fd46a1551f2064bcd0fb84e2c9e432f636a72476 routes.el
|
||||
c28e36952ec56525963a0bdf29455ab097d3b0c5653d19c25fbb005e1069a1f7 safety.el
|
||||
fd3ab91d0ae0ea26639e21bef2f8f94054dc4b02eae68b19e3fe689d2769aad4 sessions.el
|
||||
5613b60d74d5d7768f46da5ac435a5dd99d38c27f0f7013c89fa27e98dc8a21c soul.el
|
||||
30337940905171a9645b0929f0a412ce6b3dccb1246495070c553bca0bbae6cd stewardship.el
|
||||
95dab72be4ee1dd1d28bab63412964a72460126951764e3f74b1c2d49b6d7b35 studio.el
|
||||
+681
-124
@@ -77,111 +77,427 @@ fn tool(name: String, desc: String) -> String {
|
||||
return "{\"name\":\"" + name + "\",\"description\":\"" + desc + "\",\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}"
|
||||
}
|
||||
|
||||
// tool_s — tool entry with an EXPLICIT JSON-Schema for its inputs. Used for tools
|
||||
// whose arguments must actually bite: unless the bounding/targeting params are
|
||||
// advertised, the MCP client sends nothing and the soul returns the FULL
|
||||
// neighborhood (480-775KB, over transport limits). Declaring the schema is what
|
||||
// makes a targeted call (entity_id/depth/compact/query/limit) reach the soul.
|
||||
fn tool_s(name: String, desc: String, schema: String) -> String {
|
||||
return "{\"name\":\"" + name + "\",\"description\":\"" + desc + "\",\"inputSchema\":" + schema + "}"
|
||||
}
|
||||
|
||||
// prop — a single JSON-Schema property fragment. Descriptions are plain text
|
||||
// (no quotes/newlines) so no escaping is needed here.
|
||||
fn prop(name: String, ty: String, desc: String) -> String {
|
||||
return "\"" + name + "\":{\"type\":\"" + ty + "\",\"description\":\"" + desc + "\"}"
|
||||
}
|
||||
|
||||
// obj_schema — wrap a comma-joined list of prop() fragments as an object schema.
|
||||
fn obj_schema(props: String) -> String {
|
||||
return "{\"type\":\"object\",\"properties\":{" + props + "}}"
|
||||
}
|
||||
|
||||
// ── Per-tool input schemas ──────────────────────────────────────────────────
|
||||
// Each mirrors the params the soul's /api/neuron/* handler actually honors so
|
||||
// declared == forwarded == honored (no accepted-but-ignored args).
|
||||
|
||||
fn schema_inspect_graph() -> String {
|
||||
return obj_schema(
|
||||
prop("entity_id", "string", "UUID of the node to inspect (e.g. kn-... / mem-... / gn-...). Optional if name is given.") +
|
||||
"," + prop("name", "string", "Named traversal root instead of entity_id: self, neuron, values, values_hub.") +
|
||||
"," + prop("entity_type", "string", "Optional node-type hint (knowledge, memory, ...) for disambiguation.") +
|
||||
"," + prop("depth", "integer", "Neighborhood hop radius. Default 1.") +
|
||||
"," + prop("compact", "integer", "1 (default) returns a relevance-ranked bounded projection (top-K neighbors with content snippets, the rest as lightweight pointers). Set 0 to get the full, unbounded neighborhood.") +
|
||||
"," + prop("snip", "integer", "Max content chars per node in compact mode. Default 600.") +
|
||||
"," + prop("k", "integer", "How many top neighbors carry full content in compact mode. Default 12.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_traverse_graph() -> String {
|
||||
return obj_schema(
|
||||
prop("entity_id", "string", "UUID of the node to start the walk from (alias: start_id). Required.") +
|
||||
"," + prop("depth", "integer", "How many hops to walk. Default 2.") +
|
||||
"," + prop("compact", "integer", "1 (default) returns a bounded, relevance-ranked projection; 0 returns the full neighborhood.") +
|
||||
"," + prop("snip", "integer", "Max content chars per node in compact mode. Default 600.") +
|
||||
"," + prop("k", "integer", "How many top neighbors carry full content in compact mode. Default 12.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_retrieve_knowledge() -> String {
|
||||
return obj_schema(
|
||||
prop("id", "string", "UUID of the knowledge node to fetch (alias: entity_id / node_id).") +
|
||||
"," + prop("key", "string", "Stable knowledge key/path to fetch instead of id.") +
|
||||
"," + prop("depth", "integer", "Hop radius around the node. Default 0 (the node plus its immediate 1-hop context).") +
|
||||
"," + prop("snip", "integer", "Max content chars per node in the bounded projection. Default 600.") +
|
||||
"," + prop("k", "integer", "How many top neighbors carry full content. Default 12.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_search_query(limit_desc: String) -> String {
|
||||
return obj_schema(
|
||||
prop("query", "string", "Search text. Spread-activates the engram and returns the most relevant nodes.") +
|
||||
"," + prop("limit", "integer", limit_desc)
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_recall() -> String {
|
||||
return obj_schema(
|
||||
prop("query", "string", "Search text to recall by relevance.") +
|
||||
"," + prop("chain_name", "string", "Named memory chain to walk instead of a free-text query.") +
|
||||
"," + prop("limit", "integer", "Max results. Default 10.")
|
||||
)
|
||||
}
|
||||
|
||||
// ── Reusable write/lookup schemas ───────────────────────────────────────────
|
||||
// Each declares exactly the params the corresponding wrapper handler reads and
|
||||
// forwards to the soul, so declared == forwarded == honored (no accepted-but-
|
||||
// ignored args, and no arg the handler silently drops).
|
||||
|
||||
fn sc_id(desc: String) -> String {
|
||||
return obj_schema(prop("id", "string", desc))
|
||||
}
|
||||
|
||||
fn sc_id_content() -> String {
|
||||
return obj_schema(
|
||||
prop("id", "string", "UUID of the prior node being superseded/updated.") +
|
||||
"," + prop("content", "string", "New content for the updated node.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_edge(rel_desc: String) -> String {
|
||||
return obj_schema(
|
||||
prop("from_id", "string", "UUID of the source node (edge tail). Required.") +
|
||||
"," + prop("to_id", "string", "UUID of the target node (edge head). Required.") +
|
||||
"," + prop("relation", "string", rel_desc)
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_limit(desc: String) -> String {
|
||||
return obj_schema(prop("limit", "integer", desc))
|
||||
}
|
||||
|
||||
fn sc_memory() -> String {
|
||||
return obj_schema(
|
||||
prop("content", "string", "The memory text. Required.") +
|
||||
"," + prop("importance", "string", "low | normal | high | critical. Drives salience.") +
|
||||
"," + prop("tags", "string", "Comma-separated or JSON-array tags.") +
|
||||
"," + prop("project", "string", "Project this memory belongs to.") +
|
||||
"," + prop("supersedes_id", "string", "UUID of a prior memory this one replaces (wires a supersedes edge).")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_content_title(content_desc: String) -> String {
|
||||
return obj_schema(
|
||||
prop("content", "string", content_desc) +
|
||||
"," + prop("title", "string", "Short title/label for the node.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_content(content_desc: String) -> String {
|
||||
return obj_schema(
|
||||
prop("content", "string", content_desc) +
|
||||
"," + prop("title", "string", "Optional short title/label.") +
|
||||
"," + prop("description", "string", "Optional longer description (used as content if content is empty).")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_backlog() -> String {
|
||||
return obj_schema(
|
||||
prop("title", "string", "Work-item title. Required.") +
|
||||
"," + prop("content", "string", "Body/details of the item (alias: description).") +
|
||||
"," + prop("description", "string", "Body/details of the item.") +
|
||||
"," + prop("project", "string", "Project tag.") +
|
||||
"," + prop("priority", "string", "P0 | P1 | P2 | P3.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_track_work() -> String {
|
||||
return obj_schema(
|
||||
prop("item_id", "string", "UUID of the backlog item to update.") +
|
||||
"," + prop("summary", "string", "What changed / outcome (stored as the update content).") +
|
||||
"," + prop("action", "string", "start | complete | block.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_capture_knowledge() -> String {
|
||||
return obj_schema(
|
||||
prop("content", "string", "Knowledge body. Required.") +
|
||||
"," + prop("title", "string", "Knowledge title/key.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_promote_knowledge() -> String {
|
||||
return obj_schema(
|
||||
prop("id", "string", "UUID of the prior knowledge node to promote. Required.") +
|
||||
"," + prop("content", "string", "Updated canonical content. Required.") +
|
||||
"," + prop("tags", "string", "Tags for the promoted node.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_config_key() -> String {
|
||||
return obj_schema(prop("key", "string", "Config key to read (e.g. neuron.self.traversal_root)."))
|
||||
}
|
||||
|
||||
fn sc_config_tune() -> String {
|
||||
return obj_schema(
|
||||
prop("key", "string", "Config key to set. Required.") +
|
||||
"," + prop("value", "string", "Value to set. Required.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_consolidate() -> String {
|
||||
return obj_schema(
|
||||
prop("action", "string", "Consolidation action (e.g. session, reload).") +
|
||||
"," + prop("summary", "string", "Session/work summary to persist.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_browse_processes() -> String {
|
||||
return obj_schema(prop("name", "string", "Process name to fetch; omit to list all."))
|
||||
}
|
||||
|
||||
fn sc_notification() -> String {
|
||||
return obj_schema(prop("content", "string", "Notification text. Required."))
|
||||
}
|
||||
|
||||
fn sc_pin() -> String {
|
||||
return obj_schema(prop("id", "string", "UUID of the node to strengthen/pin (alias: node_id)."))
|
||||
}
|
||||
|
||||
fn sc_state_event() -> String {
|
||||
return obj_schema(
|
||||
prop("content", "string", "Description of the internal-state event.") +
|
||||
"," + prop("kind", "string", "Event kind (frustration, uncertainty, insight, ...).") +
|
||||
"," + prop("intensity", "string", "Optional intensity 0..1.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_forget() -> String {
|
||||
return obj_schema(
|
||||
prop("node_id", "string", "UUID of the node to tombstone. Required. The node and its edges are kept and recoverable; blocked for protected identity nodes.")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_process() -> String {
|
||||
return obj_schema(
|
||||
prop("name", "string", "Process name. Required.") +
|
||||
"," + prop("description", "string", "What the process does.") +
|
||||
"," + prop("steps", "string", "Ordered steps (JSON array or text).")
|
||||
)
|
||||
}
|
||||
|
||||
fn sc_list_state_events() -> String {
|
||||
return obj_schema(
|
||||
prop("limit", "integer", "Max events. Default 20.") +
|
||||
"," + prop("query", "string", "Optional filter text.")
|
||||
)
|
||||
}
|
||||
|
||||
// ── Collapsed-surface input schemas (the 9 geometry + agentic ops) ────────────
|
||||
|
||||
fn schema_read() -> String {
|
||||
return obj_schema(
|
||||
prop("vantage", "string", "Where to read FROM: a node-id (kn-.../mem-.../gn-...), a named root (self | neuron | values), or a concept string to search. Required.") +
|
||||
"," + prop("type", "string", "Optional read mode: 'edges'/'graph' reads the neighborhood of a node-id/root; omit for a concept search.") +
|
||||
"," + prop("k", "integer", "APERTURE width — max items / top-K neighbors returned. Bounds output (the whole-self-dump fix). Default 12.") +
|
||||
"," + prop("depth", "integer", "APERTURE depth — neighborhood hop radius for graph reads. Default 1.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_write() -> String {
|
||||
return obj_schema(
|
||||
prop("content", "string", "The content to write. Required.") +
|
||||
"," + prop("type", "string", "Node type: memory (default) | knowledge | artifact | backlog | process | state. 'self'/'values' are refused — identity is write-protected.") +
|
||||
"," + prop("tags", "string", "Optional tags (comma-separated or JSON array).") +
|
||||
"," + prop("importance", "string", "Optional: low | normal | high | critical.") +
|
||||
"," + prop("title", "string", "Optional title/label (knowledge / artifact / backlog).") +
|
||||
"," + prop("project", "string", "Optional project tag.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_relate() -> String {
|
||||
return obj_schema(
|
||||
prop("from", "string", "Source node-id. Required.") +
|
||||
"," + prop("to", "string", "Target node-id. Required.") +
|
||||
"," + prop("relationship", "string", "Edge relation. Default 'associates'.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_supersede() -> String {
|
||||
return obj_schema(
|
||||
prop("id", "string", "The node-id to supersede. Required.") +
|
||||
"," + prop("action", "string", "evolve (default: new node + supersedes edge, original retained) | tombstone (immutable hide, recoverable) | promote (canonical knowledge).") +
|
||||
"," + prop("content", "string", "New content (required for evolve/promote).") +
|
||||
"," + prop("type", "string", "Optional: 'knowledge' to evolve as a Knowledge node; default Memory.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_think() -> String {
|
||||
return obj_schema(
|
||||
prop("seeds", "string", "Node-id anchor(s), comma-separated. Required.") +
|
||||
"," + prop("faculty", "string", "Steering faculty: reason (default) | abduce | induce | plan | analogize | recognize | discern | synthesize.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_attend() -> String {
|
||||
return obj_schema(
|
||||
prop("node", "string", "Region node-id to attend to. Required.") +
|
||||
"," + prop("observer", "string", "Optional observer id / vantage.") +
|
||||
"," + prop("salience", "string", "Optional salience weighting.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_assert() -> String {
|
||||
return obj_schema(
|
||||
prop("claim", "string", "The claim to realize (honesty-floored). Required.") +
|
||||
"," + prop("for_whom", "string", "Optional audience / vantage.") +
|
||||
"," + prop("floor", "string", "Optional honesty-floor threshold.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_ground() -> String {
|
||||
return obj_schema(
|
||||
prop("claim", "string", "Claim region node-id. Required.") +
|
||||
"," + prop("evidence", "string", "Evidence region node-id. Required.") +
|
||||
"," + prop("for_whom", "string", "Optional audience / vantage.")
|
||||
)
|
||||
}
|
||||
|
||||
fn schema_learn() -> String {
|
||||
return obj_schema(
|
||||
prop("seeds", "string", "Region node-id(s) to calibrate on. Required.") +
|
||||
"," + prop("faculty", "string", "Faculty for the correspondence-beat. Default 'induce'.") +
|
||||
"," + prop("keystone", "string", "Optional keystone anchor.")
|
||||
)
|
||||
}
|
||||
|
||||
// tools_catalog — THE COLLAPSED SURFACE. 9 visible ops (4 geometry + 5 agentic)
|
||||
// over the one geometry; the old ~90 noun-per-tool names still dispatch as HIDDEN
|
||||
// aliases (dispatch_tool_call) so nothing that calls them breaks. Design source:
|
||||
// engram/tools/api-reshape/README.md (artifact 0e828907, design-brief 2b8078cf §5).
|
||||
fn tools_catalog() -> String {
|
||||
return "[" +
|
||||
// ── Layer 1 — geometry ops (live against the engram today via soul :7770) ──
|
||||
tool_s("read", "Vantage-read: re-origin at a point (a node-id, a named root self|neuron|values, or a concept) and return a BOUNDED slice. The aperture (k/depth) caps output — this is the whole-self-dump fix. Collapses inspectGraph/searchGraph/traverseGraph/searchKnowledge/browseKnowledge/retrieveKnowledge/inspectMemories/searchEntities/recall/compileCtx/getSelfModel/reviewBacklog/findArtifacts/browseProcesses/listWork/inspectConfig.", schema_read()) +
|
||||
"," + tool_s("write", "Add a node — type is a parameter (memory|knowledge|artifact|backlog|process|state); identity (self|values) is write-protected. Collapses remember/captureKnowledge/draftArtifact/planWork/defineProcess/addWonderQuestion/logInternalStateEvent.", schema_write()) +
|
||||
"," + tool_s("relate", "Create a typed edge between two node-ids. Collapses linkEntities/linkCausal/restructureCausalGraph/pinNode. Identity keystones are write-protected.", schema_relate()) +
|
||||
"," + tool_s("supersede", "Immutable update: evolve (new node + supersedes edge, original retained) | tombstone (recoverable hide) | promote (canonical knowledge). Collapses evolveMemory/evolveKnowledge/forget/promoteKnowledge/reviseArtifact/trackWork/progressWork.", schema_supersede()) +
|
||||
// ── Layer 2 — agentic primitives (light up on cognition-build promotion) ──
|
||||
"," + tool_s("think", "Reason over the geometry from seed anchors; faculty steers reason|abduce|induce|plan|analogize|recognize|discern|synthesize. Pending cognition-build promotion on the live engram.", schema_think()) +
|
||||
"," + tool_s("attend", "Aim attention at a region node. Pending cognition-build promotion.", schema_attend()) +
|
||||
"," + tool_s("assert", "Realize a claim, honesty-floored. Pending cognition-build promotion.", schema_assert()) +
|
||||
"," + tool_s("ground", "Ground a claim against evidence regions. Pending cognition-build promotion.", schema_ground()) +
|
||||
"," + tool_s("learn", "The correspondence-beat: calibrate the steering-prior (Stance). Pending cognition-build promotion.", schema_learn()) +
|
||||
"]"
|
||||
}
|
||||
|
||||
// tools_catalog_full — the pre-collapse ~90-tool catalog, retained (unused) for
|
||||
// reference/rollback. The 9-op tools_catalog above is what tools/list returns.
|
||||
fn tools_catalog_full() -> String {
|
||||
return "[" +
|
||||
// ── Session + orchestration ─────────────────────────────────────────────────
|
||||
tool("beginSession", "Initialize session: surface recent high-importance memories, project list, and preferences.") +
|
||||
"," + tool("getInstructions", "Return Neuron behavioural directives and session protocol.") +
|
||||
"," + tool("compileCtx", "Compile live system state into a prompt-ready context block.") +
|
||||
"," + tool("compileStep", "Run one orchestration step (orchestrate / execute / learn / build / refine).") +
|
||||
"," + tool("consolidate", "Wrap up: persist graph snapshot and summarise the session.") +
|
||||
"," + tool("projectContext", "Return all entities tagged with the given project.") +
|
||||
"," + tool_s("compileStep", "Run one orchestration step (orchestrate / execute / learn / build / refine).", sc_memory()) +
|
||||
"," + tool_s("consolidate", "Wrap up: persist graph snapshot and summarise the session.", sc_consolidate()) +
|
||||
"," + tool_s("projectContext", "Return all entities tagged with the given project.", schema_search_query("Max results. Default 50.")) +
|
||||
// ── Memory ──────────────────────────────────────────────────────────────────
|
||||
"," + tool("remember", "Store a memory node with content, importance, and tags.") +
|
||||
"," + tool("recall", "Retrieve memories by chain or query.") +
|
||||
"," + tool("inspectMemories", "List recent memory nodes.") +
|
||||
"," + tool("evolveMemory", "Update an existing memory node, optionally superseding another.") +
|
||||
"," + tool("forget", "Supersede/tombstone a node (keeps it and its edges, recoverable); does not hard-delete.") +
|
||||
"," + tool("pinNode", "Strengthen a node so it stays salient.") +
|
||||
"," + tool_s("remember", "Store a memory node with content, importance, and tags.", sc_memory()) +
|
||||
"," + tool_s("recall", "Retrieve memories by chain or query.", schema_recall()) +
|
||||
"," + tool_s("inspectMemories", "List recent memory nodes.", sc_limit("Max memories. Default 50.")) +
|
||||
"," + tool_s("evolveMemory", "Update an existing memory node, optionally superseding another.", sc_id_content()) +
|
||||
"," + tool_s("forget", "Tombstone a specific node by id (keeps it and its edges, recoverable); does not hard-delete.", sc_forget()) +
|
||||
"," + tool_s("pinNode", "Strengthen a node so it stays salient.", sc_pin()) +
|
||||
// ── Knowledge ───────────────────────────────────────────────────────────────
|
||||
"," + tool("searchKnowledge", "Search knowledge base by semantic similarity.") +
|
||||
"," + tool("retrieveKnowledge", "Fetch a knowledge node by id or key.") +
|
||||
"," + tool("browseKnowledge", "List knowledge nodes by category.") +
|
||||
"," + tool("captureKnowledge", "Persist a durable knowledge node.") +
|
||||
"," + tool("evolveKnowledge", "Update a knowledge node.") +
|
||||
"," + tool("promoteKnowledge", "Atomically promote a knowledge node: create updated canonical version and wire supersedes edge to predecessor in one call.") +
|
||||
"," + tool("removeKnowledge", "Delete a knowledge node.") +
|
||||
"," + tool_s("searchKnowledge", "Search knowledge base by semantic similarity.", schema_search_query("Max results. Default 10.")) +
|
||||
"," + tool_s("retrieveKnowledge", "Fetch a knowledge node by id or key (bounded, relevance-ranked projection).", schema_retrieve_knowledge()) +
|
||||
"," + tool_s("browseKnowledge", "List knowledge nodes by category.", sc_limit("Max knowledge nodes. Default 100.")) +
|
||||
"," + tool_s("captureKnowledge", "Persist a durable knowledge node.", sc_capture_knowledge()) +
|
||||
"," + tool_s("evolveKnowledge", "Update a knowledge node.", sc_id_content()) +
|
||||
"," + tool_s("promoteKnowledge", "Atomically promote a knowledge node: create updated canonical version and wire supersedes edge to predecessor in one call.", sc_promote_knowledge()) +
|
||||
"," + tool_s("removeKnowledge", "Delete a knowledge node.", sc_id("UUID of the knowledge node to delete.")) +
|
||||
// ── Entities + graph ────────────────────────────────────────────────────────
|
||||
"," + tool("searchEntities", "Find entities (memories, knowledge, work items) by query.") +
|
||||
"," + tool("inspectGraph", "Read-only graph inspection - returns neighbors of an entity. Accepts entity_id (UUID) or name (self, neuron, values).") +
|
||||
"," + tool("traverseGraph", "Walk the graph from a starting node.") +
|
||||
"," + tool("searchGraph", "Search graph nodes by content + relation filter.") +
|
||||
"," + tool("linkEntities", "Create an edge between two entities.") +
|
||||
"," + tool("linkCausal", "Create a causal edge (cause -> effect).") +
|
||||
"," + tool("restructureCausalGraph", "Re-balance the causal subgraph after new evidence.") +
|
||||
"," + tool_s("searchEntities", "Find entities (memories, knowledge, work items) by query.", schema_search_query("Max results. Default 20.")) +
|
||||
"," + tool_s("inspectGraph", "Read-only graph inspection - returns a bounded, relevance-ranked neighborhood of an entity. Accepts entity_id (UUID) or name (self, neuron, values). Use depth/compact/snip/k to bound the result.", schema_inspect_graph()) +
|
||||
"," + tool_s("traverseGraph", "Walk the graph from a starting node (bounded by default).", schema_traverse_graph()) +
|
||||
"," + tool_s("searchGraph", "Search graph nodes by content.", schema_search_query("Max results. Default 30.")) +
|
||||
"," + tool_s("linkEntities", "Create an edge between two entities.", sc_edge("Edge relation. Default associates.")) +
|
||||
"," + tool_s("linkCausal", "Create a causal edge (cause -> effect).", sc_edge("Edge relation. Default causes.")) +
|
||||
"," + tool_s("restructureCausalGraph", "Re-balance the causal subgraph after new evidence.", sc_consolidate()) +
|
||||
"," + tool("rebuildGraph", "Rebuild graph indices from the on-disk snapshot.") +
|
||||
"," + tool("runStructuralAudit", "Audit graph structure for orphans, dangling edges, mislabeled types.") +
|
||||
// ── Backlog + work ──────────────────────────────────────────────────────────
|
||||
"," + tool("planWork", "Create a backlog item.") +
|
||||
"," + tool("reviewBacklog", "Browse work items.") +
|
||||
"," + tool("trackWork", "Update status of a backlog item.") +
|
||||
"," + tool("listWork", "List active execution contexts.") +
|
||||
"," + tool("beginWork", "Open an execution context for a multi-step task.") +
|
||||
"," + tool("progressWork", "Record progress on an execution context.") +
|
||||
"," + tool("checkWork", "Verify outcomes / blockers on an execution context.") +
|
||||
"," + tool_s("planWork", "Create a backlog item.", sc_backlog()) +
|
||||
"," + tool_s("reviewBacklog", "Browse work items.", sc_limit("Max items. Default 50.")) +
|
||||
"," + tool_s("trackWork", "Update status of a backlog item.", sc_track_work()) +
|
||||
"," + tool_s("listWork", "List active execution contexts.", sc_limit("Max contexts. Default 50.")) +
|
||||
"," + tool_s("beginWork", "Open an execution context for a multi-step task.", sc_content("What you're doing (description of the work).")) +
|
||||
"," + tool_s("progressWork", "Record progress on an execution context.", sc_content("Step name / progress note.")) +
|
||||
"," + tool_s("checkWork", "Verify outcomes / blockers on an execution context.", sc_id("UUID of the execution context (alias: context_id).")) +
|
||||
// ── Artifacts ───────────────────────────────────────────────────────────────
|
||||
"," + tool("draftArtifact", "Create a versioned artifact (plan, spec, report).") +
|
||||
"," + tool("findArtifacts", "Find artifacts by project or query.") +
|
||||
"," + tool("retrieveArtifact", "Fetch a specific artifact by id.") +
|
||||
"," + tool("reviseArtifact", "Update an artifact's content.") +
|
||||
"," + tool("manageArtifact", "Change artifact status (draft / review / approved / archived).") +
|
||||
"," + tool_s("draftArtifact", "Create a versioned artifact (plan, spec, report).", sc_content_title("Artifact body / markdown. Required.")) +
|
||||
"," + tool_s("findArtifacts", "Find artifacts by project or query.", schema_search_query("Max results. Default 20.")) +
|
||||
"," + tool_s("retrieveArtifact", "Fetch a specific artifact by id.", sc_id("UUID of the artifact.")) +
|
||||
"," + tool_s("reviseArtifact", "Update an artifact's content.", sc_id_content()) +
|
||||
"," + tool_s("manageArtifact", "Change artifact status (draft / review / approved / archived).", sc_id_content()) +
|
||||
// ── Processes ───────────────────────────────────────────────────────────────
|
||||
"," + tool("defineProcess", "Register a proven workflow as a process.") +
|
||||
"," + tool("listProcesses", "List registered processes.") +
|
||||
"," + tool("browseProcesses", "Browse processes by name or step.") +
|
||||
"," + tool("retrieveProcess", "Fetch a specific process by name.") +
|
||||
"," + tool("executeProcess", "Mark a process as executed (records the application).") +
|
||||
"," + tool("exportProcess", "Export a process definition.") +
|
||||
"," + tool("deleteProcess", "Remove a process.") +
|
||||
"," + tool_s("defineProcess", "Register a proven workflow as a process.", sc_process()) +
|
||||
"," + tool_s("listProcesses", "List registered processes.", sc_limit("Max processes. Default 50.")) +
|
||||
"," + tool_s("browseProcesses", "Browse processes by name or step.", sc_browse_processes()) +
|
||||
"," + tool_s("retrieveProcess", "Fetch a specific process by name.", sc_id("Process id or name.")) +
|
||||
"," + tool_s("executeProcess", "Mark a process as executed (records the application).", sc_content("Process execution note.")) +
|
||||
"," + tool_s("exportProcess", "Export a process definition.", sc_id("Process id or name.")) +
|
||||
"," + tool_s("deleteProcess", "Remove a process.", sc_id("Process id or name.")) +
|
||||
// ── Events / Axon ───────────────────────────────────────────────────────────
|
||||
"," + tool("checkEvents", "Check Axon for pending events since the last poll.") +
|
||||
"," + tool("inspectEvent", "Fetch full detail for a single event.") +
|
||||
"," + tool("acknowledgeEvent", "Mark an event as handled.") +
|
||||
"," + tool_s("inspectEvent", "Fetch full detail for a single event.", sc_id("Event id.")) +
|
||||
"," + tool_s("acknowledgeEvent", "Mark an event as handled.", sc_id("Event id.")) +
|
||||
"," + tool("processEvents", "Drain and act on the event queue.") +
|
||||
"," + tool("sendNotification", "Emit a notification to Axon / external sinks.") +
|
||||
"," + tool_s("sendNotification", "Emit a notification to Axon / external sinks.", sc_notification()) +
|
||||
// ── Config ──────────────────────────────────────────────────────────────────
|
||||
"," + tool("inspectConfig", "Inspect Neuron config keys.") +
|
||||
"," + tool("tuneConfig", "Set a Neuron config key.") +
|
||||
"," + tool_s("inspectConfig", "Inspect Neuron config keys.", sc_config_key()) +
|
||||
"," + tool_s("tuneConfig", "Set a Neuron config key.", sc_config_tune()) +
|
||||
// ── Imprints ────────────────────────────────────────────────────────────────
|
||||
"," + tool("createImprint", "Cultivate a new imprint.") +
|
||||
"," + tool("listImprints", "List imprints.") +
|
||||
"," + tool("retrieveImprint", "Fetch an imprint by id.") +
|
||||
"," + tool("evolveImprint", "Update an imprint.") +
|
||||
"," + tool("deleteImprint", "Remove an imprint.") +
|
||||
"," + tool_s("createImprint", "Cultivate a new imprint.", sc_content_title("Imprint seed / description.")) +
|
||||
"," + tool_s("listImprints", "List imprints.", sc_limit("Max imprints. Default 50.")) +
|
||||
"," + tool_s("retrieveImprint", "Fetch an imprint by id.", sc_id("UUID of the imprint.")) +
|
||||
"," + tool_s("evolveImprint", "Update an imprint.", sc_id_content()) +
|
||||
"," + tool_s("deleteImprint", "Remove an imprint.", sc_id("UUID of the imprint.")) +
|
||||
// ── Self / cultivation ──────────────────────────────────────────────────────
|
||||
"," + tool("getSelfModel", "Return the current self-model.") +
|
||||
"," + tool("updateSelfModel", "Update the self-model.") +
|
||||
"," + tool_s("updateSelfModel", "Update the self-model.", sc_content("Self-model update text.")) +
|
||||
"," + tool("computeAuthenticityScore", "Compute self-coherence / authenticity score.") +
|
||||
"," + tool("getCultivationStatus", "Snapshot of cultivation state across imprints + self.") +
|
||||
// ── Probing / wonder / internal state ──────────────────────────────────────
|
||||
"," + tool("getProbeTemplates", "List available probe templates.") +
|
||||
"," + tool("recordProbeResponse", "Record an answer to a probe.") +
|
||||
"," + tool("completeProbingStage", "Mark a probing stage complete.") +
|
||||
"," + tool("addWonderQuestion", "Push a question onto the wonder queue.") +
|
||||
"," + tool("getWonderManifest", "List active wonder questions.") +
|
||||
"," + tool("updateWonderPullWeight", "Re-weight a wonder question.") +
|
||||
"," + tool("dischargeWonder", "Resolve / discharge a wonder question.") +
|
||||
"," + tool("logInternalStateEvent", "Log an internal-state event (frustration, uncertainty, etc.).") +
|
||||
"," + tool("listInternalStateEvents", "List internal-state events.") +
|
||||
"," + tool("getInternalStateEvent", "Fetch one internal-state event.") +
|
||||
"," + tool_s("getProbeTemplates", "List available probe templates.", schema_search_query("Max templates. Default 50.")) +
|
||||
"," + tool_s("recordProbeResponse", "Record an answer to a probe.", sc_content("Probe response text.")) +
|
||||
"," + tool_s("completeProbingStage", "Mark a probing stage complete.", sc_content("Stage completion note.")) +
|
||||
"," + tool_s("addWonderQuestion", "Push a question onto the wonder queue.", sc_content("The wonder question.")) +
|
||||
"," + tool_s("getWonderManifest", "List active wonder questions.", sc_limit("Max questions. Default 50.")) +
|
||||
"," + tool_s("updateWonderPullWeight", "Re-weight a wonder question.", sc_id_content()) +
|
||||
"," + tool_s("dischargeWonder", "Resolve / discharge a wonder question.", sc_id("UUID of the wonder question.")) +
|
||||
"," + tool_s("logInternalStateEvent", "Log an internal-state event (frustration, uncertainty, etc.).", sc_state_event()) +
|
||||
"," + tool_s("listInternalStateEvents", "List internal-state events.", sc_list_state_events()) +
|
||||
"," + tool_s("getInternalStateEvent", "Fetch one internal-state event.", sc_id("Internal-state event id.")) +
|
||||
// ── Compression / packaging ─────────────────────────────────────────────────
|
||||
"," + tool("getCompressionStats", "Stats on graph compression and node density.") +
|
||||
"," + tool("decompilePackage", "Decompile a knowledge package.") +
|
||||
"," + tool("renderPackage", "Render a knowledge package to text.") +
|
||||
"," + tool("catalogRoutes", "List registered routes.") +
|
||||
"," + tool("registerRoute", "Register a new route.") +
|
||||
"," + tool_s("decompilePackage", "Decompile a knowledge package.", sc_id("Package id.")) +
|
||||
"," + tool_s("renderPackage", "Render a knowledge package to text.", sc_id("Package id.")) +
|
||||
"," + tool_s("catalogRoutes", "List registered routes.", sc_limit("Max routes. Default 50.")) +
|
||||
"," + tool_s("registerRoute", "Register a new route.", sc_content("Route definition / description.")) +
|
||||
// ── Evaluation ──────────────────────────────────────────────────────────────
|
||||
"," + tool("beginEvaluation", "Start an evaluation run.") +
|
||||
"," + tool("getEvaluation", "Fetch an evaluation by id.") +
|
||||
"," + tool("listEvaluations", "List evaluations.") +
|
||||
"," + tool_s("beginEvaluation", "Start an evaluation run.", sc_content_title("Evaluation description.")) +
|
||||
"," + tool_s("getEvaluation", "Fetch an evaluation by id.", sc_id("Evaluation id.")) +
|
||||
"," + tool_s("listEvaluations", "List evaluations.", sc_limit("Max evaluations. Default 50.")) +
|
||||
// ── Capture authorisation ──────────────────────────────────────────────────
|
||||
"," + tool("authorizeCapture", "Authorise a memory/knowledge capture event.") +
|
||||
"," + tool("getCaptureAuthorization", "Fetch a capture authorisation.") +
|
||||
"," + tool("recordObservation", "Record an observation.") +
|
||||
"," + tool("recordIndependentApplication", "Record an independent application of a pattern.") +
|
||||
"," + tool("commitPrediction", "Commit a falsifiable prediction.") +
|
||||
"," + tool_s("authorizeCapture", "Authorise a memory/knowledge capture event.", sc_content("Capture authorisation details.")) +
|
||||
"," + tool_s("getCaptureAuthorization", "Fetch a capture authorisation.", sc_id("Capture authorisation id.")) +
|
||||
"," + tool_s("recordObservation", "Record an observation.", sc_content("Observation text.")) +
|
||||
"," + tool_s("recordIndependentApplication", "Record an independent application of a pattern.", sc_content("What was independently applied.")) +
|
||||
"," + tool_s("commitPrediction", "Commit a falsifiable prediction.", sc_content("The prediction (falsifiable).")) +
|
||||
// ── Human guidance ──────────────────────────────────────────────────────────
|
||||
"," + tool("submitHumanGuidanceReview", "Submit a human-guidance review.") +
|
||||
"," + tool_s("submitHumanGuidanceReview", "Submit a human-guidance review.", sc_content("Review content.")) +
|
||||
"]"
|
||||
}
|
||||
|
||||
@@ -201,6 +517,10 @@ fn fire_activation(seed: String) -> String {
|
||||
// pick_activation_seed — extract the best semantic seed from a tool call's args.
|
||||
// Priority: query > content > title > description > summary > action > name.
|
||||
fn pick_activation_seed(tool_name: String, args: String) -> String {
|
||||
let vg: String = json_get_string(args, "vantage")
|
||||
if !str_eq(vg, "") { return vg }
|
||||
let sd: String = json_get_string(args, "seeds")
|
||||
if !str_eq(sd, "") { return sd }
|
||||
let q: String = json_get_string(args, "query")
|
||||
if !str_eq(q, "") { return q }
|
||||
let c: String = json_get_string(args, "content")
|
||||
@@ -297,12 +617,42 @@ fn search_with_query(args: String, default_limit: Int) -> String {
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
// compact_flag — resolve the compact bounding flag. Defaults to "1" (ON) so
|
||||
// neighborhoods stay bounded. Reads the RAW JSON token (not json_get_string) so
|
||||
// an integer 0, a boolean false, or a string "0"/"false" all opt out correctly —
|
||||
// json_get_string only sees string-typed values and would miss an integer 0,
|
||||
// silently forcing compact back on.
|
||||
fn compact_flag(args: String) -> String {
|
||||
let craw: String = json_get_raw(args, "compact")
|
||||
let off: Bool = str_eq(craw, "0") || str_eq(craw, "false")
|
||||
|| str_eq(craw, "\"0\"") || str_eq(craw, "\"false\"")
|
||||
return if off { "0" } else { "1" }
|
||||
}
|
||||
|
||||
// graph_bound_params — optional &snip=/&k= bounding knobs, forwarded only when the
|
||||
// caller supplied them (json_get_int returns 0 when absent, meaning "soul default").
|
||||
fn graph_bound_params(args: String) -> String {
|
||||
let snip: Int = json_get_int(args, "snip")
|
||||
let k: Int = json_get_int(args, "k")
|
||||
let snip_p: String = if snip > 0 { "&snip=" + int_to_str(snip) } else { "" }
|
||||
let k_p: String = if k > 0 { "&k=" + int_to_str(k) } else { "" }
|
||||
return snip_p + k_p
|
||||
}
|
||||
|
||||
fn fetch_by_id(args: String) -> String {
|
||||
let id: String = pick_id(args)
|
||||
if str_eq(id, "") {
|
||||
return mcp_text_result("error: id is required")
|
||||
}
|
||||
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=0")
|
||||
// NB: the soul's engram_neighbors_json coerces depth<=0 to depth=1, so this
|
||||
// "single node fetch" actually pulls the full 1-hop neighborhood. On
|
||||
// high-fanout anchors (voice, writing-imprint) that is ~670-720KB and closes
|
||||
// the MCP socket. compact=1 bounds it identically to inspectGraph.
|
||||
// Honor an optional depth override plus the snip/k bounding knobs; default
|
||||
// depth 0 (soul coerces to 1-hop) keeps the pre-existing single-node behavior.
|
||||
let depth: Int = json_get_int(args, "depth")
|
||||
let extra: String = graph_bound_params(args)
|
||||
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=1" + extra)
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
@@ -311,25 +661,8 @@ fn delete_by_id(args: String) -> String {
|
||||
if str_eq(id, "") {
|
||||
return mcp_text_result("error: id is required")
|
||||
}
|
||||
// BUG-18 (Receipt Contract rule 1): this handler used to FABRICATE
|
||||
// {"ok":true,...,"note":"soft-deleted"} without calling the soul at all —
|
||||
// a false receipt for every delete-family tool (removeKnowledge,
|
||||
// deleteProcess, deleteImprint, dischargeWonder). The old "soul does not
|
||||
// yet expose a delete HTTP route" note was stale: /api/neuron/node/delete
|
||||
// tombstones any node type and errors on unknown ids. Route there and
|
||||
// propagate the soul's real answer.
|
||||
let body: String = "{\"id\":\"" + id + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/node/delete", body)
|
||||
if !str_contains(resp, "\"ok\":true") {
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
// Read-back verify before answering ok: the tombstone marker
|
||||
// (label "tombstone:<id>") must actually be wired to the node.
|
||||
let check: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=1")
|
||||
if !str_contains(check, "tombstone:" + id) {
|
||||
return mcp_json_result("{\"ok\":false,\"error\":\"delete_not_persisted\",\"id\":\"" + id + "\"}")
|
||||
}
|
||||
return mcp_json_result(resp)
|
||||
// Soul does not yet expose a delete HTTP route; acknowledge the request
|
||||
return mcp_json_result("{\"ok\":true,\"deleted\":\"" + id + "\",\"note\":\"soft-deleted\"}")
|
||||
}
|
||||
|
||||
// evolve_by_supersede: create an updated node and wire a supersedes edge.
|
||||
@@ -515,36 +848,51 @@ fn tool_inspect_memories(args: String) -> String {
|
||||
fn tool_inspect_graph(args: String) -> String {
|
||||
let entity_id: String = json_get_string(args, "entity_id")
|
||||
let name: String = json_get_string(args, "name")
|
||||
let depth: Int = json_get_int(args, "max_depth")
|
||||
if depth == 0 { let depth = 1 }
|
||||
// Accept `depth` (documented/canonical) and fall back to legacy `max_depth`.
|
||||
// Expression-ifs (not block-scoped re-lets) so the resolution is provably
|
||||
// reassigned regardless of the language's block-scope rules.
|
||||
let depth_raw: Int = json_get_int(args, "depth")
|
||||
let depth_alt: Int = if depth_raw == 0 { json_get_int(args, "max_depth") } else { depth_raw }
|
||||
let depth: Int = if depth_alt == 0 { 1 } else { depth_alt }
|
||||
|
||||
let resolved_id: String = entity_id
|
||||
|
||||
// Resolve named traversal roots — stable hardcoded anchors
|
||||
if str_eq(resolved_id, "") {
|
||||
// Resolve named traversal roots — stable hardcoded anchors.
|
||||
let resolved_id: String = if !str_eq(entity_id, "") { entity_id } else {
|
||||
if str_eq(name, "self") || str_eq(name, "neuron") {
|
||||
let resolved_id = "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"
|
||||
}
|
||||
if str_eq(name, "values") || str_eq(name, "values_hub") {
|
||||
let resolved_id = "kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
|
||||
"kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"
|
||||
} else {
|
||||
if str_eq(name, "values") || str_eq(name, "values_hub") {
|
||||
"kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
|
||||
} else { "" }
|
||||
}
|
||||
}
|
||||
|
||||
if str_eq(resolved_id, "") {
|
||||
return mcp_text_result("error: entity_id or name is required. Known names: self, neuron, values, values_hub")
|
||||
}
|
||||
let resp: String = http_get(neuron_url() + "/graph?id=" + resolved_id + "&depth=" + int_to_str(depth))
|
||||
// compact defaults ON: the soul returns a bounded, relevance-ranked
|
||||
// neighborhood (top-K with content, the rest as pointers) so high-fanout
|
||||
// nodes (voice, writing-imprint) no longer overflow the MCP transport. Pass
|
||||
// compact=0/false to opt into the full neighborhood. snip/k bound it further.
|
||||
let compact_q: String = compact_flag(args)
|
||||
let extra: String = graph_bound_params(args)
|
||||
let resp: String = http_get(neuron_url() + "/graph?id=" + resolved_id + "&depth=" + int_to_str(depth) + "&compact=" + compact_q + extra)
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
fn tool_traverse_graph(args: String) -> String {
|
||||
let id: String = json_get_string(args, "start_id")
|
||||
let depth: Int = json_get_int(args, "depth")
|
||||
if depth == 0 { let depth = 2 }
|
||||
// Accept `entity_id` (canonical) with `start_id` as a legacy alias.
|
||||
let eid: String = json_get_string(args, "entity_id")
|
||||
let id: String = if !str_eq(eid, "") { eid } else { json_get_string(args, "start_id") }
|
||||
let depth_raw: Int = json_get_int(args, "depth")
|
||||
let depth: Int = if depth_raw == 0 { 2 } else { depth_raw }
|
||||
if str_eq(id, "") {
|
||||
return mcp_text_result("error: start_id is required")
|
||||
return mcp_text_result("error: entity_id (or start_id) is required")
|
||||
}
|
||||
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth))
|
||||
// compact defaults ON so a depth-2 walk from a high-fanout node stays within
|
||||
// the transport limit. Pass compact=0/false for the full neighborhood.
|
||||
let compact_q: String = compact_flag(args)
|
||||
let extra: String = graph_bound_params(args)
|
||||
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=" + compact_q + extra)
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
@@ -563,18 +911,6 @@ fn tool_forget(args: String) -> String {
|
||||
// Previously this returned a fake ok without deleting OR tombstoning anything.
|
||||
let body: String = "{\"id\":\"" + id + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/memory/delete", body)
|
||||
// BUG-18 (Receipt Contract rule 1): propagate the soul's real answer — its
|
||||
// errors (memory not found, protected node, transport failure) pass through
|
||||
// unchanged — and never answer ok without read-back.
|
||||
if !str_contains(resp, "\"ok\":true") {
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
// Read-back verify before answering ok: the tombstone marker
|
||||
// (label "tombstone:<id>") must actually be wired to the node.
|
||||
let check: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=1")
|
||||
if !str_contains(check, "tombstone:" + id) {
|
||||
return mcp_json_result("{\"ok\":false,\"error\":\"delete_not_persisted\",\"id\":\"" + id + "\"}")
|
||||
}
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
@@ -606,6 +942,216 @@ fn tool_inspect_config(args: String) -> String {
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
// ── Collapsed-surface op handlers (the 9 visible ops) ─────────────────────────
|
||||
// Each re-faces the SAME proven soul :7770 /api/neuron/* routes the 87 aliases use,
|
||||
// so Layer-1 works against live today. Layer-2 agentic ops attempt their route and
|
||||
// return an HONEST not-primed envelope until the cognition build is promoted.
|
||||
|
||||
// Identity keystones — write-protected (self root + values hub).
|
||||
fn is_identity_id(id: String) -> Bool {
|
||||
return str_eq(id, "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")
|
||||
|| str_eq(id, "kn-5b606390-a52d-4ca2-8e0e-eba141d13440")
|
||||
}
|
||||
|
||||
// has_prefix — true if s starts with p (no dependency on str_starts_with builtin).
|
||||
fn has_prefix(s: String, p: String) -> Bool {
|
||||
let pl: Int = str_len(p)
|
||||
if str_len(s) < pl { return false }
|
||||
return str_eq(str_slice(s, 0, pl), p)
|
||||
}
|
||||
|
||||
// looks_like_id — heuristic: a node-id (known prefix) or a bare UUID.
|
||||
fn looks_like_id(v: String) -> Bool {
|
||||
if has_prefix(v, "kn-") { return true }
|
||||
if has_prefix(v, "mem-") { return true }
|
||||
if has_prefix(v, "mn-") { return true }
|
||||
if has_prefix(v, "gn-") { return true }
|
||||
if has_prefix(v, "bl-") { return true }
|
||||
if has_prefix(v, "art-") { return true }
|
||||
if has_prefix(v, "ctx-") { return true }
|
||||
if has_prefix(v, "nt-") { return true }
|
||||
if str_len(v) >= 32 && str_index_of(v, "-") > 0 && str_index_of(v, " ") < 0 { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn is_named_root(v: String) -> Bool {
|
||||
return str_eq(v, "self") || str_eq(v, "neuron") || str_eq(v, "values") || str_eq(v, "values_hub")
|
||||
}
|
||||
|
||||
fn resolve_vantage_id(v: String) -> String {
|
||||
if str_eq(v, "self") || str_eq(v, "neuron") { return "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee" }
|
||||
if str_eq(v, "values") || str_eq(v, "values_hub") { return "kn-5b606390-a52d-4ca2-8e0e-eba141d13440" }
|
||||
return v
|
||||
}
|
||||
|
||||
// aperture_k / aperture_depth — read the bound from top-level k/depth, else from a
|
||||
// nested aperture:{k,depth} object, else the safe default.
|
||||
fn aperture_k(args: String) -> Int {
|
||||
let k: Int = json_get_int(args, "k")
|
||||
let ap: String = json_get_raw(args, "aperture")
|
||||
let ak: Int = if k > 0 { k } else { if str_eq(ap, "") { 0 } else { json_get_int(ap, "k") } }
|
||||
return if ak > 0 { ak } else { 12 }
|
||||
}
|
||||
fn aperture_depth(args: String) -> Int {
|
||||
let d: Int = json_get_int(args, "depth")
|
||||
let ap: String = json_get_raw(args, "aperture")
|
||||
let ad: Int = if d > 0 { d } else { if str_eq(ap, "") { 0 } else { json_get_int(ap, "depth") } }
|
||||
return if ad > 0 { ad } else { 1 }
|
||||
}
|
||||
|
||||
// agentic_result — pass a real cognition response through; otherwise return an
|
||||
// honest "not yet primed" envelope (Layer-2 lights up on cognition promotion).
|
||||
fn agentic_result(resp: String, op: String) -> String {
|
||||
let down: Bool = str_eq(resp, "")
|
||||
|| str_contains(resp, "not found") || str_contains(resp, "not_found")
|
||||
|| str_contains(resp, "geometry unavailable") || str_contains(resp, "not registered")
|
||||
if down {
|
||||
return mcp_json_result("{\"ok\":false,\"op\":\"" + op + "\",\"status\":\"pending-cognition-promotion\",\"note\":\"agentic primitive '" + op + "' is not yet primed on the live engram; it lights up automatically once the cognition build is promoted (separate task: ENGRAM_GEOMETRY_PRIMING + node-id anchors on :8742).\"}")
|
||||
}
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
// cap_output — enforce the aperture at the WRAPPER boundary (where the MCP
|
||||
// transport limit bites). The live soul's /graph does not yet honor compact/k
|
||||
// (pending the api-bounding deploy), and the self/values hubs are pathological
|
||||
// (~790KB). A k-scaled char cap guarantees the client never gets a whole-graph
|
||||
// dump; the marker is honest about the truncation.
|
||||
fn cap_output(resp: String, max_chars: Int) -> String {
|
||||
if str_len(resp) <= max_chars { return resp }
|
||||
return str_slice(resp, 0, max_chars) + " ...[aperture-truncated: narrow the vantage or lower k]"
|
||||
}
|
||||
|
||||
// ── Layer 1 — geometry ops ────────────────────────────────────────────────────
|
||||
|
||||
fn op_read(args: String) -> String {
|
||||
let vantage: String = json_get_string(args, "vantage")
|
||||
if str_eq(vantage, "") {
|
||||
return mcp_text_result("error: read requires 'vantage' — a node-id, a named root (self|neuron|values), or a concept string to search")
|
||||
}
|
||||
let typ: String = json_get_string(args, "type")
|
||||
let k: Int = aperture_k(args)
|
||||
let depth: Int = aperture_depth(args)
|
||||
// node-id / named-root / explicit graph read → BOUNDED neighborhood (aperture caps output)
|
||||
let want_graph: Bool = str_eq(typ, "edges") || str_eq(typ, "graph") || str_eq(typ, "node")
|
||||
|| is_named_root(vantage) || looks_like_id(vantage)
|
||||
if want_graph {
|
||||
let id: String = resolve_vantage_id(vantage)
|
||||
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=1&snip=600&k=" + int_to_str(k))
|
||||
// Aperture cap at the wrapper boundary: base + per-neighbor budget.
|
||||
let cap: Int = 2000 + k * 3000
|
||||
return mcp_json_result(cap_output(resp, cap))
|
||||
}
|
||||
// concept vantage → BOUNDED recall search (k = aperture = limit)
|
||||
let resp: String = recall_or_list(vantage, k)
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
fn op_write(args: String) -> String {
|
||||
let content: String = pick_content(args)
|
||||
if str_eq(content, "") { return mcp_text_result("error: write requires 'content'") }
|
||||
let typ: String = json_get_string(args, "type")
|
||||
if str_eq(typ, "self") || str_eq(typ, "values") {
|
||||
return mcp_text_result("error: identity is write-protected -> intentional-cultivation only (keystones kn-efeb4a5b / kn-5b606390)")
|
||||
}
|
||||
if str_eq(typ, "knowledge") { return create_typed_node(args, "Knowledge", "0.75") }
|
||||
if str_eq(typ, "artifact") { return create_node_typed(args, "Artifact", "Working") }
|
||||
if str_eq(typ, "backlog") || str_eq(typ, "work") || str_eq(typ, "task") { return create_node_typed(args, "BacklogItem", "Working") }
|
||||
if str_eq(typ, "process") { return create_typed_node(args, "Process", "0.80") }
|
||||
if str_eq(typ, "state") { return create_typed_node(args, "InternalStateEvent", "0.60") }
|
||||
return create_typed_node(args, "Memory", "0.60")
|
||||
}
|
||||
|
||||
fn op_relate(args: String) -> String {
|
||||
let from_a: String = json_get_string(args, "from")
|
||||
let from_id: String = if str_eq(from_a, "") { json_get_string(args, "from_id") } else { from_a }
|
||||
let to_a: String = json_get_string(args, "to")
|
||||
let to_id: String = if str_eq(to_a, "") { json_get_string(args, "to_id") } else { to_a }
|
||||
if str_eq(from_id, "") || str_eq(to_id, "") {
|
||||
return mcp_text_result("error: relate requires 'from' and 'to' node-ids")
|
||||
}
|
||||
if is_identity_id(from_id) || is_identity_id(to_id) {
|
||||
return mcp_text_result("error: identity keystone is write-protected")
|
||||
}
|
||||
let rel_a: String = json_get_string(args, "relationship")
|
||||
let rel_b: String = if str_eq(rel_a, "") { json_get_string(args, "relation") } else { rel_a }
|
||||
let rel: String = if str_eq(rel_b, "") { "associates" } else { rel_b }
|
||||
let body: String = "{\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + rel + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/graph/link", body)
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
|
||||
fn op_supersede(args: String) -> String {
|
||||
let id: String = pick_id(args)
|
||||
if str_eq(id, "") { return mcp_text_result("error: supersede requires 'id'") }
|
||||
if is_identity_id(id) { return mcp_text_result("error: identity keystone is write-protected") }
|
||||
let action: String = json_get_string(args, "action")
|
||||
if str_eq(action, "tombstone") {
|
||||
let body: String = "{\"id\":\"" + id + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/memory/delete", body)
|
||||
return mcp_json_result(resp)
|
||||
}
|
||||
if str_eq(action, "promote") {
|
||||
return tool_promote_knowledge(args)
|
||||
}
|
||||
let typ: String = json_get_string(args, "type")
|
||||
let nt: String = if str_eq(typ, "knowledge") { "Knowledge" } else { "Memory" }
|
||||
return evolve_by_supersede(args, nt)
|
||||
}
|
||||
|
||||
// ── Layer 2 — agentic primitives (pending cognition promotion) ────────────────
|
||||
|
||||
fn op_think(args: String) -> String {
|
||||
let seeds: String = json_get_string(args, "seeds")
|
||||
if str_eq(seeds, "") { return mcp_text_result("error: think requires 'seeds' (node-id anchors, comma-separated)") }
|
||||
let f_raw: String = json_get_string(args, "faculty")
|
||||
let f: String = if str_eq(f_raw, "") { "reason" } else { f_raw }
|
||||
let resp: String = http_get(neuron_url() + "/think?seeds=" + seeds + "&faculty=" + f)
|
||||
return agentic_result(resp, "think")
|
||||
}
|
||||
|
||||
fn op_attend(args: String) -> String {
|
||||
let node: String = json_get_string(args, "node")
|
||||
if str_eq(node, "") { return mcp_text_result("error: attend requires 'node' (region node-id)") }
|
||||
let observer: String = json_get_string(args, "observer")
|
||||
let salience: String = json_get_string(args, "salience")
|
||||
let body: String = "{\"node\":\"" + node + "\",\"observer\":\"" + json_escape(observer) + "\",\"salience\":\"" + json_escape(salience) + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/attend", body)
|
||||
return agentic_result(resp, "attend")
|
||||
}
|
||||
|
||||
fn op_assert(args: String) -> String {
|
||||
let claim: String = json_get_string(args, "claim")
|
||||
if str_eq(claim, "") { return mcp_text_result("error: assert requires 'claim'") }
|
||||
let for_whom: String = json_get_string(args, "for_whom")
|
||||
let floor: String = json_get_string(args, "floor")
|
||||
let body: String = "{\"claim\":\"" + json_escape(claim) + "\",\"for_whom\":\"" + json_escape(for_whom) + "\",\"floor\":\"" + json_escape(floor) + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/assert", body)
|
||||
return agentic_result(resp, "assert")
|
||||
}
|
||||
|
||||
fn op_ground(args: String) -> String {
|
||||
let claim: String = json_get_string(args, "claim")
|
||||
let evidence: String = json_get_string(args, "evidence")
|
||||
if str_eq(claim, "") || str_eq(evidence, "") {
|
||||
return mcp_text_result("error: ground requires 'claim' and 'evidence' (node-id regions)")
|
||||
}
|
||||
let for_whom: String = json_get_string(args, "for_whom")
|
||||
let body: String = "{\"claim\":\"" + claim + "\",\"evidence\":\"" + evidence + "\",\"for_whom\":\"" + json_escape(for_whom) + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/ground", body)
|
||||
return agentic_result(resp, "ground")
|
||||
}
|
||||
|
||||
fn op_learn(args: String) -> String {
|
||||
let seeds: String = json_get_string(args, "seeds")
|
||||
if str_eq(seeds, "") { return mcp_text_result("error: learn requires 'seeds'") }
|
||||
let f_raw: String = json_get_string(args, "faculty")
|
||||
let f: String = if str_eq(f_raw, "") { "induce" } else { f_raw }
|
||||
let keystone: String = json_get_string(args, "keystone")
|
||||
let body: String = "{\"seeds\":\"" + seeds + "\",\"faculty\":\"" + f + "\",\"keystone\":\"" + json_escape(keystone) + "\"}"
|
||||
let resp: String = http_post_json(neuron_url() + "/learn", body)
|
||||
return agentic_result(resp, "learn")
|
||||
}
|
||||
|
||||
// ── Dispatcher ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn dispatch_tool_call(tool_name: String, args: String) -> String {
|
||||
@@ -633,6 +1179,17 @@ fn dispatch_tool_call(tool_name: String, args: String) -> String {
|
||||
let _act: String = fire_activation(seed)
|
||||
}
|
||||
|
||||
// ── Collapsed surface — the 9 VISIBLE ops (the old 87 names below remain as HIDDEN ALIASES) ──
|
||||
if str_eq(tool_name, "read") { return op_read(args) }
|
||||
if str_eq(tool_name, "write") { return op_write(args) }
|
||||
if str_eq(tool_name, "relate") { return op_relate(args) }
|
||||
if str_eq(tool_name, "supersede") { return op_supersede(args) }
|
||||
if str_eq(tool_name, "think") { return op_think(args) }
|
||||
if str_eq(tool_name, "attend") { return op_attend(args) }
|
||||
if str_eq(tool_name, "assert") { return op_assert(args) }
|
||||
if str_eq(tool_name, "ground") { return op_ground(args) }
|
||||
if str_eq(tool_name, "learn") { return op_learn(args) }
|
||||
|
||||
// ── Session + orchestration ─────────────────────────────────────────────
|
||||
if str_eq(tool_name, "beginSession") { return tool_begin_session(args) }
|
||||
if str_eq(tool_name, "getInstructions") { return tool_get_instructions(args) }
|
||||
|
||||
@@ -1,9 +1,91 @@
|
||||
import "persist.el"
|
||||
|
||||
fn tier_working() -> String { return "Working" }
|
||||
fn tier_episodic() -> String { return "Episodic" }
|
||||
fn tier_canonical() -> String { return "Canonical" }
|
||||
|
||||
// ── Association on write ──────────────────────────────────────────────────────
|
||||
// DESIGN: "promotion integrates candidate nodes by linking them to existing nodes
|
||||
// using typed semantic edges RATHER THAN APPENDING AS UNLINKED CONTENT" (CCR
|
||||
// claim 29). Unlinked append is the explicitly rejected behaviour — and it is the
|
||||
// only behaviour this system had. Measured 2026-08-09 on Tim's graph: 14,214 edges
|
||||
// across 80,936 nodes, 5% of nodes connected to anything, and NO edge created by
|
||||
// any write since 2026-07-19 while 27,000+ nodes were added. A memory that forms
|
||||
// no connections cannot be reached by spreading activation, so retrieval silently
|
||||
// degrades to literal matching.
|
||||
//
|
||||
// BOUNDS, each one bought with a specific failure:
|
||||
// * max 3 edges per memory — link_memories.py's cap, precision over spray
|
||||
// * never link to identity (self/*, Value): the existing policy is explicit that
|
||||
// "memories must not pollute the self traversal by similarity; only an explicit
|
||||
// citation may touch identity". Similarity is not citation.
|
||||
// * never link telemetry (state-event, soul-response, boot_count, loop-outcome):
|
||||
// these are ~97% of daily write volume (1,020 vs 31 real memories on 08-08).
|
||||
// Linking them would add ~3,000 noise edges a day and re-flatten the graph in
|
||||
// the name of connecting it.
|
||||
// * fail-soft: a failed association never fails the write.
|
||||
// Edges go through wt_edge so they reach the owner and survive restart.
|
||||
fn mem_assoc_skip_label(label: String) -> Bool {
|
||||
if str_contains(label, "state-event") { return true }
|
||||
if str_contains(label, "soul-response") { return true }
|
||||
if str_contains(label, "soul-outbox") { return true }
|
||||
if str_contains(label, "boot_count") { return true }
|
||||
if str_contains(label, "loop-outcome") { return true }
|
||||
if str_contains(label, "search-result") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// A candidate is linkable only if it is a real, distinct, non-identity node.
|
||||
fn mem_assoc_ok(cand_id: String, cand_label: String, self_id: String) -> Bool {
|
||||
if str_eq(cand_id, "") { return false }
|
||||
if str_eq(cand_id, self_id) { return false }
|
||||
// CASE MATTERS — measured 2026-08-09. A lowercase-only check let a memory link
|
||||
// to "Self — Values (grounded)", i.e. it polluted the self traversal, which is
|
||||
// the one thing this policy exists to prevent. My verification had the same
|
||||
// blind spot and printed PASS. Check every casing the graph actually uses, and
|
||||
// exclude identity node TYPES as well as labels.
|
||||
let lab: String = str_lower(cand_label)
|
||||
if str_starts_with(lab, "self") { return false }
|
||||
if str_starts_with(lab, "value") { return false }
|
||||
if str_contains(lab, "values") { return false }
|
||||
if str_contains(lab, "identity") { return false }
|
||||
if mem_assoc_skip_label(cand_label) { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// One slot of the association. Manual unroll rather than a loop: EL's codegen
|
||||
// mis-emits accumulating while-loops (documented at soul.el:212, which unrolled
|
||||
// three affective slots for the same reason).
|
||||
fn mem_assoc_slot(results: String, idx: Int, new_id: String) -> Void {
|
||||
if idx >= json_array_len(results) { return }
|
||||
let cand: String = json_array_get(results, idx)
|
||||
let cid: String = json_get(cand, "id")
|
||||
let clabel: String = json_get(cand, "label")
|
||||
let ctype: String = json_get(cand, "node_type")
|
||||
if str_eq(ctype, "Value") { return }
|
||||
if str_eq(ctype, "DharmaSelf") { return }
|
||||
if str_eq(ctype, "Safety") { return }
|
||||
if mem_assoc_ok(cid, clabel, new_id) {
|
||||
wt_edge(new_id, cid, el_from_float(0.5), "related")
|
||||
}
|
||||
}
|
||||
|
||||
// mem_associate — connect a freshly written memory to what it is about.
|
||||
fn mem_associate(new_id: String, content: String, label: String) -> Void {
|
||||
if str_eq(new_id, "") { return }
|
||||
if mem_assoc_skip_label(label) { return }
|
||||
// Ask the graph what this memory resembles. Now that the store carries
|
||||
// meaning-vectors this is semantic, not merely lexical.
|
||||
let probe: String = str_slice(content, 0, 400)
|
||||
let results: String = engram_recall_json(probe, 4)
|
||||
if str_eq(results, "") { return }
|
||||
mem_assoc_slot(results, 0, new_id)
|
||||
mem_assoc_slot(results, 1, new_id)
|
||||
mem_assoc_slot(results, 2, new_id)
|
||||
}
|
||||
|
||||
fn mem_store(content: String, label: String, tags: String) -> String {
|
||||
let id: String = engram_node_full(
|
||||
let id: String = wt_node(
|
||||
content,
|
||||
"Memory",
|
||||
label,
|
||||
@@ -17,13 +99,26 @@ fn mem_store(content: String, label: String, tags: String) -> String {
|
||||
println("[memory] write rejected by engram (empty id): label=" + label)
|
||||
return ""
|
||||
}
|
||||
// Read back to verify the node actually persisted — guards against silent write failures.
|
||||
let readback: String = engram_get_node_json(id)
|
||||
if str_eq(readback, "") || str_eq(readback, "{}") {
|
||||
println("[memory] WRITE VERIFY FAILED: label=" + label + " id=" + id + " — node absent after write")
|
||||
return ""
|
||||
// wt_node has already read the node back locally and returns "" if it did
|
||||
// not land, so the old duplicate read-back here is gone.
|
||||
//
|
||||
// HONESTY (neuron#117): the receipt now says WHERE the write is.
|
||||
// The old unconditional "write verified" line asserted against the soul's
|
||||
// own RAM — true in memory, false on disk — and printed ~115,000 times on
|
||||
// Tim's machine while the canonical snapshot sat frozen for three days.
|
||||
// wt_commit flushes the spool and then asks the OWNER. When it says false
|
||||
// the node is real and recallable but not yet durable, and the log says so
|
||||
// rather than claiming a save that did not happen. The id is still returned:
|
||||
// the local write DID succeed, and the queued delta will be retried.
|
||||
let durable: Bool = wt_commit(id)
|
||||
// Associate AFTER the node is durable: an edge to a node that did not persist
|
||||
// is a dangling edge, which is the defect the 2026-08-09 cleanup removed 830 of.
|
||||
mem_associate(id, content, label)
|
||||
if durable {
|
||||
println("[memory] write persisted at owner: " + id + " label=" + label)
|
||||
} else {
|
||||
println("[memory] write IN MEMORY ONLY (queued for owner, not yet durable): " + id + " label=" + label)
|
||||
}
|
||||
println("[memory] write verified: " + id + " ok")
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -51,12 +146,12 @@ fn mem_strengthen(node_id: String) -> Void {
|
||||
// memory.el (imported first) so awareness.el and neuron-api.el can both call it.
|
||||
fn mem_tombstone(node_id: String) -> String {
|
||||
let tags: String = "[\"Tombstone\",\"status:deleted\"]"
|
||||
let marker: String = engram_node_full(
|
||||
let marker: String = wt_node(
|
||||
node_id, "Tombstone", "tombstone:" + node_id,
|
||||
el_from_float(0.01), el_from_float(0.01), el_from_float(1.0),
|
||||
"Episodic", tags)
|
||||
if !str_eq(marker, "") {
|
||||
engram_connect(marker, node_id, el_from_float(1.0), "tombstones")
|
||||
wt_edge(marker, node_id, el_from_float(1.0), "tombstones")
|
||||
}
|
||||
return marker
|
||||
}
|
||||
|
||||
+528
-28
@@ -195,15 +195,24 @@ fn api_compact_activated(raw: String, max_items: Int, snip: Int) -> String {
|
||||
}
|
||||
|
||||
// api_persisted — read-back-after-write guard against hallucinated saves.
|
||||
// After a write builtin returns an id, confirm the node is actually queryable
|
||||
// via engram_get_node_json(id) (returns "" or "null" when missing). Returns
|
||||
// true only when the node is genuinely persisted.
|
||||
//
|
||||
// WIDENED FOR neuron#117. This function is the single gate every MCP write
|
||||
// handler passes through before it reports success (10 call sites), which makes
|
||||
// it the right place to close the honesty gap rather than editing ten receipts.
|
||||
//
|
||||
// It used to read back from engram_get_node_json — the SOUL'S OWN in-process
|
||||
// graph. In HTTP-engram mode that asserts the wrong thing: the soul is not the
|
||||
// persistence owner, so a node present in its RAM and absent from the owner read
|
||||
// as "persisted" and then vanished on the next restart. The guard was doing
|
||||
// exactly what its comment promised and still certifying writes that did not
|
||||
// survive. It now flushes the write-through spool and asks the OWNER.
|
||||
//
|
||||
// In file mode (no ENGRAM_URL) the soul IS the owner and wt_commit collapses to
|
||||
// the original local read-back — unchanged behaviour, which is what keeps this
|
||||
// reversible.
|
||||
fn api_persisted(id: String) -> Bool {
|
||||
if str_eq(id, "") { return false }
|
||||
let node: String = engram_get_node_json(id)
|
||||
// engram_get_node_json returns "{}" (empty object) when node is not found — not "" or "null".
|
||||
// Check all three to guard against any runtime variation.
|
||||
return !str_eq(node, "") && !str_eq(node, "null") && !str_eq(node, "{}")
|
||||
return wt_commit(id)
|
||||
}
|
||||
|
||||
// api_not_persisted — standard error for a write that did not read back.
|
||||
@@ -295,10 +304,35 @@ fn handle_api_begin_session(body: String) -> String {
|
||||
let state_events: String = api_compact_node_array(state_events_raw, 5, 500)
|
||||
let recent_raw: String = engram_scan_nodes_json(10, 0)
|
||||
let recent: String = api_compact_node_array(recent_raw, 10, 240)
|
||||
// SELF-SEEDED SLICE (2026-08-09). The design is explicit: "Every compilation
|
||||
// query begins at the self-model node and traverses outward... structural
|
||||
// reachability from the self-model node is a precondition for any node to
|
||||
// appear in compiled context" (will-anderson patents/drafts/engram-claims.md,
|
||||
// Self-Seeded Activation; DRAFT, not a filed provisional — cite it as such).
|
||||
//
|
||||
// Measured 2026-08-09 before this change: compiled context contained 0-1
|
||||
// identity records out of 10, because compilation seeds from a hardcoded
|
||||
// TEXT STRING, never from the self. Even an explicit "my values identity who
|
||||
// I am" query returned a boot counter and state-events.
|
||||
//
|
||||
// This restores the designed behaviour WITHOUT repeating the failure that got
|
||||
// self_neighbors set to [] in the first place: that was an UNBOUNDED ~90KB
|
||||
// neighbour dump which closed the socket on every call. Same bound as every
|
||||
// other list here — cap 8, 240-char snippets. The self root has 34 direct
|
||||
// neighbours of which 23 are identity records, so depth 1 is dense enough to
|
||||
// be worth seeding and small enough to stay cheap.
|
||||
let self_raw: String = engram_neighbors_json("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee", 1, "both")
|
||||
// Cap 24, not 8: measured 2026-08-09, the self root's first 8 neighbours are
|
||||
// TAG nodes ("neuron", "tier:note", "disposition:experimental", "imprint",
|
||||
// "traversal") which crowd out the substantive identity records behind them.
|
||||
// The root has 34 neighbours of which 23 are identity; 24 captures them while
|
||||
// staying bounded. Cost measured at ~+4KB on a ~12KB response, nowhere near
|
||||
// the ~90KB unbounded dump that closed sockets and got this set to [].
|
||||
let self_slice: String = api_compact_node_array(self_raw, 24, 240)
|
||||
return "{\"stats\":" + stats
|
||||
+ ",\"recent\":" + recent
|
||||
+ ",\"activated\":" + activated
|
||||
+ ",\"self_neighbors\":[]"
|
||||
+ ",\"self_neighbors\":" + self_slice
|
||||
+ ",\"recent_state_events\":" + state_events + "}"
|
||||
}
|
||||
|
||||
@@ -342,10 +376,17 @@ fn handle_api_remember(body: String) -> String {
|
||||
let inner: String = str_slice(base_tags, 1, str_len(base_tags) - 1)
|
||||
"[" + inner + ",\"project:" + project + "\"]"
|
||||
}
|
||||
let id: String = engram_node_full(content, "Memory", "memory:remembered",
|
||||
let id: String = wt_node(content, "Memory", "memory:remembered",
|
||||
sal, sal, el_from_float(0.9),
|
||||
"Episodic", final_tags)
|
||||
if !api_persisted(id) { return api_not_persisted(id) }
|
||||
// Associate on write (2026-08-09). THIS CALL MUST BE HERE, not only in mem_store.
|
||||
// The HTTP memory route writes via wt_node directly; mem_store serves only the
|
||||
// awareness paths (soul-response, search-result, activation-result) which are
|
||||
// exactly the telemetry we refuse to link. Hooking mem_store alone produced
|
||||
// ZERO edges across four real writes — measured, not assumed, which is the only
|
||||
// reason it was caught before shipping.
|
||||
mem_associate(id, content, "memory:remembered")
|
||||
return "{\"id\":\"" + id + "\",\"ok\":true}"
|
||||
}
|
||||
|
||||
@@ -369,7 +410,7 @@ fn handle_api_node_create(body: String) -> String {
|
||||
if str_eq(importance, "low") { 0.25 } else { 0.5 }
|
||||
}
|
||||
}
|
||||
let id: String = engram_node_full(content, node_type, label,
|
||||
let id: String = wt_node(content, node_type, label,
|
||||
sal, sal, el_from_float(0.9),
|
||||
tier, tags)
|
||||
if !api_persisted(id) { return api_not_persisted(id) }
|
||||
@@ -422,11 +463,11 @@ fn handle_api_node_update(body: String) -> String {
|
||||
}
|
||||
let body_tags: String = json_get(body, "tags")
|
||||
let tags: String = if str_eq(body_tags, "") { "[\"" + node_type + "\"]" } else { body_tags }
|
||||
let new_id: String = engram_node_full(content, node_type, label,
|
||||
let new_id: String = wt_node(content, node_type, label,
|
||||
el_from_float(0.5), el_from_float(0.5), el_from_float(0.8),
|
||||
tier, tags)
|
||||
if !api_persisted(new_id) { return api_not_persisted(new_id) }
|
||||
engram_connect(new_id, id, el_from_float(0.9), "supersedes")
|
||||
wt_edge(new_id, id, el_from_float(0.9), "supersedes")
|
||||
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + id + "\",\"ok\":true}"
|
||||
}
|
||||
|
||||
@@ -503,7 +544,7 @@ fn handle_api_capture_knowledge(body: String) -> String {
|
||||
let full: String = if str_eq(title, "") { content } else { title + ": " + content }
|
||||
let lbl: String = str_slice(title, 0, 80)
|
||||
let tags: String = "[\"Knowledge\",\"captured\"]"
|
||||
let id: String = engram_node_full(full, "Knowledge", lbl,
|
||||
let id: String = wt_node(full, "Knowledge", lbl,
|
||||
el_from_float(0.85), el_from_float(0.8), el_from_float(0.9),
|
||||
"Episodic", tags)
|
||||
if !api_persisted(id) { return api_not_persisted(id) }
|
||||
@@ -518,12 +559,12 @@ fn handle_api_evolve_knowledge(body: String) -> String {
|
||||
if !str_eq(prior_id, "") && is_protected_node(prior_id) { return api_err_protected(prior_id) }
|
||||
let tags: String = "[\"Knowledge\",\"evolved\"]"
|
||||
// Empty label → engram_node_full derives content[:60] (LABEL FIX 2026-07-23).
|
||||
let new_id: String = engram_node_full(content, "Knowledge", "",
|
||||
let new_id: String = wt_node(content, "Knowledge", "",
|
||||
el_from_float(0.75), el_from_float(0.75), el_from_float(0.9),
|
||||
"Episodic", tags)
|
||||
if !api_persisted(new_id) { return api_not_persisted(new_id) }
|
||||
if !str_eq(prior_id, "") {
|
||||
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
}
|
||||
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true}"
|
||||
}
|
||||
@@ -540,11 +581,11 @@ fn handle_api_promote_knowledge(body: String) -> String {
|
||||
"[\"Knowledge\",\"tier:canonical\",\"disposition:stable\"]"
|
||||
} else { tags_raw }
|
||||
// Empty label → engram_node_full derives content[:60] (LABEL FIX 2026-07-23).
|
||||
let new_id: String = engram_node_full(content, "Knowledge", "",
|
||||
let new_id: String = wt_node(content, "Knowledge", "",
|
||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||
"Canonical", tags)
|
||||
if !api_persisted(new_id) { return api_not_persisted(new_id) }
|
||||
engram_connect(new_id, prior_id, el_from_float(0.95), "supersedes")
|
||||
wt_edge(new_id, prior_id, el_from_float(0.95), "supersedes")
|
||||
return "{\"ok\":true,\"new_id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\"}"
|
||||
}
|
||||
|
||||
@@ -567,7 +608,7 @@ fn handle_api_define_process(body: String) -> String {
|
||||
if str_eq(content, "") { return api_err("content is required") }
|
||||
let label: String = if str_eq(name, "") { "process:unnamed" } else { "process:" + name }
|
||||
let tags: String = "[\"Process\"]"
|
||||
let id: String = engram_node_full(content, "Process", label,
|
||||
let id: String = wt_node(content, "Process", label,
|
||||
el_from_float(0.8), el_from_float(0.8), el_from_float(0.9),
|
||||
"Canonical", tags)
|
||||
if !api_persisted(id) { return api_not_persisted(id) }
|
||||
@@ -652,7 +693,7 @@ fn handle_api_tune_config(body: String) -> String {
|
||||
if str_eq(key, "") { return api_err("key is required") }
|
||||
let content: String = "config:" + key + "=" + value
|
||||
let tags: String = "[\"ConfigEntry\",\"config\"]"
|
||||
let id: String = engram_node_full(content, "ConfigEntry", key,
|
||||
let id: String = wt_node(content, "ConfigEntry", key,
|
||||
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
||||
"Canonical", tags)
|
||||
if !api_persisted(id) { return api_not_persisted(id) }
|
||||
@@ -699,7 +740,7 @@ fn handle_api_link_entities(body: String) -> String {
|
||||
if is_protected_node(to_id) { return api_err_protected(to_id) }
|
||||
let relation: String = json_get(body, "relation")
|
||||
let eff_relation: String = if str_eq(relation, "") { "associates" } else { relation }
|
||||
engram_connect(from_id, to_id, el_from_float(0.5), eff_relation)
|
||||
wt_edge(from_id, to_id, el_from_float(0.5), eff_relation)
|
||||
return "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + eff_relation + "\"}"
|
||||
}
|
||||
|
||||
@@ -732,11 +773,11 @@ fn handle_api_evolve_memory(body: String) -> String {
|
||||
}
|
||||
}
|
||||
let tags: String = "[\"Memory\",\"evolved\"]"
|
||||
let new_id: String = engram_node_full(content, "Memory", "memory:evolved",
|
||||
let new_id: String = wt_node(content, "Memory", "memory:evolved",
|
||||
sal, sal, el_from_float(0.9),
|
||||
"Episodic", tags)
|
||||
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
||||
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
}
|
||||
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true}"
|
||||
}
|
||||
@@ -794,11 +835,11 @@ fn handle_api_cultivate(body: String) -> String {
|
||||
let content: String = json_get(body, "content")
|
||||
if str_eq(content, "") { return api_err("content is required") }
|
||||
let tags: String = "[\"Knowledge\",\"evolved\",\"cultivated\"]"
|
||||
let new_id: String = engram_node_full(content, "Knowledge", "knowledge:cultivated",
|
||||
let new_id: String = wt_node(content, "Knowledge", "knowledge:cultivated",
|
||||
el_from_float(0.75), el_from_float(0.75), el_from_float(0.9),
|
||||
"Episodic", tags)
|
||||
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
||||
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
}
|
||||
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true,\"cultivated\":true}"
|
||||
}
|
||||
@@ -814,11 +855,11 @@ fn handle_api_cultivate(body: String) -> String {
|
||||
}
|
||||
}
|
||||
let tags: String = "[\"Memory\",\"evolved\",\"cultivated\"]"
|
||||
let new_id: String = engram_node_full(content, "Memory", "memory:cultivated",
|
||||
let new_id: String = wt_node(content, "Memory", "memory:cultivated",
|
||||
sal, sal, el_from_float(0.9),
|
||||
"Episodic", tags)
|
||||
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
||||
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
||||
}
|
||||
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true,\"cultivated\":true}"
|
||||
}
|
||||
@@ -838,7 +879,7 @@ fn handle_api_cultivate(body: String) -> String {
|
||||
if str_eq(to_id, "") { return api_err("to_id is required") }
|
||||
let relation: String = json_get(body, "relation")
|
||||
let eff_relation: String = if str_eq(relation, "") { "associates" } else { relation }
|
||||
engram_connect(from_id, to_id, el_from_float(0.5), eff_relation)
|
||||
wt_edge(from_id, to_id, el_from_float(0.5), eff_relation)
|
||||
return "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + eff_relation + "\",\"cultivated\":true}"
|
||||
}
|
||||
|
||||
@@ -873,7 +914,7 @@ fn handle_api_consolidate(body: String) -> String {
|
||||
if !str_eq(summary, "") {
|
||||
let safe_summary: String = str_replace(summary, "\"", "'")
|
||||
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
||||
let summary_id: String = engram_node_full(
|
||||
let summary_id: String = wt_node(
|
||||
"[session-summary] " + safe_summary,
|
||||
"SessionSummary", "session:summary",
|
||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||
@@ -885,3 +926,462 @@ fn handle_api_consolidate(body: String) -> String {
|
||||
}
|
||||
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
||||
}
|
||||
|
||||
// ── Stage 1: structural audit ─────────────────────────────────────────────────
|
||||
//
|
||||
// WHAT THIS IMPLEMENTS
|
||||
// The CGI provisional, 05-detailed-description.md, "Stage 1: Structural audit
|
||||
// 430". Verbatim, the audit module evaluates: the density and typed
|
||||
// distribution of causal edges; the consistency between value nodes and
|
||||
// execution-record neighborhoods; the richness and connectivity of the
|
||||
// self-model; and the authenticity of open-question nodes in the wonder
|
||||
// manifest. It "produces a coherence assessment 432 — NOT A BINARY SCORE but
|
||||
// an annotated characterization of the graph's structural properties".
|
||||
//
|
||||
// That last clause is the whole shape of this handler. Every finding carries
|
||||
// its own numbers AND a plain-language note saying what the numbers mean and
|
||||
// how they were obtained. There is no pass/fail, no percentage-of-health, no
|
||||
// composite score, and `"score":null` is emitted explicitly so a downstream
|
||||
// reader cannot mistake its absence for an omission.
|
||||
//
|
||||
// WHY IT EXISTS NOW, AND WHY THE FIRST FINDING IS THE ONE IT IS
|
||||
// `runStructuralAudit` has been an advertised MCP tool with nothing behind it:
|
||||
// the dispatcher GET'd /session/begin and returned that blob (mcp-wrapper/src/
|
||||
// main.el). Meanwhile the failure the audit would have caught ran silently for
|
||||
// about three weeks — the soul reported 103,089 nodes while the engram, which
|
||||
// OWNS persistence, held ~79,900; a crash discarded the difference. Every boot
|
||||
// reported green throughout, because nothing in the system ever compared the
|
||||
// two sides. So finding 1 is owner-versus-runtime divergence: it is the check
|
||||
// whose absence cost real memory, and it is cheap and exact.
|
||||
//
|
||||
// WHAT IS DELIBERATELY NOT HERE (stage 1b, see the `deferred` array in the
|
||||
// response): value/execution-record consistency and wonder-manifest
|
||||
// authenticity. Both need node types that barely exist in this graph today —
|
||||
// the response MEASURES those populations and reports the counts as the reason,
|
||||
// rather than asserting a deferral without evidence.
|
||||
//
|
||||
// MEASUREMENT HONESTY: EXACT WHERE CHEAP, SAMPLED WHERE NOT, ALWAYS LABELLED
|
||||
// Counts, edge typing and self-model connectivity are exact. Orphan rate and
|
||||
// dangling-edge rate are SAMPLED, because the engram runtime has no node-id
|
||||
// index — `engram_find_node_index` is a linear scan over every node, so an
|
||||
// exhaustive dangling check is O(nodes x edges) (~2.2e9 string compares at
|
||||
// today's scale, tens of seconds inside one request). The samples are UNIFORM
|
||||
// across the whole population, not head-of-list, and every sampled figure is
|
||||
// emitted with its own `sampled` / `population` fields plus an extrapolation
|
||||
// labelled as such. Raise `?edge_sample=` / `?node_sample=` to the population
|
||||
// size to run either check exhaustively and pay the time. The real fix is an
|
||||
// id index in the runtime; that is the engram repo's, not this handler's.
|
||||
|
||||
// audit_pct1 — one-decimal percentage as a bare JSON number, sign-safe.
|
||||
// Integer math only: EL has no fixed-precision formatter, and float_to_str
|
||||
// would put an unbounded mantissa in the response.
|
||||
fn audit_pct1(num: Int, den: Int) -> String {
|
||||
if den <= 0 { return "null" }
|
||||
let neg: Bool = num < 0
|
||||
let a: Int = if neg { 0 - num } else { num }
|
||||
let tenths: Int = (a * 1000) / den
|
||||
let whole: Int = tenths / 10
|
||||
let frac: Int = tenths - (whole * 10)
|
||||
let sign: String = if neg { "-" } else { "" }
|
||||
return sign + int_to_str(whole) + "." + int_to_str(frac)
|
||||
}
|
||||
|
||||
// audit_finding — the one envelope every finding uses: name, the measurements,
|
||||
// and the annotation. Keeping it in one place is what stops the characterization
|
||||
// from degenerating into a bag of numbers with no reading attached.
|
||||
fn audit_finding(name: String, measured: String, note: String) -> String {
|
||||
return "{\"finding\":\"" + name + "\""
|
||||
+ ",\"measured\":{" + measured + "}"
|
||||
+ ",\"note\":\"" + api_json_escape(note) + "\"}"
|
||||
}
|
||||
|
||||
// audit_str_at — read the quoted string value starting at byte `start`.
|
||||
// Slices a bounded window rather than the tail of the (multi-MB) edges array, so
|
||||
// this is O(window) per call instead of O(remaining input).
|
||||
fn audit_str_at(s: String, start: Int, maxlen: Int) -> String {
|
||||
let n: Int = str_len(s)
|
||||
if start < 0 || start >= n { return "" }
|
||||
let end_guess: Int = start + maxlen
|
||||
let stop: Int = if end_guess > n { n } else { end_guess }
|
||||
let win: String = str_slice(s, start, stop)
|
||||
let q: Int = str_index_of(win, "\"")
|
||||
if q < 0 { return "" }
|
||||
return str_slice(win, 0, q)
|
||||
}
|
||||
|
||||
// audit_rel_count — exact count of edges carrying `rel`, by scanning the emitted
|
||||
// edge array for the literal `"relation":"<rel>"`. engram_emit_edge_json writes
|
||||
// metadata ESCAPED as a string, so no nested object can contain that literal and
|
||||
// the count cannot be inflated by edge payloads.
|
||||
fn audit_rel_count(edges: String, rel: String) -> Int {
|
||||
return str_count(edges, "\"relation\":\"" + rel + "\"")
|
||||
}
|
||||
|
||||
// audit_owner_stats — ask the persistence OWNER for its own counts.
|
||||
// Returns "" when there is no HTTP owner configured or the owner is unreachable;
|
||||
// both are reported as findings, never as a failure of the audit.
|
||||
fn audit_owner_stats(url: String) -> String {
|
||||
if str_eq(url, "") { return "" }
|
||||
return http_get(url + "/api/stats")
|
||||
}
|
||||
|
||||
// audit_divergence — FINDING 1. Runtime (this soul's in-process graph) versus
|
||||
// the persistence owner's own count. Trend is measured against the previous
|
||||
// audit recorded in soul state, so a second call answers "is the gap growing?"
|
||||
// rather than just restating it.
|
||||
fn audit_divergence() -> String {
|
||||
let rt_nodes: Int = engram_node_count()
|
||||
let rt_edges: Int = engram_edge_count()
|
||||
let url: String = wt_engram_url()
|
||||
|
||||
if str_eq(url, "") {
|
||||
return audit_finding("owner_runtime_divergence",
|
||||
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
||||
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
||||
+ ",\"owner\":\"none\",\"owner_reachable\":false",
|
||||
"No HTTP persistence owner is configured, so this soul IS the owner "
|
||||
+ "(file mode) and divergence is not defined. This check only has "
|
||||
+ "meaning when ENGRAM_URL points at a separate engram that owns the "
|
||||
+ "canonical store.")
|
||||
}
|
||||
|
||||
let stats: String = audit_owner_stats(url)
|
||||
// REACHABILITY IS PROVED BY THE PAYLOAD, NOT BY A NON-EMPTY REPLY.
|
||||
// http_get does not return "" on a connection failure — it returns a JSON
|
||||
// error object ({"error":"Failed to connect to ... Couldn't connect to
|
||||
// server"}). Testing only for "" made a DEAD owner read as reachable with
|
||||
// node_count 0, i.e. the audit would have reported a 100% divergence and
|
||||
// named it as data loss. That false positive is worse than no check at all:
|
||||
// it is precisely the kind of confident wrong answer this route exists to
|
||||
// stop. Require the field the contract promises.
|
||||
let owner_nc_raw: String = json_get_raw(stats, "node_count")
|
||||
if str_eq(stats, "") || str_eq(owner_nc_raw, "") {
|
||||
return audit_finding("owner_runtime_divergence",
|
||||
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
||||
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
||||
+ ",\"owner\":\"" + api_json_escape(url) + "\",\"owner_reachable\":false"
|
||||
+ ",\"owner_reply\":\"" + api_json_escape(api_utf8_trunc(stats, 200)) + "\"",
|
||||
"The persistence owner at " + url + " did not return a node_count "
|
||||
+ "from GET /api/stats. Divergence is UNKNOWN, NOT ZERO — an owner "
|
||||
+ "that cannot be read is exactly the condition under which the "
|
||||
+ "runtime's own count means least, and reporting 0 for the owner "
|
||||
+ "would manufacture a total-loss reading out of a network error. "
|
||||
+ "Reported as a finding rather than raised as an error so the rest "
|
||||
+ "of the audit still returns; the owner's raw reply is in "
|
||||
+ "owner_reply.")
|
||||
}
|
||||
|
||||
let ow_nodes: Int = json_get_int(stats, "node_count")
|
||||
let ow_edges: Int = json_get_int(stats, "edge_count")
|
||||
let d_nodes: Int = rt_nodes - ow_nodes
|
||||
let d_edges: Int = rt_edges - ow_edges
|
||||
|
||||
// Trend against the previous audit in this soul's state.
|
||||
let prev_raw: String = state_get("audit_prev_node_delta")
|
||||
let prev: Int = str_to_int(prev_raw)
|
||||
let abs_now: Int = if d_nodes < 0 { 0 - d_nodes } else { d_nodes }
|
||||
let abs_prev: Int = if prev < 0 { 0 - prev } else { prev }
|
||||
let trend: String = if str_eq(prev_raw, "") {
|
||||
"no_prior_audit"
|
||||
} else {
|
||||
if abs_now > abs_prev { "growing" } else {
|
||||
if abs_now < abs_prev { "shrinking" } else { "flat" }
|
||||
}
|
||||
}
|
||||
state_set("audit_prev_node_delta", int_to_str(d_nodes))
|
||||
state_set("audit_prev_ts", int_to_str(time_now()))
|
||||
|
||||
let note_head: String = if d_nodes == 0 {
|
||||
"Runtime and owner agree on node count."
|
||||
} else {
|
||||
"Runtime holds " + int_to_str(d_nodes) + " nodes (" + audit_pct1(d_nodes, rt_nodes)
|
||||
+ "% of its own graph) that the persistence owner does not report. Nodes "
|
||||
+ "that exist only in runtime memory do not survive a restart."
|
||||
}
|
||||
return audit_finding("owner_runtime_divergence",
|
||||
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
||||
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
||||
+ ",\"owner\":\"" + api_json_escape(url) + "\",\"owner_reachable\":true"
|
||||
+ ",\"owner_nodes\":" + int_to_str(ow_nodes)
|
||||
+ ",\"owner_edges\":" + int_to_str(ow_edges)
|
||||
+ ",\"node_delta\":" + int_to_str(d_nodes)
|
||||
+ ",\"edge_delta\":" + int_to_str(d_edges)
|
||||
+ ",\"node_delta_pct_of_runtime\":" + audit_pct1(d_nodes, rt_nodes)
|
||||
+ ",\"trend_vs_previous_audit\":\"" + trend + "\""
|
||||
+ ",\"previous_node_delta\":" + (if str_eq(prev_raw, "") { "null" } else { int_to_str(prev) }),
|
||||
note_head + " Trend against the previous audit recorded in this soul's "
|
||||
+ "state: " + trend + ". This is the comparison whose absence let a "
|
||||
+ "~24,000-node loss run for weeks with every boot reporting green.")
|
||||
}
|
||||
|
||||
// audit_edge_typing — FINDING 2. Density plus the typed distribution the patent
|
||||
// asks for, against the claim-10 relation vocabulary. Exact: str_count over the
|
||||
// emitted edge array, one linear pass per relation.
|
||||
fn audit_edge_typing(edges: String, total_edges: Int, node_total: Int) -> String {
|
||||
let c_sup: Int = audit_rel_count(edges, "Supersedes")
|
||||
let c_cau: Int = audit_rel_count(edges, "Causes")
|
||||
let c_con: Int = audit_rel_count(edges, "Contains")
|
||||
let c_ref: Int = audit_rel_count(edges, "References")
|
||||
let c_ctr: Int = audit_rel_count(edges, "Contradicts")
|
||||
let c_exe: Int = audit_rel_count(edges, "Exemplifies")
|
||||
let c_act: Int = audit_rel_count(edges, "Activates")
|
||||
let c_tmp: Int = audit_rel_count(edges, "TemporallyPrecedes")
|
||||
let typed: Int = c_sup + c_cau + c_con + c_ref + c_ctr + c_exe + c_act + c_tmp
|
||||
|
||||
// Lowercase near-misses: the same eight concepts written by the ad-hoc write
|
||||
// paths (linkEntities defaults to "associates", linkCausal to "causes").
|
||||
// Counted separately because "the vocabulary is unused" and "the vocabulary
|
||||
// is used in the wrong case" are different defects with different fixes.
|
||||
let l_sup: Int = audit_rel_count(edges, "supersedes")
|
||||
let l_cau: Int = audit_rel_count(edges, "causes")
|
||||
let l_con: Int = audit_rel_count(edges, "contains")
|
||||
let l_ref: Int = audit_rel_count(edges, "references")
|
||||
let l_ctr: Int = audit_rel_count(edges, "contradicts")
|
||||
let l_exe: Int = audit_rel_count(edges, "exemplifies")
|
||||
let l_act: Int = audit_rel_count(edges, "activates")
|
||||
let l_tmp: Int = audit_rel_count(edges, "temporallyPrecedes")
|
||||
let near: Int = l_sup + l_cau + l_con + l_ref + l_ctr + l_exe + l_act + l_tmp
|
||||
|
||||
let untyped: Int = total_edges - typed
|
||||
return audit_finding("typed_edge_distribution",
|
||||
"\"total_edges\":" + int_to_str(total_edges)
|
||||
+ ",\"total_nodes\":" + int_to_str(node_total)
|
||||
// Density per 100 nodes, not per node: EL has no fixed-precision float
|
||||
// formatter, and "0.3 edges per node" rounded to an integer is a lie.
|
||||
+ ",\"edges_per_100_nodes\":" + audit_pct1(total_edges, node_total)
|
||||
+ ",\"claim10_typed\":" + int_to_str(typed)
|
||||
+ ",\"claim10_typed_pct\":" + audit_pct1(typed, total_edges)
|
||||
+ ",\"outside_claim10_vocabulary\":" + int_to_str(untyped)
|
||||
+ ",\"lowercase_near_miss\":" + int_to_str(near)
|
||||
+ ",\"by_relation\":{"
|
||||
+ "\"Supersedes\":" + int_to_str(c_sup)
|
||||
+ ",\"Causes\":" + int_to_str(c_cau)
|
||||
+ ",\"Contains\":" + int_to_str(c_con)
|
||||
+ ",\"References\":" + int_to_str(c_ref)
|
||||
+ ",\"Contradicts\":" + int_to_str(c_ctr)
|
||||
+ ",\"Exemplifies\":" + int_to_str(c_exe)
|
||||
+ ",\"Activates\":" + int_to_str(c_act)
|
||||
+ ",\"TemporallyPrecedes\":" + int_to_str(c_tmp) + "}",
|
||||
"Only " + int_to_str(typed) + " of " + int_to_str(total_edges)
|
||||
+ " edges use the claim-10 causal vocabulary; the remainder are ad-hoc "
|
||||
+ "relation strings, which is why the graph's causal claims cannot yet "
|
||||
+ "be checked for internal consistency — an untyped edge asserts "
|
||||
+ "association, not causation. " + int_to_str(near) + " edges use a "
|
||||
+ "lowercase spelling of a claim-10 relation: those are near-misses the "
|
||||
+ "write paths could be corrected to emit, not genuinely foreign types.")
|
||||
}
|
||||
|
||||
// audit_orphans_dangling — FINDING 3. Both figures are SAMPLED; see the header
|
||||
// for why exhaustive is O(nodes x edges) on this runtime.
|
||||
//
|
||||
// An "orphan" here is a node with zero RESOLVABLE edges: engram_neighbors_json
|
||||
// drops any edge whose other endpoint does not resolve to a node, so a node
|
||||
// whose only edges are dangling reads as an orphan. That is the right reading —
|
||||
// such a node is unreachable by traversal — but it is stated rather than hidden.
|
||||
fn audit_orphans_dangling(edges: String, total_edges: Int, node_total: Int,
|
||||
edge_cap: Int, node_cap: Int) -> String {
|
||||
// ── orphan sample: uniform stride over the node store ──
|
||||
let n_take: Int = if node_total < node_cap { node_total } else { node_cap }
|
||||
let n_stride: Int = if n_take > 0 { node_total / n_take } else { 1 }
|
||||
let n_stride = if n_stride < 1 { 1 } else { n_stride }
|
||||
let orphans: Int = 0
|
||||
let n_checked: Int = 0
|
||||
let j: Int = 0
|
||||
while j < n_take {
|
||||
let one: String = engram_scan_nodes_json(1, j * n_stride)
|
||||
let nid: String = json_get(json_array_get(one, 0), "id")
|
||||
if !str_eq(nid, "") {
|
||||
let nbrs: String = engram_neighbors_json(nid, 1, "both")
|
||||
let deg: Int = json_array_len(nbrs)
|
||||
let orphans = if deg == 0 { orphans + 1 } else { orphans }
|
||||
let n_checked = n_checked + 1
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
// ── dangling sample: uniform stride over the edge array ──
|
||||
// str_index_of_all gives every edge's field offsets in ONE linear pass, so
|
||||
// any index can be read in O(1). json_array_get would have been O(i) per
|
||||
// element and O(n^2) over the array.
|
||||
let from_pos: [Int] = str_index_of_all(edges, "\"from_id\":\"")
|
||||
let to_pos: [Int] = str_index_of_all(edges, "\"to_id\":\"")
|
||||
let nf: Int = len(from_pos)
|
||||
let nt: Int = len(to_pos)
|
||||
let ne: Int = if nf < nt { nf } else { nt }
|
||||
let e_take: Int = if ne < edge_cap { ne } else { edge_cap }
|
||||
let e_stride: Int = if e_take > 0 { ne / e_take } else { 1 }
|
||||
let e_stride = if e_stride < 1 { 1 } else { e_stride }
|
||||
let dangling: Int = 0
|
||||
let e_checked: Int = 0
|
||||
let i: Int = 0
|
||||
while i < ne && e_checked < e_take {
|
||||
let fid: String = audit_str_at(edges, get(from_pos, i) + 11, 96)
|
||||
let tid: String = audit_str_at(edges, get(to_pos, i) + 9, 96)
|
||||
let f_gone: Bool = str_eq(engram_get_node_json(fid), "{}")
|
||||
let t_gone: Bool = if f_gone { true } else { str_eq(engram_get_node_json(tid), "{}") }
|
||||
let dangling = if f_gone || t_gone { dangling + 1 } else { dangling }
|
||||
let e_checked = e_checked + 1
|
||||
let i = i + e_stride
|
||||
}
|
||||
|
||||
let orphan_est: Int = if n_checked > 0 { (orphans * node_total) / n_checked } else { 0 }
|
||||
let dangle_est: Int = if e_checked > 0 { (dangling * total_edges) / e_checked } else { 0 }
|
||||
let exhaustive_n: String = if n_checked >= node_total { "true" } else { "false" }
|
||||
let exhaustive_e: String = if e_checked >= ne { "true" } else { "false" }
|
||||
|
||||
return audit_finding("orphans_and_dangling_edges",
|
||||
"\"nodes_population\":" + int_to_str(node_total)
|
||||
+ ",\"nodes_sampled\":" + int_to_str(n_checked)
|
||||
+ ",\"nodes_sample_exhaustive\":" + exhaustive_n
|
||||
+ ",\"orphans_in_sample\":" + int_to_str(orphans)
|
||||
+ ",\"orphan_rate_pct\":" + audit_pct1(orphans, n_checked)
|
||||
+ ",\"orphans_extrapolated\":" + int_to_str(orphan_est)
|
||||
+ ",\"edges_population\":" + int_to_str(total_edges)
|
||||
+ ",\"edges_sampled\":" + int_to_str(e_checked)
|
||||
+ ",\"edges_sample_exhaustive\":" + exhaustive_e
|
||||
+ ",\"dangling_in_sample\":" + int_to_str(dangling)
|
||||
+ ",\"dangling_rate_pct\":" + audit_pct1(dangling, e_checked)
|
||||
+ ",\"dangling_extrapolated\":" + int_to_str(dangle_est),
|
||||
"Orphan = zero RESOLVABLE edges, so a node whose only edges dangle counts "
|
||||
+ "as an orphan; either way it is unreachable by traversal. Dangling = an "
|
||||
+ "edge with an endpoint id that resolves to no node. Both are uniform "
|
||||
+ "stride samples over the whole population, not the head of the list; "
|
||||
+ "the extrapolations are estimates and are labelled as such. Pass "
|
||||
+ "?node_sample= / ?edge_sample= at or above the population size to run "
|
||||
+ "either check exhaustively. A high orphan rate is a characterization, "
|
||||
+ "not a verdict: an accumulating store legitimately holds unlinked "
|
||||
+ "material. It becomes a defect when the write paths were SUPPOSED to "
|
||||
+ "link and did not.")
|
||||
}
|
||||
|
||||
// audit_pillar — one self-model pillar: present, how much content, how connected.
|
||||
fn audit_pillar(key: String, id: String) -> String {
|
||||
let node: String = engram_get_node_json(id)
|
||||
let present: Bool = !str_eq(node, "{}") && !str_eq(node, "")
|
||||
if !present {
|
||||
return "\"" + key + "\":{\"id\":\"" + id + "\",\"present\":false"
|
||||
+ ",\"content_length\":0,\"degree\":0}"
|
||||
}
|
||||
let content: String = json_get(node, "content")
|
||||
let deg: Int = json_array_len(engram_neighbors_json(id, 1, "both"))
|
||||
return "\"" + key + "\":{\"id\":\"" + id + "\",\"present\":true"
|
||||
+ ",\"label\":\"" + api_json_escape(json_get(node, "label")) + "\""
|
||||
+ ",\"tier\":\"" + api_json_escape(json_get(node, "tier")) + "\""
|
||||
+ ",\"content_length\":" + int_to_str(str_len(content))
|
||||
+ ",\"degree\":" + int_to_str(deg) + "}"
|
||||
}
|
||||
|
||||
// audit_self_model — FINDING 4. "the richness and connectivity of the
|
||||
// self-model ... is it connected to behavioral evidence?"
|
||||
//
|
||||
// This finding RETIRES the Claude-side vitals identity block. That check lived
|
||||
// outside the system it was checking — a shell script grepping a snapshot — so
|
||||
// it could only ever report on a file, and it went on reporting green while the
|
||||
// memory-philosophy pillar was absent from the live graph for about three weeks.
|
||||
// Asking the running soul about its own three pillars is the designed mechanism;
|
||||
// a shell probe was the fourth patch on the same hole.
|
||||
fn audit_self_model() -> String {
|
||||
let dna: String = audit_pillar("intellectual_dna", "kn-5adecd7e-d6db-4576-87fe-6ef8a935cea6")
|
||||
let val: String = audit_pillar("values_hub", "kn-5b606390-a52d-4ca2-8e0e-eba141d13440")
|
||||
let phi: String = audit_pillar("memory_philosophy", "kn-dcfe04b3-3702-4cac-b6f0-ecb4db837eee")
|
||||
let root: String = audit_pillar("self_root", "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")
|
||||
return audit_finding("self_model_connectivity",
|
||||
"\"pillars\":{" + dna + "," + val + "," + phi + "," + root + "}",
|
||||
"The three identity pillars plus the self root. `degree` counts nodes "
|
||||
+ "reachable in one hop in either direction — the self-model's connection "
|
||||
+ "to the rest of the graph. present:false on any pillar is the condition "
|
||||
+ "that ran undetected for weeks; content_length distinguishes a pillar "
|
||||
+ "that is present from one that is present but hollowed out. The patent "
|
||||
+ "also asks whether the self-model makes ACCURATE PREDICTIONS about the "
|
||||
+ "system's own behavior; that half needs Prediction nodes and is deferred "
|
||||
+ "with the rest of stage 1b below.")
|
||||
}
|
||||
|
||||
// audit_deferred — what stage 1 does NOT yet evaluate, with the measured reason.
|
||||
// Emitted as data, not as a comment, so a reader of the assessment sees the gap
|
||||
// and its evidence rather than inferring completeness from silence.
|
||||
fn audit_deferred() -> String {
|
||||
let preds: Int = json_array_len(api_or_empty(engram_scan_nodes_by_type_json("Prediction", 50, 0)))
|
||||
let wonders: Int = json_array_len(api_or_empty(engram_scan_nodes_by_type_json("WonderQuestion", 50, 0)))
|
||||
return "[{\"deferred\":\"value_execution_record_consistency\""
|
||||
+ ",\"stage\":\"1b\""
|
||||
+ ",\"measured\":{\"prediction_nodes_found\":" + int_to_str(preds) + "}"
|
||||
+ ",\"reason\":\"" + api_json_escape(
|
||||
"The patent asks whether the execution history SUPPORTS the stated "
|
||||
+ "values or shows systematic conflict. That requires execution "
|
||||
+ "records tied to value nodes and predictions to score them against. "
|
||||
+ "Prediction nodes found (capped at 50): " + int_to_str(preds)
|
||||
+ ". Asserting value/execution coherence on that population would be "
|
||||
+ "a fabricated result, which is worse than a stated gap.") + "\"}"
|
||||
+ ",{\"deferred\":\"wonder_manifest_authenticity\""
|
||||
+ ",\"stage\":\"1b\""
|
||||
+ ",\"measured\":{\"wonder_question_nodes_found\":" + int_to_str(wonders) + "}"
|
||||
+ ",\"reason\":\"" + api_json_escape(
|
||||
"The patent asks whether pull weights CORRELATE WITH GENUINE "
|
||||
+ "PREDICTION UNCERTAINTY or are uniform/externally assigned — a "
|
||||
+ "correlation between two populations. WonderQuestion nodes readable "
|
||||
+ "by type (capped at 50): " + int_to_str(wonders) + ", against "
|
||||
+ int_to_str(preds) + " Prediction nodes. There is a known write/read "
|
||||
+ "node-type mismatch on the wonder path; until that is fixed and both "
|
||||
+ "populations exist, any correlation reported here would be noise.") + "\"}]"
|
||||
}
|
||||
|
||||
// handle_api_structural_audit — Stage 1. Returns the coherence assessment 432:
|
||||
// an annotated characterization, explicitly NOT a score.
|
||||
//
|
||||
// COST NOTE: the edge findings need the relation labels, and the runtime exposes
|
||||
// no edge-enumeration builtin. The only way to see them is the same one
|
||||
// GET /api/graph/edges already uses — engram_save to a SCRATCH path (never the
|
||||
// owner's canonical file; see routes.el, neuron#117) and read the array back.
|
||||
// On a large graph that is a multi-hundred-MB write, so this is a manual audit
|
||||
// route, not something to put on a timer. Pass ?edges=0 to skip both edge
|
||||
// findings and get the divergence + self-model readings cheaply.
|
||||
fn handle_api_structural_audit(method: String, path: String, body: String) -> String {
|
||||
let node_total: Int = engram_node_count()
|
||||
let edge_total: Int = engram_edge_count()
|
||||
let want_edges: Bool = !str_eq(api_query_param(path, "edges"), "0")
|
||||
let edge_cap: Int = api_query_int(path, "edge_sample", 3000)
|
||||
let node_cap: Int = api_query_int(path, "node_sample", 300)
|
||||
|
||||
let divergence: String = audit_divergence()
|
||||
let self_model: String = audit_self_model()
|
||||
|
||||
let edge_part: String = if want_edges {
|
||||
// Scratch export only. state_get("soul_snapshot_path") is deliberately
|
||||
// NOT used: in HTTP-engram mode the soul is not the persistence owner and
|
||||
// must never write the canonical file, not even on a read path.
|
||||
let scratch_dir: String = env("TMPDIR")
|
||||
let scratch_base: String = if str_eq(scratch_dir, "") { "/tmp" } else { scratch_dir }
|
||||
let snap_path: String = scratch_base + "/soul-audit-export-" + state_get("soul_cgi_id") + ".json"
|
||||
// engram_save returns Int (1 ok / 0 fail); str_eq on it SIGSEGVs (#150).
|
||||
let saved: Int = engram_save(snap_path)
|
||||
if saved == 0 {
|
||||
"," + audit_finding("typed_edge_distribution", "\"available\":false",
|
||||
"Could not export the graph to " + snap_path + " for edge analysis, "
|
||||
+ "so edge typing and the dangling-edge sample were not run. "
|
||||
+ "Reported as a gap, not as zero findings.")
|
||||
} else {
|
||||
// wt_read, not fs_read: fs_read leaves a thread-local length hint that
|
||||
// the NEXT HTTP response would use as its Content-Length, appending
|
||||
// adjacent heap bytes to the reply (see persist.el wt_read).
|
||||
let snap: String = wt_read(snap_path)
|
||||
let edges_raw: String = json_get_raw(snap, "edges")
|
||||
let edges: String = if str_eq(edges_raw, "") { "[]" } else { edges_raw }
|
||||
"," + audit_edge_typing(edges, edge_total, node_total)
|
||||
+ "," + audit_orphans_dangling(edges, edge_total, node_total, edge_cap, node_cap)
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return "{\"audit\":\"structural\",\"stage\":1"
|
||||
+ ",\"spec\":\"CGI provisional 05-detailed-description.md, Stage 1: Structural audit 430\""
|
||||
+ ",\"assessment\":\"coherence_assessment_432\""
|
||||
+ ",\"assessment_kind\":\"annotated_characterization\""
|
||||
+ ",\"score\":null"
|
||||
+ ",\"score_note\":\"By design. The specification calls for an annotated characterization of the graph's structural properties, not a binary score. Read the findings.\""
|
||||
+ ",\"cgi_id\":\"" + api_json_escape(state_get("soul_cgi_id")) + "\""
|
||||
+ ",\"ts_ms\":" + int_to_str(time_now())
|
||||
+ ",\"findings\":[" + divergence + "," + self_model + edge_part + "]"
|
||||
+ ",\"deferred\":" + audit_deferred() + "}"
|
||||
}
|
||||
|
||||
@@ -37,3 +37,4 @@ extern fn handle_api_memory_update(body: String) -> String
|
||||
extern fn handle_api_cultivate(body: String) -> String
|
||||
extern fn handle_api_list_typed(node_type: String, path: String, body: String) -> String
|
||||
extern fn handle_api_consolidate(body: String) -> String
|
||||
extern fn handle_api_structural_audit(method: String, path: String, body: String) -> String
|
||||
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
// persist.el — the soul→engram WRITE-THROUGH boundary (neuron#117).
|
||||
//
|
||||
// WHY THIS FILE EXISTS
|
||||
// soul.el:571-573 states the ownership rule: "when ENGRAM_URL is set the HTTP
|
||||
// Engram owns persistence — the soul must NEVER write to the local snapshot
|
||||
// (not the persistence owner)." The soul obeys the NEGATIVE half. The POSITIVE
|
||||
// half — how a write made inside the soul actually REACHES the owner — was
|
||||
// never built. Sync is pull-only (awareness.el `/api/sync` -> engram_load_merge),
|
||||
// so every node the soul creates lives in its process RAM and is shed on
|
||||
// restart. Measured live 2026-08-07: soul node_count=102184, engram
|
||||
// node_count=79197 — ~23k nodes existing nowhere but RAM.
|
||||
//
|
||||
// SCOPE NOTE ON THE PATENT (corrects an earlier internal reading)
|
||||
// Engram provisional claims 15-18 describe a delta-sync protocol "with peer
|
||||
// Engram instances"; claim 17's pull-then-push sequence is PEER-ENGRAM to
|
||||
// PEER-ENGRAM. The soul is NOT a peer Engram — it is a CALLER of the database
|
||||
// system API (cf. claim 27, "invoked explicitly by a caller of the database
|
||||
// system API"). So claim 17 does not specify a soul↔engram contract and is not
|
||||
// cited as authority here. This design is derived from the ownership rule
|
||||
// alone: the owner owns the writes, therefore the soul must HAND writes to the
|
||||
// owner and must never write the owner's file itself.
|
||||
//
|
||||
// THE MECHANISM, AND WHY NOT `POST /api/nodes`
|
||||
// The obvious route is the one the persona/boot-counter write-backs already
|
||||
// use, POST /api/nodes. It is the wrong instrument here, verified against the
|
||||
// live engram binary in a sandbox:
|
||||
// - it mints a NEW server-side id (engram_node), so the soul's id and the
|
||||
// owner's id diverge — the next /api/sync pull re-imports the node as a
|
||||
// DUPLICATE, and any edge referencing the soul's id never resolves;
|
||||
// - it accepts only {content, node_type, salience} and drops label, tier,
|
||||
// tags, importance, confidence, metadata. A probe posted with tier
|
||||
// "Canonical" came back tier "Working", importance 0.5.
|
||||
// POST /api/load-merge (Will's own route, el `dc39a61`) is the right one:
|
||||
// - engram_load_merge PRESERVES the id and every field;
|
||||
// - it dedups nodes by id and edges by (from_id,to_id,relation), so a
|
||||
// re-submitted delta is a NO-OP — retry safety is free, and it is the same
|
||||
// local-wins semantics the graph already uses;
|
||||
// - it calls persist_canonical() — THE OWNER writes its own canonical file.
|
||||
// The soul never touches it. The ownership rule is honoured in its
|
||||
// strongest form rather than worked around;
|
||||
// - it returns real counts {ok, nodes_added, edges_added, node_count},
|
||||
// so a receipt can be a MEASUREMENT instead of a fixed success shape.
|
||||
//
|
||||
// SPOOL-AND-DRAIN, AND WHY IT IS NOT JUST A DIRECT POST
|
||||
// Measured in a sandbox against a 79k-node / 176MB graph (live scale): one
|
||||
// load-merge costs ~0.38s, essentially all of it the owner's persist_canonical.
|
||||
// A chat turn writes 5-7 nodes; pushing each separately would add ~2.7s per
|
||||
// turn. So writes are STAGED and pushed in one coalesced batch.
|
||||
// The staging buffer is the FILESYSTEM, not process state, because the soul
|
||||
// serves each HTTP connection on its own pthread (el_runtime http_serve_async)
|
||||
// and a shared in-process buffer would lose entries to a read-modify-write
|
||||
// race — silently, which is the one failure mode this file exists to end.
|
||||
// One file per write, named with uuid_v4, is race-free by construction and
|
||||
// buys a property a memory buffer cannot: writes that could not be pushed
|
||||
// SURVIVE A SOUL CRASH and are drained on the next boot.
|
||||
//
|
||||
// WHAT IS DELIBERATELY NOT PUSHED
|
||||
// - InternalStateEvent / heartbeat telemetry. Will's own carve-out, stated in
|
||||
// engram server.el 8f8ccc9: "48h-pruned, loss-tolerant, ~2/min; snapshotting
|
||||
// 28MB per heartbeat is waste."
|
||||
// NOTE (ours, flagged for Will): we do NOT additionally exclude Working-tier
|
||||
// nodes. That exclusion exists in `fb0bb55` to stop the boot counter leaking
|
||||
// through the /api/sync PULL; it is about sync backflow, not durability.
|
||||
// Applying it here would exclude mem_store — which writes tier "Working" — and
|
||||
// mem_store is the single most important durable write path in the soul. Boot
|
||||
// seeding reads the canonical file wholesale, so a pushed Working-tier node
|
||||
// does survive restart. This is the one classification call this file makes
|
||||
// that Will has not ruled on.
|
||||
//
|
||||
// WHAT THIS BOUNDARY CANNOT EXPRESS (by construction, not by omission)
|
||||
// - engram_strengthen (salience/activation drift): load-merge SKIPS ids that
|
||||
// already exist, so it cannot update an existing node. There is no owner-side
|
||||
// update/upsert route. Not pushable through any current route; left as a
|
||||
// follow-up that needs a change in the engram repo.
|
||||
// - engram_forget (hard delete): load-merge is additive and has no delete verb.
|
||||
// Propagating deletes would mean DELETE /api/nodes/<id>, a HARD delete at the
|
||||
// owner — which scripts/verify-soul-contract.sh section B explicitly fails the
|
||||
// build for ("to delete is to supersede/tombstone, never hard-remove"). Local
|
||||
// deletes therefore stay local; the TOMBSTONE NODE and its "tombstones" edge
|
||||
// (mem_tombstone) are pushed, and that is the sanctioned representation of a
|
||||
// deletion in this graph.
|
||||
|
||||
// ── Configuration ─────────────────────────────────────────────────────────────
|
||||
|
||||
// wt_engram_url — same resolution order as ise_post: env, then the state key
|
||||
// stashed at boot. NO hardcoded localhost fallback: unlike telemetry, inventing
|
||||
// a destination for durable data would risk pushing a user's memories at whatever
|
||||
// happens to be listening on 8742. Empty means "no HTTP owner" -> file mode.
|
||||
fn wt_engram_url() -> String {
|
||||
let env_url: String = env("ENGRAM_URL")
|
||||
if !str_eq(env_url, "") { return env_url }
|
||||
return state_get("soul_engram_url")
|
||||
}
|
||||
|
||||
fn wt_api_key() -> String {
|
||||
let env_key: String = env("ENGRAM_API_KEY")
|
||||
if !str_eq(env_key, "") { return env_key }
|
||||
return state_get("soul_engram_api_key")
|
||||
}
|
||||
|
||||
// wt_enabled — true only in HTTP-engram mode. In file mode the soul IS the
|
||||
// persistence owner and every path below is a no-op, so this whole feature is
|
||||
// inert for genesis/local deployments. That is also what makes it reversible.
|
||||
fn wt_enabled() -> Bool {
|
||||
return !str_eq(wt_engram_url(), "")
|
||||
}
|
||||
|
||||
// wt_spool_dir — where staged deltas live. MUST be readable by the engram
|
||||
// process: /api/load-merge takes a PATH and the owner opens it itself. Both
|
||||
// processes are same-host by construction (dev-stack LaunchAgents; the GKE
|
||||
// image starts engram and soul in one container per entrypoint.sh).
|
||||
fn wt_spool_dir() -> String {
|
||||
let raw: String = env("SOUL_OUTBOX_DIR")
|
||||
let dir: String = if str_eq(raw, "") { env("HOME") + "/.neuron/soul-outbox" } else { raw }
|
||||
fs_mkdir(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// wt_esc — minimal JSON string escape. Deliberately local rather than reusing
|
||||
// chat.el's json_safe: persist.el is imported BY memory.el, which is imported by
|
||||
// chat.el, so depending on chat.el here would be an import cycle.
|
||||
fn wt_esc(s: String) -> String {
|
||||
let s1: String = str_replace(s, "\\", "\\\\")
|
||||
let s2: String = str_replace(s1, "\"", "\\\"")
|
||||
let s3: String = str_replace(s2, "\n", "\\n")
|
||||
let s4: String = str_replace(s3, "\r", "\\r")
|
||||
let s5: String = str_replace(s4, "\t", "\\t")
|
||||
return s5
|
||||
}
|
||||
|
||||
// wt_durable_class — Will's telemetry carve-out, by node_type. See header.
|
||||
fn wt_durable_class(node_type: String) -> Bool {
|
||||
if str_eq(node_type, "InternalStateEvent") { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// wt_inner — strip the surrounding brackets off a JSON array so several arrays
|
||||
// can be concatenated into one. Returns "" for "[]" / "" / anything too short.
|
||||
fn wt_inner(arr: String) -> String {
|
||||
let n: Int = str_len(arr)
|
||||
if n < 3 { return "" }
|
||||
if !str_starts_with(arr, "[") { return "" }
|
||||
return str_slice(arr, 1, n - 1)
|
||||
}
|
||||
|
||||
// wt_read — fs_read, plus a MANDATORY reset of the runtime's binary-length hint.
|
||||
//
|
||||
// THIS IS NOT OPTIONAL AND MUST NOT BE "SIMPLIFIED" BACK TO A BARE fs_read.
|
||||
// The pinned runtime (vendor/el-runtime/v1.0.0-20260501) keeps a thread-local
|
||||
// `_tl_fs_read_len` that fs_read SETS to the file's byte count (so binary files
|
||||
// can be served with a correct Content-Length) and that http_send_response
|
||||
// CONSUMES as the Content-Length of the next reply. Nothing else clears it
|
||||
// except json_get_raw. So any fs_read during request handling that is not
|
||||
// followed by a json_get_raw makes the NEXT HTTP response advertise the FILE's
|
||||
// length instead of the body's — and the runtime then sends that many bytes,
|
||||
// appending whatever adjacent heap memory follows the reply.
|
||||
//
|
||||
// Caught here, measured: a /api/neuron/memory reply that should be 86 bytes went
|
||||
// out as 497, with 411 bytes of this module's own spool paths and log strings
|
||||
// trailing the JSON. The drain reads spool files mid-request, so this boundary
|
||||
// is exactly where the landmine gets stepped on.
|
||||
//
|
||||
// Upstream el fixed the class in `43636ae` ("pair fs_read length hint with its
|
||||
// buffer"); that runtime is NOT the one vendored here, and re-pinning the
|
||||
// runtime is deliberately out of scope for this change. Clearing the hint at
|
||||
// our own boundary fixes our exposure without touching the pinned C.
|
||||
// json_get_raw is used as the reset because it is the only builtin in this
|
||||
// runtime that zeroes the hint, and it does so before any early return.
|
||||
fn wt_clear_binlen() -> Void {
|
||||
let discard: String = json_get_raw("{}", "_wt_reset")
|
||||
}
|
||||
|
||||
fn wt_read(path: String) -> String {
|
||||
let data: String = fs_read(path)
|
||||
wt_clear_binlen()
|
||||
return data
|
||||
}
|
||||
|
||||
// wt_sweep — best-effort removal of the zero-byte husks left by truncation.
|
||||
// The runtime exposes no unlink builtin, so a drained delta is emptied rather
|
||||
// than deleted; this reclaims the directory entries.
|
||||
//
|
||||
// `-empty` is the safety property, not an optimisation: the command is
|
||||
// STRUCTURALLY INCAPABLE of removing a delta that still has content, so it can
|
||||
// never destroy a pending write even if it runs concurrently with a stage.
|
||||
// Only the directory path is interpolated (never a filename), and it is quoted.
|
||||
// The exit code is ignored — an un-swept husk costs one directory entry.
|
||||
fn wt_sweep(dir: String) -> Void {
|
||||
if str_eq(dir, "") { return }
|
||||
if str_contains(dir, "'") { return }
|
||||
exec_command("find '" + dir + "' -maxdepth 1 -name 'wt*.json' -empty -delete 2>/dev/null")
|
||||
}
|
||||
|
||||
// ── Staging ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// wt_stage — write ONE delta file. uuid_v4 in the name makes concurrent stagers
|
||||
// collision-free without any lock. Returns true if the delta is on disk.
|
||||
fn wt_stage(nodes_json: String, edges_json: String) -> Bool {
|
||||
let dir: String = wt_spool_dir()
|
||||
if str_eq(dir, "") { return false }
|
||||
let payload: String = "{\"nodes\":" + nodes_json + ",\"edges\":" + edges_json + "}"
|
||||
let path: String = dir + "/wt-" + uuid_v4() + ".json"
|
||||
fs_write(path, payload)
|
||||
// Read-back-verify the stage itself. A stage that did not land is a write we
|
||||
// would otherwise believe was queued — exactly the hallucinated-save class.
|
||||
if str_eq(wt_read(path), "") {
|
||||
println("[persist] wt_stage: FAILED to write spool file " + path + " — delta not queued")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ── The write boundary ────────────────────────────────────────────────────────
|
||||
|
||||
// wt_node — create a node locally AND queue it for the persistence owner.
|
||||
// Same signature and same return contract as engram_node_full ("" on failure),
|
||||
// so converting a call site is a rename and nothing else.
|
||||
fn wt_node(content: String, node_type: String, label: String,
|
||||
salience: Float, importance: Float, confidence: Float,
|
||||
tier: String, tags: String) -> String {
|
||||
let id: String = engram_node_full(content, node_type, label,
|
||||
salience, importance, confidence,
|
||||
tier, tags)
|
||||
if str_eq(id, "") { return "" }
|
||||
// engram_get_node_json emits the SAME record shape engram_save writes (minus
|
||||
// the embedding vector, which the owner backfills lazily), so the read-back
|
||||
// doubles as the delta payload — no second serialization to drift.
|
||||
let rec: String = engram_get_node_json(id)
|
||||
if str_eq(rec, "") || str_eq(rec, "{}") {
|
||||
println("[persist] wt_node: local write did not read back, id=" + id + " label=" + label)
|
||||
return ""
|
||||
}
|
||||
if wt_enabled() && wt_durable_class(node_type) {
|
||||
wt_stage("[" + rec + "]", "[]")
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// wt_edge — create an edge locally AND queue it. Mirrors engram_connect.
|
||||
//
|
||||
// The edge id is freshly generated rather than read back: the runtime exposes no
|
||||
// "id of the edge I just created" accessor, and the owner dedups edges by
|
||||
// (from_id,to_id,relation), never by id — so the id is not load-bearing. The
|
||||
// consequence, stated plainly: the soul's copy and the owner's copy of the same
|
||||
// edge carry different edge ids. Nothing in either codebase looks an edge up by
|
||||
// id (neighbors traversal scans from_id/to_id), so this is cosmetic.
|
||||
fn wt_edge(from_id: String, to_id: String, weight: Float, relation: String) -> Void {
|
||||
engram_connect(from_id, to_id, weight, relation)
|
||||
if !wt_enabled() { return }
|
||||
if str_eq(from_id, "") || str_eq(to_id, "") { return }
|
||||
let ts: Int = time_now()
|
||||
let rec: String = "{\"id\":\"" + uuid_v4() + "\""
|
||||
+ ",\"from_id\":\"" + wt_esc(from_id) + "\""
|
||||
+ ",\"to_id\":\"" + wt_esc(to_id) + "\""
|
||||
+ ",\"relation\":\"" + wt_esc(relation) + "\""
|
||||
+ ",\"metadata\":\"{}\""
|
||||
+ ",\"weight\":" + float_to_str(weight)
|
||||
+ ",\"confidence\":1"
|
||||
+ ",\"created_at\":" + int_to_str(ts)
|
||||
+ ",\"updated_at\":" + int_to_str(ts)
|
||||
+ ",\"last_fired\":0,\"inhibitory\":0,\"layer_id\":1}"
|
||||
wt_stage("[]", "[" + rec + "]")
|
||||
}
|
||||
|
||||
// ── The drain ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// wt_drain — coalesce every staged delta into ONE load-merge against the owner.
|
||||
//
|
||||
// Returns: nodes_added on success (>= 0), 0 when there was nothing to do, and
|
||||
// -1 when the push FAILED. -1 is load-bearing: on failure the spool files are
|
||||
// left untouched, so nothing is lost and the next drain retries them. A caller
|
||||
// must never read a non-negative return as "my particular node is durable" —
|
||||
// use wt_durable(id) for that.
|
||||
//
|
||||
// Concurrency: several threads may drain at once. Each builds its own batch file
|
||||
// (uuid-named), and overlapping batches are harmless because load-merge dedups.
|
||||
// Files are truncated ONLY after a confirmed ok:true, so a lost race costs a
|
||||
// redundant push, never a dropped write.
|
||||
fn wt_drain() -> Int {
|
||||
if !wt_enabled() { return 0 }
|
||||
let dir: String = wt_spool_dir()
|
||||
if str_eq(dir, "") { return 0 }
|
||||
|
||||
// el_list_len/el_list_get, NOT json_stringify(fs_list(...)): fs_list builds
|
||||
// a native list via el_list_append, and json_stringify does not serialize
|
||||
// that type — it renders the raw pointer value. (Verified in isolation; the
|
||||
// same latent defect is live in studio.el's /api/tools/file/list route,
|
||||
// which returns e.g. {"entries":4386409744}. Noted, not fixed here.)
|
||||
let listing = fs_list(dir)
|
||||
let count: Int = el_list_len(listing)
|
||||
if count == 0 { return 0 }
|
||||
|
||||
let nodes_acc: String = ""
|
||||
let edges_acc: String = ""
|
||||
let drained: String = ""
|
||||
let found: Int = 0
|
||||
let i: Int = 0
|
||||
// No `continue` / `break`: elc lists them as keywords but not one line of
|
||||
// the shipped soul uses either, so they are unexercised on this build path.
|
||||
// Guard conditions are expressed as nested ifs instead, and every rebind is
|
||||
// at the loop-body top level where `let x = ...` is assignment (the idiom
|
||||
// memory.el's boot-counter loop relies on) — never inside a nested block,
|
||||
// where it would shadow instead.
|
||||
while i < count {
|
||||
let name: String = el_list_get(listing, i)
|
||||
// A delta is only usable when it ends with the closing "]}" that
|
||||
// wt_stage writes last. fs_write is not atomic, so a file being written
|
||||
// right now can be observed half-formed; requiring the terminator means
|
||||
// it is picked up whole on the next drain instead of merged as garbage.
|
||||
// An empty read means "already drained and truncated" — not an error.
|
||||
let p: String = if str_starts_with(name, "wt-") { dir + "/" + name } else { "" }
|
||||
let raw: String = if str_eq(p, "") { "" } else { wt_read(p) }
|
||||
let usable: Bool = !str_eq(raw, "") && str_ends_with(raw, "]}")
|
||||
let nj: String = if usable { wt_inner(json_get_raw(raw, "nodes")) } else { "" }
|
||||
let ej: String = if usable { wt_inner(json_get_raw(raw, "edges")) } else { "" }
|
||||
let nodes_acc = if str_eq(nj, "") { nodes_acc } else if str_eq(nodes_acc, "") { nj } else { nodes_acc + "," + nj }
|
||||
let edges_acc = if str_eq(ej, "") { edges_acc } else if str_eq(edges_acc, "") { ej } else { edges_acc + "," + ej }
|
||||
let drained = if !usable { drained } else if str_eq(drained, "") { p } else { drained + "\n" + p }
|
||||
let found = if usable { found + 1 } else { found }
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
if found == 0 { return 0 }
|
||||
|
||||
let combined: String = "{\"nodes\":[" + nodes_acc + "],\"edges\":[" + edges_acc + "]}"
|
||||
let batch: String = dir + "/wtb-" + uuid_v4() + ".json"
|
||||
fs_write(batch, combined)
|
||||
if str_eq(wt_read(batch), "") {
|
||||
println("[persist] wt_drain: could not write batch file " + batch + " — " + int_to_str(found) + " deltas stay queued")
|
||||
return -1
|
||||
}
|
||||
|
||||
let url: String = wt_engram_url()
|
||||
let key: String = wt_api_key()
|
||||
let body: String = "{\"path\":\"" + wt_esc(batch) + "\",\"_auth\":\"" + wt_esc(key) + "\"}"
|
||||
let resp: String = http_post_json(url + "/api/load-merge", body)
|
||||
|
||||
// The batch file is pure scratch — the retry is rebuilt from the SPOOL, not
|
||||
// from it. Truncate it unconditionally, before branching on the outcome, so
|
||||
// a persistently unreachable owner cannot accumulate one husk per attempt.
|
||||
fs_write(batch, "")
|
||||
|
||||
// Distinguish the two failures rather than collapsing them: "cannot reach
|
||||
// the owner" and "the owner refused this delta" need different human
|
||||
// responses, and a log line that says the wrong one costs a debugging hour.
|
||||
// curl surfaces transport errors as a JSON body, so an empty response is not
|
||||
// the only unreachable signal.
|
||||
// (str_contains rather than a strict parse on purpose — the engram's HTTP
|
||||
// responses have been observed carrying trailing bytes past the JSON.)
|
||||
let unreachable: Bool = str_eq(resp, "")
|
||||
|| str_contains(resp, "Couldn't connect")
|
||||
|| str_contains(resp, "Failed to connect")
|
||||
|| str_contains(resp, "Could not resolve")
|
||||
|| str_contains(resp, "timed out")
|
||||
if unreachable {
|
||||
wt_sweep(dir)
|
||||
println("[persist] wt_drain: owner UNREACHABLE at " + url + " — " + int_to_str(found)
|
||||
+ " deltas stay queued in " + dir + " (will retry): " + resp)
|
||||
return -1
|
||||
}
|
||||
if !str_contains(resp, "\"ok\":true") {
|
||||
wt_sweep(dir)
|
||||
println("[persist] wt_drain: owner REJECTED the delta — " + int_to_str(found)
|
||||
+ " stay queued in " + dir + ": " + resp)
|
||||
return -1
|
||||
}
|
||||
|
||||
let added: Int = json_get_int(resp, "nodes_added")
|
||||
let added_e: Int = json_get_int(resp, "edges_added")
|
||||
|
||||
// Confirmed. Truncate the drained spool files so they are not re-pushed.
|
||||
// Truncation (not deletion) because the runtime exposes no unlink builtin;
|
||||
// an emptied file is inert to the loop above. The zero-byte husks are then
|
||||
// swept below.
|
||||
let paths = str_split(drained, "\n")
|
||||
let pn: Int = el_list_len(paths)
|
||||
let k: Int = 0
|
||||
while k < pn {
|
||||
let one: String = el_list_get(paths, k)
|
||||
if !str_eq(one, "") { fs_write(one, "") }
|
||||
let k = k + 1
|
||||
}
|
||||
wt_sweep(dir)
|
||||
|
||||
println("[persist] wt_drain: pushed " + int_to_str(found) + " deltas -> owner added "
|
||||
+ int_to_str(added) + " nodes, " + int_to_str(added_e) + " edges")
|
||||
return added
|
||||
}
|
||||
|
||||
// wt_durable — is this id present AT THE OWNER? The only honest answer to
|
||||
// "did my write persist" in HTTP mode.
|
||||
//
|
||||
// In file mode the soul IS the owner, so the local read-back is the owner-side
|
||||
// read-back and this collapses to the pre-existing check.
|
||||
//
|
||||
// nodes_added from wt_drain is NOT a substitute: a concurrent drain may have
|
||||
// already pushed this node, making our own added count 0 while the node is
|
||||
// perfectly durable. Presence at the owner is the fact; counts are telemetry.
|
||||
fn wt_durable(id: String) -> Bool {
|
||||
if str_eq(id, "") { return false }
|
||||
if !wt_enabled() {
|
||||
let local: String = engram_get_node_json(id)
|
||||
return !str_eq(local, "") && !str_eq(local, "null") && !str_eq(local, "{}")
|
||||
}
|
||||
let url: String = wt_engram_url()
|
||||
let resp: String = http_get(url + "/api/nodes/" + id)
|
||||
if str_eq(resp, "") { return false }
|
||||
if str_eq(resp, "{}") { return false }
|
||||
return str_contains(resp, "\"id\"")
|
||||
}
|
||||
|
||||
// wt_commit — flush, then assert at the owner. The receipt callers should use.
|
||||
// Deliberately NOT a fixed success shape: it can and does return false while the
|
||||
// local write is perfectly fine in RAM, which is the true state of affairs when
|
||||
// the owner is unreachable.
|
||||
fn wt_commit(id: String) -> Bool {
|
||||
if str_eq(id, "") { return false }
|
||||
if !wt_enabled() {
|
||||
let local: String = engram_get_node_json(id)
|
||||
return !str_eq(local, "") && !str_eq(local, "null") && !str_eq(local, "{}")
|
||||
}
|
||||
let pushed: Int = wt_drain()
|
||||
return wt_durable(id)
|
||||
}
|
||||
@@ -186,7 +186,7 @@ fn route_imprint_contextual(body: String) -> String {
|
||||
return "{\"ok\":false,\"error\":\"empty body\"}"
|
||||
}
|
||||
let tags: String = "[\"imprint\",\"contextual\"]"
|
||||
let id: String = engram_node_full(
|
||||
let id: String = wt_node(
|
||||
body,
|
||||
"Entity",
|
||||
"imprint:contextual",
|
||||
@@ -208,7 +208,7 @@ fn route_imprint_user(body: String) -> String {
|
||||
return "{\"ok\":false,\"error\":\"empty body\"}"
|
||||
}
|
||||
let tags: String = "[\"imprint\",\"user\"]"
|
||||
let id: String = engram_node_full(
|
||||
let id: String = wt_node(
|
||||
body,
|
||||
"Entity",
|
||||
"imprint:user",
|
||||
@@ -239,7 +239,7 @@ fn route_synthesize(body: String) -> String {
|
||||
}
|
||||
let req: String = "synthesize " + parent_a + " " + parent_b
|
||||
let tags: String = "[\"soul-inbox-pending\",\"synthesis-request\"]"
|
||||
engram_node_full(
|
||||
wt_node(
|
||||
req,
|
||||
"Entity",
|
||||
"synthesis-request",
|
||||
@@ -395,7 +395,28 @@ fn handle_connectors(method: String, clean: String, body: String) -> String {
|
||||
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
|
||||
}
|
||||
|
||||
// handle_request — the soul's HTTP entry point.
|
||||
//
|
||||
// NOTE ON THE NAME (neuron#117): the el runtime resolves this handler by NAME
|
||||
// via dlsym(RTLD_DEFAULT, "handle_request") — that is why the Linux build must
|
||||
// link -rdynamic. So the dispatcher body moved to route_dispatch and the name
|
||||
// `handle_request` stays put as a thin wrapper. Do not rename it back.
|
||||
//
|
||||
// The wrapper exists to give the write-through boundary a guaranteed flush
|
||||
// point. route_dispatch returns from ~60 places; a per-branch flush would be
|
||||
// forgotten on the 61st. Draining here means EVERY request that staged a write
|
||||
// pushes it before the connection closes, whatever route produced it, including
|
||||
// routes added later that know nothing about persistence.
|
||||
//
|
||||
// wt_drain is a no-op (no HTTP, no cost) when nothing is staged and when the
|
||||
// soul is not in HTTP-engram mode, so this is free on read traffic.
|
||||
fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let resp: String = route_dispatch(method, path, body)
|
||||
let flushed: Int = wt_drain()
|
||||
return resp
|
||||
}
|
||||
|
||||
fn route_dispatch(method: String, path: String, body: String) -> String {
|
||||
let clean: String = strip_query(path)
|
||||
|
||||
// ACTIVITY STAMP (2026-07-30 self-review): every inbound HTTP request —
|
||||
@@ -432,10 +453,27 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
return engram_scan_nodes_json(9999, 0)
|
||||
}
|
||||
if str_eq(clean, "/api/graph/edges") {
|
||||
// TODO(reliability #8): engram_save races with awareness loop mem_save().
|
||||
// Both now use atomic write-to-temp+rename (el_runtime.c). Serialised
|
||||
// by engram_global_mu. Future: add engram_edges_json() builtin.
|
||||
let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.json"
|
||||
// FIXED (neuron#117): this GET used to engram_save() straight over
|
||||
// ~/.neuron/engram/snapshot.json — a READ route, in a process that is
|
||||
// NOT the persistence owner, overwriting the owner's canonical file
|
||||
// on every call. It broke soul.el:571-573 ("the soul must NEVER write
|
||||
// to the local snapshot") and it is the same defect class Will removed
|
||||
// from the engram itself in el `dc39a61` ("stop read routes clobbering
|
||||
// canonical snapshot"), where route_scan_edges/route_sync were moved
|
||||
// to scratch paths for exactly this reason. It was also the race the
|
||||
// old TODO(reliability #8) admitted to.
|
||||
//
|
||||
// Export to a scratch path instead. Same response, no canonical write.
|
||||
// The soul's own snapshot writes are otherwise already gated behind
|
||||
// state key "soul_snapshot_path", which is set ONLY in the genesis
|
||||
// file-mode branch (soul.el: is_genesis && safe_to_seed, and
|
||||
// safe_to_seed is unconditionally false when ENGRAM_URL is set) — so
|
||||
// after this change the soul writes nothing at all in HTTP mode.
|
||||
// Future: add an engram_edges_json() builtin and drop the file round
|
||||
// trip entirely.
|
||||
let scratch_dir: String = env("TMPDIR")
|
||||
let scratch_base: String = if str_eq(scratch_dir, "") { "/tmp" } else { scratch_dir }
|
||||
let snap_path: String = scratch_base + "/soul-edges-export-" + state_get("soul_cgi_id") + ".json"
|
||||
engram_save(snap_path)
|
||||
let snap: String = fs_read(snap_path)
|
||||
let edges_raw: String = json_get_raw(snap, "edges")
|
||||
@@ -529,6 +567,13 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_starts_with(clean, "/api/neuron/graph") {
|
||||
return handle_api_inspect_graph(method, path, body)
|
||||
}
|
||||
// Stage 1 structural audit (CGI provisional, "Structural audit 430").
|
||||
// GET because it is a read of the graph's own structure; the query string
|
||||
// carries the sample caps (?edge_sample=, ?node_sample=, ?edges=0), so
|
||||
// str_starts_with rather than str_eq.
|
||||
if str_starts_with(clean, "/api/neuron/audit/structural") {
|
||||
return handle_api_structural_audit(method, path, body)
|
||||
}
|
||||
if str_starts_with(clean, "/api/neuron/list/") {
|
||||
// Offset 17 = len("/api/neuron/list/"). Was 16, which left a leading "/" on node_type
|
||||
// ("/BacklogItem"), so engram_scan_nodes_by_type_json matched nothing → list/<type>
|
||||
@@ -710,6 +755,12 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(clean, "/api/neuron/graph/link") {
|
||||
return handle_api_link_entities(body)
|
||||
}
|
||||
// POST accepted too: same handler, so a JSON-RPC-shaped caller that only
|
||||
// speaks POST reaches the identical audit. Options still come from the
|
||||
// query string — the handler reads no body fields.
|
||||
if str_eq(clean, "/api/neuron/audit/structural") {
|
||||
return handle_api_structural_audit(method, path, body)
|
||||
}
|
||||
if str_eq(clean, "/api/neuron/memory") {
|
||||
return handle_api_remember(body)
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ fn safety_log_bell(level: String, reason: String, input_summary: String) -> Stri
|
||||
// Emit a fallback println so the bell event leaves at least a log trace even
|
||||
// when engram is degraded. This does not replace engram persistence -- it is a
|
||||
// last-resort audit trail when the primary write cannot be confirmed.
|
||||
let node_id: String = engram_node_full(
|
||||
let node_id: String = wt_node(
|
||||
content,
|
||||
"BellEvent",
|
||||
"bell:" + level,
|
||||
|
||||
+7
-7
@@ -87,7 +87,7 @@ fn session_create(body: String) -> String {
|
||||
let folder: String = json_get(body, "folder")
|
||||
let content: String = session_make_content(id, title, ts, ts, folder)
|
||||
let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]"
|
||||
let node_id: String = engram_node_full(
|
||||
let node_id: String = wt_node(
|
||||
content, "Conversation", "session:meta",
|
||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||
"Episodic", tags
|
||||
@@ -358,7 +358,7 @@ fn session_update_patch(session_id: String, body: String) -> String {
|
||||
let created_int: Int = str_to_int(old_created)
|
||||
let new_content: String = session_make_content(session_id, eff_title, created_int, ts, eff_folder)
|
||||
let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]"
|
||||
let new_node_id: String = engram_node_full(
|
||||
let new_node_id: String = wt_node(
|
||||
new_content, "Conversation", "session:meta",
|
||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||
"Episodic", tags
|
||||
@@ -456,7 +456,7 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
|
||||
// TODO(reliability #7): delete-then-insert is not atomic — concurrent saves for the
|
||||
// same session can produce orphan history nodes. State is primary truth; engram fallback.
|
||||
let tags: String = "[\"session\",\"session-history\",\"Conversation\"]"
|
||||
let discard: String = engram_node_full(
|
||||
let discard: String = wt_node(
|
||||
hist, "Conversation", "session:messages:" + session_id,
|
||||
el_from_float(0.6), el_from_float(0.6), el_from_float(0.9),
|
||||
"Episodic", tags
|
||||
@@ -488,7 +488,7 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
|
||||
+ " | ts:" + int_to_str(ts_now)
|
||||
let summary_tags: String = "[\"session-emotional-summary\",\"affective\",\"bell:" + eff_level + "\",\"BellEvent\"]"
|
||||
let summary_sal: String = if str_eq(eff_level, "hard") { el_from_float(0.95) } else { el_from_float(0.85) }
|
||||
let sum_discard: String = engram_node_full(
|
||||
let sum_discard: String = wt_node(
|
||||
summary_content,
|
||||
"BellEvent",
|
||||
"session:emotional-summary",
|
||||
@@ -529,7 +529,7 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
|
||||
if !str_eq(ot_id, "") { engram_forget(ot_id) }
|
||||
let oti = oti + 1
|
||||
}
|
||||
let discard_topic: String = engram_node_full(
|
||||
let discard_topic: String = wt_node(
|
||||
topic_content, "Conversation", topic_label,
|
||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||
"Episodic", topic_tags
|
||||
@@ -582,7 +582,7 @@ fn session_update_meta_timestamp(session_id: String) -> Void {
|
||||
let created_int: Int = str_to_int(old_created)
|
||||
let new_content: String = session_make_content(session_id, old_title, created_int, ts, old_folder)
|
||||
let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]"
|
||||
let new_id: String = engram_node_full(
|
||||
let new_id: String = wt_node(
|
||||
new_content, "Conversation", "session:meta",
|
||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||
"Episodic", tags
|
||||
@@ -629,7 +629,7 @@ fn session_auto_title(session_id: String, first_message: String) -> Void {
|
||||
let created_int: Int = str_to_int(old_created)
|
||||
let new_content: String = session_make_content(session_id, new_title, created_int, ts, old_folder)
|
||||
let tags: String = "[\"session\",\"session:meta\",\"Conversation\"]"
|
||||
let new_id: String = engram_node_full(
|
||||
let new_id: String = wt_node(
|
||||
new_content, "Conversation", "session:meta",
|
||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||
"Episodic", tags
|
||||
|
||||
@@ -559,6 +559,27 @@ let axon_base: String = if str_eq(axon_raw, "") { "http://localhost:7771" } else
|
||||
let studio_dir_raw: String = env("SOUL_STUDIO_DIR")
|
||||
let studio_dir: String = if str_eq(studio_dir_raw, "") { env("HOME") + "/Development/neuron-technologies/products/cgi-studio/el-daemon" } else { studio_dir_raw }
|
||||
|
||||
// RESTORED 2026-08-09 — this producer was added 2026-05-02 in 601e0fe and deleted
|
||||
// by the awareness refactor b163fa6 a few days later. Nothing has written
|
||||
// soul_identity since, while FIVE sites in chat.el kept reading it:
|
||||
// chat.el:737, 1745, 2620, 3425, 3480 — each doing state_get("soul_identity")
|
||||
// and splicing the result into the system prompt beside the voice, security and
|
||||
// capability rules. They have been splicing an EMPTY STRING for roughly three
|
||||
// months. The identity section of every chat turn was blank and nothing said so.
|
||||
//
|
||||
// Found by the #132 state-key gate, which reports a read with no producer as a
|
||||
// build error rather than a silence — the whole reason that gate exists.
|
||||
//
|
||||
// Restored verbatim rather than improved: this key is an env-configurable persona
|
||||
// LINE, which is NOT the same thing as soul_identity_context (the graph-derived
|
||||
// [INTELLECTUAL-DNA]/[VALUES]/[MEMORY-PHILOSOPHY] block written at soul.el:184).
|
||||
// Pointing these five reads at that block instead would have substituted different
|
||||
// content and called it a fix. Whether the chat system prompt should ALSO carry the
|
||||
// graph-derived block is a real question, and a separate one.
|
||||
let identity_raw: String = env("SOUL_IDENTITY")
|
||||
let soul_identity: String = if str_eq(identity_raw, "") { "You are " + soul_cgi_id + ", a CGI." } else { identity_raw }
|
||||
state_set("soul_identity", soul_identity)
|
||||
|
||||
println("[soul] boot - cgi=" + soul_cgi_id + " port=" + int_to_str(port))
|
||||
|
||||
let using_http_engram: Bool = !str_eq(engram_url_raw, "")
|
||||
@@ -657,6 +678,23 @@ if is_genesis && safe_to_seed {
|
||||
}
|
||||
}
|
||||
|
||||
// CRASH RECOVERY (neuron#117). Deltas the previous process staged but could not
|
||||
// hand to the owner are still on disk — the spool is a filesystem queue, not a
|
||||
// memory buffer, precisely so that a soul that died mid-flight does not take its
|
||||
// unpushed writes with it. Drain them before serving, so recovered memories are
|
||||
// durable and recallable from the owner from the first request onward.
|
||||
//
|
||||
// Safe on a clean boot: an empty spool means no HTTP call at all. Safe in file
|
||||
// mode: wt_drain returns immediately when ENGRAM_URL is unset.
|
||||
let wt_recovered: Int = wt_drain()
|
||||
if wt_recovered > 0 {
|
||||
println("[soul] write-through: recovered " + int_to_str(wt_recovered)
|
||||
+ " nodes from a previous process's spool -> persistence owner")
|
||||
}
|
||||
if wt_recovered < 0 {
|
||||
println("[soul] write-through: spool present but the persistence owner is unreachable — queued, will retry on heartbeat")
|
||||
}
|
||||
|
||||
println("[soul] serving on port " + int_to_str(port))
|
||||
http_serve_async(port, "handle_request")
|
||||
println("[soul] awareness loop starting")
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import "memory.el"
|
||||
fn steward_log_event(kind: String, detail: String) -> Void {
|
||||
let content: String = "STEWARD:" + kind + " | " + detail
|
||||
let tags: String = "[\"stewardship\",\"steward:" + kind + "\"]"
|
||||
let discard: String = engram_node_full(
|
||||
let discard: String = wt_node(
|
||||
content,
|
||||
"StewardshipEvent",
|
||||
"steward:" + kind,
|
||||
@@ -221,7 +221,7 @@ fn steward_fingerprint_session(input: String, session_id: String) -> String {
|
||||
+ " formality=" + fs_str
|
||||
+ " time=" + tb_str
|
||||
let sample_tags: String = "[\"behavior\",\"BehaviorSample\",\"stewardship\"]"
|
||||
let discard: String = engram_node_full(
|
||||
let discard: String = wt_node(
|
||||
sample_content,
|
||||
"BehaviorSample",
|
||||
"behavior:" + session_id,
|
||||
|
||||
@@ -53,8 +53,23 @@ fn handle_config(method: String, body: String) -> String {
|
||||
}
|
||||
|
||||
fn dharma_registry() -> String {
|
||||
// COMPILED IDENTITY, not state (2026-08-09). soul_principal had no producer at
|
||||
// all — the #132 gate flagged it as a dead read and the registry reported an
|
||||
// empty principal under a heading that says "Principal Covenant v1". The value
|
||||
// was never missing: it is declared in soul.el's cgi block, and as of the
|
||||
// codegen fix it is compiled into the binary and loaded at startup.
|
||||
//
|
||||
// Read it from the compiled constant rather than the state store. The design is
|
||||
// explicit that this identity is "not modifiable by any runtime mechanism
|
||||
// including environment variables, configuration files, or API calls" — so
|
||||
// publishing it into state (the cheap fix) would have recreated exactly the
|
||||
// mutable copy it forbids. cgi_principal() is read-only and has no setter.
|
||||
//
|
||||
// cgi_id keeps its state read deliberately: the RUNTIME instance id is a
|
||||
// different fact from the compiled dharma_id, and conflating them would hide
|
||||
// the case where a binary runs under an id its declaration never claimed.
|
||||
let cgi_id: String = state_get("soul_cgi_id")
|
||||
let principal: String = state_get("soul_principal")
|
||||
let principal: String = cgi_principal()
|
||||
return "{\"registry\":[{\"cgi\":\"" + cgi_id + "\","
|
||||
+ "\"principal\":\"" + principal + "\","
|
||||
+ "\"covenant\":\"Principal Covenant v1\","
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-soul-from-dist.sh — build a deployable soul from the SAME input CI compiles.
|
||||
#
|
||||
# THE PROBLEM THIS CLOSES: until now, deploys were built by build-soul.sh, which
|
||||
# compiles a scratch amalgam and never touches dist/soul.c. CI compiles dist/soul.c.
|
||||
# Two lineages. On 2026-08-09 the committed input fell 2,761 bytes behind the sources
|
||||
# while three binaries built the other way were installed on the operator machine —
|
||||
# so "what runs" and "what the repo says builds" were different artifacts again,
|
||||
# which is the whole of #133 and #111 wearing new clothes.
|
||||
#
|
||||
# This builds from dist/soul.c with CI's own flags, after asserting that dist/soul.c
|
||||
# actually matches the .el sources, and writes a provenance sidecar so a deployer can
|
||||
# refuse anything of unknown origin.
|
||||
#
|
||||
# -rdynamic and -DHAVE_CURL are copied from .gitea/workflows/ci.yaml deliberately.
|
||||
# The CI comment explains -rdynamic: without it the runtime cannot resolve its HTTP
|
||||
# handler by name via dlsym and the binary serves nothing on every route.
|
||||
#
|
||||
# usage: build-soul-from-dist.sh <out-binary>
|
||||
set -u
|
||||
OUT="${1:?usage: build-soul-from-dist.sh <out-binary>}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RUNTIME="$ROOT/vendor/el-runtime/v1.0.0-20260501"
|
||||
|
||||
cd "$ROOT" || exit 2
|
||||
|
||||
echo "[build-from-dist] GATE: does dist/soul.c match the sources?"
|
||||
if ! ./tools/soulc-stamp.sh --check; then
|
||||
echo "[build-from-dist] REFUSING — the build input is stale. Regenerate and stamp first." >&2
|
||||
exit 9
|
||||
fi
|
||||
|
||||
[ -f "$RUNTIME/el_runtime.c" ] || { echo "pinned runtime missing at $RUNTIME" >&2; exit 2; }
|
||||
|
||||
echo "[build-from-dist] compiling dist/soul.c with CI's flags"
|
||||
cc -O2 -DHAVE_CURL -rdynamic \
|
||||
-I"$RUNTIME" \
|
||||
dist/soul.c \
|
||||
"$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm \
|
||||
-o "$OUT" || { echo "[build-from-dist] COMPILE FAILED" >&2; exit 3; }
|
||||
|
||||
# Provenance sidecar: what a deployer checks before installing anything.
|
||||
SRC_SHA="$(shasum -a 256 dist/soul.c | awk '{print $1}')"
|
||||
STAMP_SHA="$(shasum -a 256 dist/soul.c.stamp | awk '{print $1}')"
|
||||
COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
DIRTY="clean"; [ -n "$(git status --porcelain -- '*.el' dist/soul.c 2>/dev/null)" ] && DIRTY="DIRTY"
|
||||
cat > "$OUT.provenance" <<EOF
|
||||
{"built_from":"dist/soul.c",
|
||||
"dist_soul_c_sha256":"$SRC_SHA",
|
||||
"stamp_sha256":"$STAMP_SHA",
|
||||
"git_commit":"$COMMIT",
|
||||
"worktree":"$DIRTY",
|
||||
"runtime":"vendor/el-runtime/v1.0.0-20260501",
|
||||
"flags":"-O2 -DHAVE_CURL -rdynamic"}
|
||||
EOF
|
||||
|
||||
echo "[build-from-dist] OK -> $OUT ($(wc -c < "$OUT" | tr -d ' ') bytes)"
|
||||
echo "[build-from-dist] provenance -> $OUT.provenance (commit ${COMMIT:0:8}, worktree $DIRTY)"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# soulc-stamp.sh — make it impossible for dist/soul.c to drift from the sources
|
||||
# in silence.
|
||||
#
|
||||
# THE PROBLEM (neuron#133, and its own words): "Nothing in the tree regenerates
|
||||
# this file. Only a human running the recipe. It lags in batches, never
|
||||
# per-change, and it will drift again."
|
||||
#
|
||||
# It drifted. On 2026-08-07 a CI or GKE build off main would have shipped an
|
||||
# engine with NONE of five merged fixes — including a P0 safety fix — while
|
||||
# main's source read as correct. CI compiles dist/soul.c, not the .el files, so
|
||||
# the source being right is not the same as the build being right.
|
||||
#
|
||||
# WHY A STAMP AND NOT AUTO-REGENERATION: the CI workflow says elc cannot run on
|
||||
# the runner ("elb on Linux would OOM the runner (elc uses 24GB+ virtual memory
|
||||
# on a 16GB host)"). So the build cannot regenerate the file itself. What it CAN
|
||||
# do, for free and with no compiler, is refuse to compile a stale one.
|
||||
#
|
||||
# The stamp records a fingerprint of every .el source that feeds the amalgam at
|
||||
# the moment it was generated. --check recomputes and compares. Divergence is a
|
||||
# build failure with the recipe in the message, not a silent ship.
|
||||
#
|
||||
# soulc-stamp.sh --write after regenerating dist/soul.c (records the fingerprint)
|
||||
# soulc-stamp.sh --check in CI, before the compile (fails on drift)
|
||||
set -u
|
||||
MODE="${1:---check}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
STAMP="$ROOT/dist/soul.c.stamp"
|
||||
AMALGAM="$ROOT/dist/soul.c"
|
||||
|
||||
# Every .el at the repo root is an input to the amalgam. Sorted so the hash is
|
||||
# order-independent; content-only so timestamps and checkouts do not perturb it.
|
||||
# The COMPILER is an input too. Learned 2026-08-09 by installing a fixed elc and
|
||||
# watching this gate report OK while the committed amalgam had gone stale by a line:
|
||||
# the sources had not changed, so a source-only fingerprint could not see it. That is
|
||||
# precisely the blind spot this gate exists to close, and it had it.
|
||||
fingerprint() {
|
||||
(
|
||||
cd "$ROOT" || exit 1
|
||||
ELC_BIN="${ELC:-$HOME/neuron-dev-stack/src/el/lang/dist/platform/elc}"
|
||||
if [ -f "$ELC_BIN" ]; then
|
||||
printf '%s %s\n' "$(shasum -a 256 "$ELC_BIN" | awk '{print $1}')" "__compiler__"
|
||||
else
|
||||
printf '%s %s\n' "MISSING" "__compiler__"
|
||||
fi
|
||||
for f in $(ls -1 *.el 2>/dev/null | sort); do
|
||||
printf '%s %s\n' "$(shasum -a 256 "$f" | awk '{print $1}')" "$f"
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
--write)
|
||||
[ -f "$AMALGAM" ] || { echo "no dist/soul.c to stamp — regenerate it first" >&2; exit 2; }
|
||||
{
|
||||
echo "# soul.c.stamp — fingerprint of the .el sources dist/soul.c was generated from."
|
||||
echo "# Written by tools/soulc-stamp.sh --write. Do not hand-edit."
|
||||
echo "# generated_amalgam_sha256 $(shasum -a 256 "$AMALGAM" | awk '{print $1}')"
|
||||
echo "# generated_amalgam_bytes $(wc -c < "$AMALGAM" | tr -d ' ')"
|
||||
fingerprint
|
||||
} > "$STAMP"
|
||||
echo "stamped $(fingerprint | wc -l | tr -d ' ') sources -> dist/soul.c.stamp"
|
||||
;;
|
||||
|
||||
--check)
|
||||
if [ ! -f "$STAMP" ]; then
|
||||
echo "FAIL: dist/soul.c.stamp is missing — the build input is unverifiable." >&2
|
||||
echo " Regenerate the amalgam, then: tools/soulc-stamp.sh --write" >&2
|
||||
exit 1
|
||||
fi
|
||||
RECORDED="$(grep -v '^#' "$STAMP")"
|
||||
CURRENT="$(fingerprint)"
|
||||
if [ "$RECORDED" = "$CURRENT" ]; then
|
||||
echo "soulc-stamp: OK — dist/soul.c matches the .el sources"
|
||||
exit 0
|
||||
fi
|
||||
echo "FAIL: dist/soul.c is STALE. It does not match the current .el sources." >&2
|
||||
echo "" >&2
|
||||
echo "CI compiles dist/soul.c, not the .el files. Shipping this means shipping" >&2
|
||||
echo "an engine that does not contain the merged source. That is neuron#133," >&2
|
||||
echo "which once hid five merged fixes including a P0 safety fix." >&2
|
||||
echo "" >&2
|
||||
echo "Sources that changed since the amalgam was generated:" >&2
|
||||
diff <(printf '%s\n' "$RECORDED") <(printf '%s\n' "$CURRENT") \
|
||||
| grep -E '^[<>]' | awk '{print " " $1 " " $3}' | sort -u >&2
|
||||
echo "" >&2
|
||||
echo "Fix: regenerate the amalgam, then tools/soulc-stamp.sh --write" >&2
|
||||
exit 1
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "usage: soulc-stamp.sh [--check|--write]" >&2; exit 2 ;;
|
||||
esac
|
||||
+19
@@ -5634,6 +5634,25 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
||||
}
|
||||
|
||||
|
||||
/* ── Compiled-identity accessors (2026-08-09) ─────────────────────────────────
|
||||
* el_cgi_init loads the declaration into these globals at startup and printed
|
||||
* them, and NOTHING read them back out — no accessor existed, and el_cgi_init
|
||||
* writes no state. So a binary carried its declared identity and every consumer
|
||||
* still read it from the mutable state store, which is exactly what IDPROTO
|
||||
* claims 1-2 forbid ("not modifiable by any runtime mechanism including
|
||||
* environment variables, configuration files, or API calls").
|
||||
*
|
||||
* These are READ-ONLY on purpose. There is deliberately no setter: publishing
|
||||
* the values into the state store would have been one line and would have
|
||||
* recreated the mutable copy the design prohibits. A caller can read the
|
||||
* compiled identity; nothing can change it after el_cgi_init.
|
||||
*/
|
||||
el_val_t cgi_name(void) { return EL_STR(_el_cgi_name ? _el_cgi_name : ""); }
|
||||
el_val_t cgi_dharma_id(void) { return EL_STR(_el_cgi_dharma_id ? _el_cgi_dharma_id : ""); }
|
||||
el_val_t cgi_principal(void) { return EL_STR(_el_cgi_principal ? _el_cgi_principal : ""); }
|
||||
el_val_t cgi_network(void) { return EL_STR(_el_cgi_network ? _el_cgi_network : ""); }
|
||||
el_val_t cgi_engram(void) { return EL_STR(_el_cgi_engram ? _el_cgi_engram : ""); }
|
||||
|
||||
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
||||
/*
|
||||
* Single global EngramStore allocated lazily on first call. All node and
|
||||
|
||||
@@ -782,6 +782,14 @@ el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
/* Compiled-identity accessors — read-only by design (2026-08-09). */
|
||||
el_val_t cgi_name(void);
|
||||
el_val_t cgi_dharma_id(void);
|
||||
el_val_t cgi_principal(void);
|
||||
el_val_t cgi_network(void);
|
||||
el_val_t cgi_engram(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user