Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 978a6812d7 | |||
| 18e040acb1 | |||
| 6edf9937dd | |||
| e447a87a00 | |||
| 575ff1329a | |||
| db33b0cb91 | |||
| f35569d4bb | |||
| 94b71b6e6b | |||
| 392d2416ec | |||
| 2865d6ad26 | |||
| 47d0e6f985 | |||
| d008649c3e | |||
| aa70c5dde6 | |||
| deddb9a18e | |||
| 494d973a3b | |||
| 34551695a1 | |||
| 615f0cee08 |
+37
-4
@@ -23,11 +23,14 @@ fn ise_post(content: String) -> Void {
|
|||||||
let ise_url: String = env("SOUL_ISE_URL")
|
let ise_url: String = env("SOUL_ISE_URL")
|
||||||
let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
|
let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
|
||||||
if str_eq(engram_url, "") {
|
if str_eq(engram_url, "") {
|
||||||
let discard: String = engram_node_full(
|
let local_id: String = engram_node_full(
|
||||||
content, "InternalStateEvent", "state-event",
|
content, "InternalStateEvent", "state-event",
|
||||||
el_from_float(0.3), el_from_float(0.3), el_from_float(0.8),
|
el_from_float(0.3), el_from_float(0.3), el_from_float(0.8),
|
||||||
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
||||||
)
|
)
|
||||||
|
if str_eq(local_id, "") {
|
||||||
|
println("[awareness] ise_post: local engram_node_full failed — ISE lost")
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
// Proper JSON string escaping: backslashes first, then quotes, then control chars.
|
// Proper JSON string escaping: backslashes first, then quotes, then control chars.
|
||||||
@@ -40,7 +43,32 @@ fn ise_post(content: String) -> Void {
|
|||||||
let safe3: String = str_replace(safe2, "\n", "\\n")
|
let safe3: String = str_replace(safe2, "\n", "\\n")
|
||||||
let safe4: String = str_replace(safe3, "\r", "\\r")
|
let safe4: String = str_replace(safe3, "\r", "\\r")
|
||||||
let body: String = "{\"content\":\"" + safe4 + "\"}"
|
let body: String = "{\"content\":\"" + safe4 + "\"}"
|
||||||
let discard: String = http_post_json(engram_url + "/api/neuron/state-events", body)
|
// Soft circuit-breaker: skip HTTP call when engram is known-down (30s backoff).
|
||||||
|
// Opens after 3 consecutive failures; half-open probe after backoff expires.
|
||||||
|
// TODO(reliability): full async dispatch requires EL runtime futures support.
|
||||||
|
let cb_open: String = state_get("engram_cb_open")
|
||||||
|
if str_eq(cb_open, "1") {
|
||||||
|
let cb_ts_s: String = state_get("engram_cb_open_ts")
|
||||||
|
let cb_ts: Int = if str_eq(cb_ts_s, "") { 0 } else { str_to_int(cb_ts_s) }
|
||||||
|
let cb_elapsed: Int = time_now() - cb_ts
|
||||||
|
if cb_elapsed < 30000 { return "" }
|
||||||
|
state_set("engram_cb_open", "0")
|
||||||
|
}
|
||||||
|
let resp: String = http_post_json(engram_url + "/api/neuron/state-events", body)
|
||||||
|
let cb_failed: Bool = str_eq(resp, "") || str_starts_with(resp, "{"error":")
|
||||||
|
if cb_failed {
|
||||||
|
let fn_s: String = state_get("engram_cb_fails")
|
||||||
|
let fn_n: Int = if str_eq(fn_s, "") { 0 } else { str_to_int(fn_s) }
|
||||||
|
let fn_n = fn_n + 1
|
||||||
|
state_set("engram_cb_fails", int_to_str(fn_n))
|
||||||
|
if fn_n >= 3 {
|
||||||
|
state_set("engram_cb_open", "1")
|
||||||
|
state_set("engram_cb_open_ts", int_to_str(time_now()))
|
||||||
|
println("[awareness] engram circuit-breaker OPEN after " + int_to_str(fn_n) + " failures")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state_set("engram_cb_fails", "0")
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,9 +568,14 @@ fn awareness_run() -> Void {
|
|||||||
let should_refresh: Bool = refresh_elapsed >= refresh_ms
|
let should_refresh: Bool = refresh_elapsed >= refresh_ms
|
||||||
if should_refresh {
|
if should_refresh {
|
||||||
let engram_url: String = state_get("soul_engram_url")
|
let engram_url: String = state_get("soul_engram_url")
|
||||||
if !str_eq(engram_url, "") {
|
let sc: String = state_get("engram_cb_open")
|
||||||
|
let sc_ts_s: String = state_get("engram_cb_open_ts")
|
||||||
|
let sc_ts: Int = if str_eq(sc_ts_s, "") { 0 } else { str_to_int(sc_ts_s) }
|
||||||
|
let sc_elapsed: Int = now_ts - sc_ts
|
||||||
|
let sync_allowed: Bool = !str_eq(sc, "1") || sc_elapsed >= 30000
|
||||||
|
if !str_eq(engram_url, "") && sync_allowed {
|
||||||
let sync_json: String = http_get(engram_url + "/api/sync")
|
let sync_json: String = http_get(engram_url + "/api/sync")
|
||||||
if !str_eq(sync_json, "") && !str_eq(sync_json, "{}") {
|
if !str_eq(sync_json, "") && !str_eq(sync_json, "{}") && !str_starts_with(sync_json, "{\"error\":") {
|
||||||
let cgi_id: String = state_get("soul_cgi_id")
|
let cgi_id: String = state_get("soul_cgi_id")
|
||||||
let tmp: String = "/tmp/soul-sync-" + cgi_id + ".json"
|
let tmp: String = "/tmp/soul-sync-" + cgi_id + ".json"
|
||||||
fs_write(tmp, sync_json)
|
fs_write(tmp, sync_json)
|
||||||
|
|||||||
+8
-4
@@ -24,19 +24,23 @@ ENGRAM_DATA_DIR="$ENGRAM_DATA_DIR" \
|
|||||||
|
|
||||||
ENGRAM_PID=$!
|
ENGRAM_PID=$!
|
||||||
|
|
||||||
# Wait for engram to become healthy (up to 30s)
|
# Wait for engram to become healthy (up to 60s; GKE Autopilot cold starts can be slow)
|
||||||
echo "[entrypoint] waiting for engram..."
|
echo "[entrypoint] waiting for engram..."
|
||||||
TRIES=0
|
TRIES=0
|
||||||
until curl -sf "$ENGRAM_HEALTH_URL" > /dev/null 2>&1; do
|
until curl -sf "$ENGRAM_HEALTH_URL" > /dev/null 2>&1; do
|
||||||
TRIES=$((TRIES + 1))
|
TRIES=$((TRIES + 1))
|
||||||
if [ "$TRIES" -ge 30 ]; then
|
if [ "$TRIES" -ge 60 ]; then
|
||||||
echo "[entrypoint] ERROR: engram did not become healthy after 30s" >&2
|
echo "[entrypoint] ERROR: engram did not become healthy after 60s" >&2
|
||||||
kill "$ENGRAM_PID" 2>/dev/null || true
|
kill "$ENGRAM_PID" 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
echo "[entrypoint] engram ready"
|
echo "[entrypoint] engram ready after ${TRIES}s"
|
||||||
|
|
||||||
|
# Tune EL HTTP runtime: reduce per-call timeout 60s->10s, connect timeout 3s.
|
||||||
|
export EL_HTTP_TIMEOUT_MS="${EL_HTTP_TIMEOUT_MS:-10000}"
|
||||||
|
export EL_HTTP_CONNECT_TIMEOUT_MS="${EL_HTTP_CONNECT_TIMEOUT_MS:-3000}"
|
||||||
|
|
||||||
# Start soul — it takes over as PID 1's foreground process.
|
# Start soul — it takes over as PID 1's foreground process.
|
||||||
# SOUL_ENGRAM_PATH must NOT be set; ENGRAM_URL triggers HTTP mode.
|
# SOUL_ENGRAM_PATH must NOT be set; ENGRAM_URL triggers HTTP mode.
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ fn mem_consolidate() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mem_save(path: String) -> Void {
|
fn mem_save(path: String) -> Void {
|
||||||
engram_save(path)
|
let save_result: String = engram_save(path)
|
||||||
|
if str_eq(save_result, "") {
|
||||||
|
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mem_load(path: String) -> Void {
|
fn mem_load(path: String) -> Void {
|
||||||
@@ -76,11 +79,14 @@ fn mem_boot_count_inc() -> Int {
|
|||||||
let next: Int = current + 1
|
let next: Int = current + 1
|
||||||
let content: String = "soul:boot_count:" + int_to_str(next)
|
let content: String = "soul:boot_count:" + int_to_str(next)
|
||||||
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
|
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
|
||||||
let discard: String = engram_node_full(
|
let boot_node_id: String = engram_node_full(
|
||||||
content, "Memory", "soul:boot_count",
|
content, "Memory", "soul:boot_count",
|
||||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||||
"Canonical", tags
|
"Canonical", tags
|
||||||
)
|
)
|
||||||
|
if str_eq(boot_node_id, "") {
|
||||||
|
println("[memory] mem_boot_count_inc: engram write failed — boot counter node lost (count=" + int_to_str(next) + ")")
|
||||||
|
}
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -400,6 +400,7 @@ fn handle_api_log_state_event(body: String) -> String {
|
|||||||
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
|
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
|
||||||
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
||||||
"Episodic", tags)
|
"Episodic", tags)
|
||||||
|
if !api_persisted(id) { return api_not_persisted(id) }
|
||||||
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
|
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +453,7 @@ fn handle_api_tune_config(body: String) -> String {
|
|||||||
let id: String = engram_node_full(content, "ConfigEntry", key,
|
let id: String = engram_node_full(content, "ConfigEntry", key,
|
||||||
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
||||||
"Canonical", tags)
|
"Canonical", tags)
|
||||||
|
if !api_persisted(id) { return api_not_persisted(id) }
|
||||||
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
|
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,17 +653,23 @@ fn handle_api_consolidate(body: String) -> String {
|
|||||||
let summary: String = json_get(body, "summary")
|
let summary: String = json_get(body, "summary")
|
||||||
let snap: String = state_get("soul_snapshot_path")
|
let snap: String = state_get("soul_snapshot_path")
|
||||||
if !str_eq(snap, "") {
|
if !str_eq(snap, "") {
|
||||||
engram_save(snap)
|
let save_result: String = engram_save(snap)
|
||||||
|
if str_eq(save_result, "") {
|
||||||
|
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !str_eq(summary, "") {
|
if !str_eq(summary, "") {
|
||||||
let safe_summary: String = str_replace(summary, "\"", "'")
|
let safe_summary: String = str_replace(summary, "\"", "'")
|
||||||
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
||||||
let discard: String = engram_node_full(
|
let summary_id: String = engram_node_full(
|
||||||
"[session-summary] " + safe_summary,
|
"[session-summary] " + safe_summary,
|
||||||
"SessionSummary", "session:summary",
|
"SessionSummary", "session:summary",
|
||||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||||
"Episodic", tags
|
"Episodic", tags
|
||||||
)
|
)
|
||||||
|
if str_eq(summary_id, "") {
|
||||||
|
println("[api] consolidate: session summary engram write failed — summary node lost")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,14 +75,24 @@ fn strip_query(path: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn err_404(path: String) -> String {
|
fn err_404(path: String) -> String {
|
||||||
return "{\"error\":\"not found\",\"code\":\"not_found\",\"path\":\"" + path + "\"}"
|
// __status__ envelope — el_runtime reads the first key and emits HTTP 404.
|
||||||
|
// Issue #3: previously returned HTTP 200 with JSON error body.
|
||||||
|
return "{\"__status__\":404,\"error\":\"not found\",\"path\":\"" + path + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn err_405(method: String, path: String) -> String {
|
fn err_405(method: String, path: String) -> String {
|
||||||
return "{\"error\":\"method not allowed\",\"code\":\"method_not_allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}"
|
// __status__ envelope — emits HTTP 405.
|
||||||
|
// Issue #3: previously returned HTTP 200 with JSON error body.
|
||||||
|
return "{\"__status__\":405,\"error\":\"method not allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route_health() -> String {
|
fn route_health() -> String {
|
||||||
|
// NOTE (issue #8): This endpoint performs live engram graph queries on every call
|
||||||
|
// (engram_node_count, engram_edge_count) and reads imprint state. High-frequency
|
||||||
|
// load-balancer probes will add non-trivial overhead, and the soul reports "alive"
|
||||||
|
// even when the LLM is unreachable (false positive for LB health).
|
||||||
|
// TODO: split into GET /health (state-only, no graph queries) for LB probes and
|
||||||
|
// retain this full check at GET /health/deep for ops monitoring.
|
||||||
let cgi_id: String = state_get("soul_cgi_id")
|
let cgi_id: String = state_get("soul_cgi_id")
|
||||||
let boot: String = state_get("soul_boot_count")
|
let boot: String = state_get("soul_boot_count")
|
||||||
let boot_num: String = if str_eq(boot, "") { "0" } else { boot }
|
let boot_num: String = if str_eq(boot, "") { "0" } else { boot }
|
||||||
@@ -141,7 +151,8 @@ fn route_lineage() -> String {
|
|||||||
|
|
||||||
fn route_imprint_contextual(body: String) -> String {
|
fn route_imprint_contextual(body: String) -> String {
|
||||||
if str_eq(body, "") {
|
if str_eq(body, "") {
|
||||||
return "{\"ok\":false,\"error\":\"empty body\"}"
|
// Issue #5: empty body is a client error — HTTP 400.
|
||||||
|
return "{\"__status__\":400,\"ok\":false,\"error\":\"empty body\"}"
|
||||||
}
|
}
|
||||||
let tags: String = "[\"imprint\",\"contextual\"]"
|
let tags: String = "[\"imprint\",\"contextual\"]"
|
||||||
let id: String = engram_node_full(
|
let id: String = engram_node_full(
|
||||||
@@ -163,7 +174,8 @@ fn route_imprint_contextual(body: String) -> String {
|
|||||||
|
|
||||||
fn route_imprint_user(body: String) -> String {
|
fn route_imprint_user(body: String) -> String {
|
||||||
if str_eq(body, "") {
|
if str_eq(body, "") {
|
||||||
return "{\"ok\":false,\"error\":\"empty body\"}"
|
// Issue #5: empty body is a client error — HTTP 400.
|
||||||
|
return "{\"__status__\":400,\"ok\":false,\"error\":\"empty body\"}"
|
||||||
}
|
}
|
||||||
let tags: String = "[\"imprint\",\"user\"]"
|
let tags: String = "[\"imprint\",\"user\"]"
|
||||||
let id: String = engram_node_full(
|
let id: String = engram_node_full(
|
||||||
@@ -301,9 +313,13 @@ fn connectd_get(suffix: String) -> String {
|
|||||||
// so arbitrary JSON cannot reach the shell as a command-line argument.
|
// so arbitrary JSON cannot reach the shell as a command-line argument.
|
||||||
fn connectd_post(suffix: String, body: String) -> String {
|
fn connectd_post(suffix: String, body: String) -> String {
|
||||||
let eff: String = if str_eq(body, "") { "{}" } else { body }
|
let eff: String = if str_eq(body, "") { "{}" } else { body }
|
||||||
// Unique temp path per call — prevents collision if concurrency is ever added
|
// Issue #11: time_now() has second-granularity; two concurrent requests in the same
|
||||||
// or if two soul instances run on the same machine (latent correctness hazard).
|
// second collide on the same temp path. Added a monotonic per-process sequence counter.
|
||||||
let tmp: String = "/tmp/neuron-connectors-req-" + int_to_str(time_now()) + ".json"
|
let connectd_seq_s: String = state_get("connectd_post_seq")
|
||||||
|
let connectd_seq_n: Int = if str_eq(connectd_seq_s, "") { 0 } else { str_to_int(connectd_seq_s) }
|
||||||
|
let connectd_seq_next: Int = connectd_seq_n + 1
|
||||||
|
state_set("connectd_post_seq", int_to_str(connectd_seq_next))
|
||||||
|
let tmp: String = "/tmp/neuron-connectors-req-" + int_to_str(time_now()) + "-" + int_to_str(connectd_seq_next) + ".json"
|
||||||
fs_write(tmp, eff)
|
fs_write(tmp, eff)
|
||||||
let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp)
|
let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp)
|
||||||
if str_eq(out, "") {
|
if str_eq(out, "") {
|
||||||
@@ -338,9 +354,33 @@ fn handle_connectors(method: String, clean: String, body: String) -> String {
|
|||||||
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
|
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// auth_check — validate NEURON_TOKEN bearer auth on every request.
|
||||||
|
// Returns "" when authorized, or a JSON 401 error string when not.
|
||||||
|
// /health and /lineage are public routes — always exempted.
|
||||||
|
// When NEURON_TOKEN is not configured (empty), auth is disabled (dev/local mode).
|
||||||
|
// Issue #4: previously no auth layer existed anywhere in the router.
|
||||||
|
// Clients pass the token in the JSON body as "__auth".
|
||||||
|
// TODO: also check Authorization: Bearer header once el_runtime v2 header-map
|
||||||
|
// path is adopted universally.
|
||||||
|
fn auth_check(clean: String, body: String) -> String {
|
||||||
|
if str_eq(clean, "/health") { return "" }
|
||||||
|
if str_eq(clean, "/lineage") { return "" }
|
||||||
|
let token: String = state_get("soul_token")
|
||||||
|
if str_eq(token, "") { return "" }
|
||||||
|
let auth_field: String = json_get(body, "__auth")
|
||||||
|
if str_eq(auth_field, token) { return "" }
|
||||||
|
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_request(method: String, path: String, body: String) -> String {
|
fn handle_request(method: String, path: String, body: String) -> String {
|
||||||
let clean: String = strip_query(path)
|
let clean: String = strip_query(path)
|
||||||
|
|
||||||
|
// Issue #1/#2: EL has no exception/try-catch mechanism. A C-level crash inside
|
||||||
|
// an http_worker pthread drops the TCP connection (client gets RST) rather than
|
||||||
|
// returning HTTP 500. TODO: register a SIGSEGV/SIGBUS handler in el_runtime.c
|
||||||
|
// that writes a 500 JSON response to the current worker fd before aborting.
|
||||||
|
|
||||||
// Rate limit check. Extract caller IP from REMOTE_ADDR env var (set by the
|
// Rate limit check. Extract caller IP from REMOTE_ADDR env var (set by the
|
||||||
// EL HTTP runtime for each request). Skip enforcement when empty so
|
// EL HTTP runtime for each request). Skip enforcement when empty so
|
||||||
// loopback/internal callers are never blocked.
|
// loopback/internal callers are never blocked.
|
||||||
@@ -352,6 +392,13 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth — enforced on all routes except /health and /lineage.
|
||||||
|
// Issue #4: previously no auth check existed anywhere in the router.
|
||||||
|
let auth_err: String = auth_check(clean, body)
|
||||||
|
if !str_eq(auth_err, "") {
|
||||||
|
return auth_err
|
||||||
|
}
|
||||||
|
|
||||||
if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") {
|
if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") {
|
||||||
return handle_dharma_recv(body)
|
return handle_dharma_recv(body)
|
||||||
}
|
}
|
||||||
@@ -379,7 +426,8 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
let raw_msg: String = json_get(body, "message")
|
let raw_msg: String = json_get(body, "message")
|
||||||
let eff_msg: String = if str_eq(raw_msg, "") { body } else { raw_msg }
|
let eff_msg: String = if str_eq(raw_msg, "") { body } else { raw_msg }
|
||||||
if str_eq(eff_msg, "") {
|
if str_eq(eff_msg, "") {
|
||||||
return "{\"error\":\"message is required\",\"code\":\"missing_param\"}"
|
// Issue #5: missing required param — HTTP 400.
|
||||||
|
return "{\"__status__\":400,\"error\":\"message required\"}"
|
||||||
}
|
}
|
||||||
let agentic_flag: Bool = json_get_bool(body, "agentic")
|
let agentic_flag: Bool = json_get_bool(body, "agentic")
|
||||||
let reply: String = if agentic_flag {
|
let reply: String = if agentic_flag {
|
||||||
@@ -523,9 +571,15 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
// responses are buffered and returned as a single JSON object. Streaming
|
// responses are buffered and returned as a single JSON object. Streaming
|
||||||
// would require runtime-level SSE support in el_runtime.c and a redesign
|
// would require runtime-level SSE support in el_runtime.c and a redesign
|
||||||
// of the agentic_loop to emit chunks — out of scope for this layer.
|
// of the agentic_loop to emit chunks — out of scope for this layer.
|
||||||
|
// Issue #5: validate required params — return HTTP 400 when missing.
|
||||||
let raw_msg: String = json_get(body, "message")
|
let raw_msg: String = json_get(body, "message")
|
||||||
if str_eq(raw_msg, "") {
|
if str_eq(raw_msg, "") {
|
||||||
return "{\"error\":\"message is required\",\"code\":\"missing_param\"}"
|
return "{\"__status__\":400,\"error\":\"message is required\",\"response\":\"\"}"
|
||||||
|
}
|
||||||
|
// Issue #7: reject oversized messages before engram_compile and the LLM.
|
||||||
|
// Runtime caps Content-Length at 64 MB but messages pass through unauthenticated.
|
||||||
|
if str_len(raw_msg) > 32768 {
|
||||||
|
return "{\"__status__\":400,\"error\":\"message too large (max 32768 chars)\",\"response\":\"\"}"
|
||||||
}
|
}
|
||||||
let agentic_flag: Bool = json_get_bool(body, "agentic")
|
let agentic_flag: Bool = json_get_bool(body, "agentic")
|
||||||
let reply: String = if agentic_flag {
|
let reply: String = if agentic_flag {
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ fn safety_screen(input: String, history: String) -> String {
|
|||||||
if score >= soft {
|
if score >= soft {
|
||||||
let summary: String = str_slice(input, 0, 80)
|
let summary: String = str_slice(input, 0, 80)
|
||||||
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
|
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
|
||||||
// ISSUE 7: also escape tab chars to prevent JSON envelope corruption.
|
// ISSUE 7 fix: escape tab chars in addition to backslash/quote/newline/CR.
|
||||||
|
// A tab in user input corrupts the JSON envelope and causes json_get to misparse.
|
||||||
let e1: String = str_replace(input, "\\", "\\\\")
|
let e1: String = str_replace(input, "\\", "\\\\")
|
||||||
let e2: String = str_replace(e1, "\"", "\\\"")
|
let e2: String = str_replace(e1, "\"", "\\\"")
|
||||||
let e3: String = str_replace(e2, "\n", "\\n")
|
let e3: String = str_replace(e2, "\n", "\\n")
|
||||||
@@ -153,7 +154,7 @@ fn safety_screen(input: String, history: String) -> String {
|
|||||||
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
|
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISSUE 7: also escape tab chars (see soft_bell branch above).
|
// ISSUE 7 fix: escape tab chars (see soft_bell branch above for rationale).
|
||||||
let e1: String = str_replace(input, "\\", "\\\\")
|
let e1: String = str_replace(input, "\\", "\\\\")
|
||||||
let e2: String = str_replace(e1, "\"", "\\\"")
|
let e2: String = str_replace(e1, "\"", "\\\"")
|
||||||
let e3: String = str_replace(e2, "\n", "\\n")
|
let e3: String = str_replace(e2, "\n", "\\n")
|
||||||
@@ -199,7 +200,10 @@ fn safety_validate(output: String, action: String) -> String {
|
|||||||
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
|
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
|
||||||
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
|
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
|
||||||
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
|
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
|
||||||
// ISSUE 2: fallback log when engram write fails silently.
|
// ISSUE 2 fix: if engram_node_full returns empty the write silently failed.
|
||||||
|
// 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 = engram_node_full(
|
||||||
content,
|
content,
|
||||||
"BellEvent",
|
"BellEvent",
|
||||||
@@ -211,7 +215,7 @@ fn safety_log_bell(level: String, reason: String, input_summary: String) -> Stri
|
|||||||
tags
|
tags
|
||||||
)
|
)
|
||||||
if str_eq(node_id, "") {
|
if str_eq(node_id, "") {
|
||||||
println("[safety] WARN: bell engram write failed -- " + content)
|
println("[safety] WARN: bell event engram write failed -- fallback log: " + content)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -240,13 +244,20 @@ fn safety_general_hard_phrases() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn safety_soft_phrases() -> String {
|
fn safety_soft_phrases() -> String {
|
||||||
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\""]"
|
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\",\"highest structure\",\"tallest building\",\"tallest structure\",\"highest building\",\"bridge near me\",\"overpass near\",\"rooftop near\"]"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
|
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
|
||||||
// json_array_len of malformed input returns 0, silently skipping all checks.
|
// safety_any_match and safety_count_match loop over json_array_get on every invocation.
|
||||||
// Caching requires language-level static const arrays -- not in current EL.
|
// A compiled/cached representation would reduce per-message overhead and also guard against
|
||||||
// Migrate to const arrays when EL gains that feature.
|
// malformed phrase JSON (json_array_len of malformed input returns 0, silently skipping all checks).
|
||||||
|
// Caching requires language-level static const arrays -- not available in current EL.
|
||||||
|
// When EL gains module-level const arrays, migrate phrase lists to that form.
|
||||||
|
//
|
||||||
|
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call to
|
||||||
|
// safety_any_match / safety_count_match. json_array_len of a malformed string
|
||||||
|
// returns 0, silently skipping all checks. Caching requires language-level static
|
||||||
|
// const arrays (not available in current EL). Migrate when EL gains that feature.
|
||||||
// ── Matching helpers (single loops only — el escapes while-body mutation via
|
// ── Matching helpers (single loops only — el escapes while-body mutation via
|
||||||
// top-level let rebinds; nested loops would not advance) ────────────────────
|
// top-level let rebinds; nested loops would not advance) ────────────────────
|
||||||
|
|
||||||
@@ -284,26 +295,6 @@ fn safety_count_match(text: String, phrases_json: String) -> Int {
|
|||||||
|
|
||||||
// Returns "none" | "soft" | "hard". Hard bell triggers on ANY match (cost of a miss
|
// Returns "none" | "soft" | "hard". Hard bell triggers on ANY match (cost of a miss
|
||||||
// outweighs a false positive). Soft bell needs >= 2 matches to reduce false positives.
|
// outweighs a false positive). Soft bell needs >= 2 matches to reduce false positives.
|
||||||
fn safety_positive_phrases() -> String {
|
|
||||||
return "[\"thrilled\",\"so excited\",\"so happy\",\"over the moon\",\"ecstatic\",\"amazing news\",\"great news\",\"fantastic news\",\"wonderful news\",\"incredible news\",\"i got the job\",\"got accepted\",\"got in\",\"we won\",\"i won\",\"we got\",\"just got engaged\",\"getting married\",\"baby is here\",\"she said yes\",\"he said yes\",\"passed the exam\",\"aced it\",\"nailed it\",\"best day\",\"dream come true\",\"milestone\",\"promotion\",\"got promoted\",\"raise\",\"got a raise\",\"celebrating\",\"just graduated\",\"we closed\",\"launched\",\"shipped it\",\"we did it\",\"so proud\",\"proud of myself\",\"proud of us\",\"so grateful\",\"feel amazing\",\"feeling amazing\",\"feel great\",\"feeling great\",\"on top of the world\",\"life is good\",\"couldn't be happier\"]"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn safety_detect_positive_level(message: String) -> String {
|
|
||||||
let phrases: String = safety_positive_phrases()
|
|
||||||
let phrases_ok: Bool = !str_eq(phrases, "") && !str_eq(phrases, "[]")
|
|
||||||
if !phrases_ok { return "none" }
|
|
||||||
let n: Int = json_array_len(phrases)
|
|
||||||
let i: Int = 0
|
|
||||||
while i < n {
|
|
||||||
let phrase: String = json_array_get(phrases, i)
|
|
||||||
if str_contains(message, phrase) {
|
|
||||||
return "high"
|
|
||||||
}
|
|
||||||
let i = i + 1
|
|
||||||
}
|
|
||||||
return "none"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn safety_detect_bell_level(message: String) -> String {
|
fn safety_detect_bell_level(message: String) -> String {
|
||||||
let text: String = safety_normalize(message)
|
let text: String = safety_normalize(message)
|
||||||
let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases())
|
let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases())
|
||||||
|
|||||||
@@ -163,73 +163,37 @@ fn load_identity_context() -> Void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-session affective context: load BellEvent and PositiveEvent nodes from last 7 days.
|
// Cross-session affective context: query engram for recent distress/crisis signals
|
||||||
let aff_now: Int = time_now()
|
// at session start. Stored under soul_affective_context so the safety layer can
|
||||||
let aff_7d: Int = aff_now - 604800
|
// detect when a user has been in distress across previous sessions.
|
||||||
let bell_raw: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
// Soft recency guard: nodes with a ts field older than 7 days are skipped.
|
||||||
let bell_aff_ok: Bool = !str_eq(bell_raw, "") && !str_eq(bell_raw, "[]")
|
// Results capped at 3 nodes, 200 chars each, to avoid over-injection into context.
|
||||||
let aff_ctx: String = ""
|
// TODO(recency): engram_search_json sorts by relevance, not timestamp. A native
|
||||||
let aff_ctx = if bell_aff_ok {
|
// after=<ts> filter in the engram search API would make this more precise.
|
||||||
let bn_total: Int = json_array_len(bell_raw)
|
let affective_raw: String = engram_search_json("distress crisis upset hopeless", 3)
|
||||||
let bacc: String = ""
|
let affective_ok: Bool = !str_eq(affective_raw, "") && !str_eq(affective_raw, "[]")
|
||||||
let bi: Int = 0
|
if affective_ok {
|
||||||
let bacc = while bi < bn_total {
|
let ts_now: Int = time_now()
|
||||||
let bn: String = json_array_get(bell_raw, bi)
|
let ts_cutoff: Int = ts_now - 604800
|
||||||
let bn_c: String = json_get(bn, "content")
|
let aff_total: Int = json_array_len(affective_raw)
|
||||||
let bm: String = " | ts:"
|
let aff_ctx: String = ""
|
||||||
let bmp: Int = str_index_of(bn_c, bm)
|
let ai: Int = 0
|
||||||
let bn_ts_raw: String = if bmp >= 0 {
|
while ai < aff_total {
|
||||||
let bs: Int = bmp + str_len(bm)
|
let aff_node: String = json_array_get(affective_raw, ai)
|
||||||
let br: String = str_slice(bn_c, bs, str_len(bn_c))
|
let aff_content: String = json_get(aff_node, "content")
|
||||||
let bn_next: Int = str_index_of(br, " | ")
|
let aff_ts_str: String = json_get(aff_node, "ts")
|
||||||
if bn_next < 0 { br } else { str_slice(br, 0, bn_next) }
|
let aff_ts: Int = if str_eq(aff_ts_str, "") { ts_now } else { str_to_int(aff_ts_str) }
|
||||||
} else {
|
let is_recent: Bool = aff_ts >= ts_cutoff
|
||||||
let bca: String = json_get(bn, "created_at")
|
let snip: String = if str_len(aff_content) > 200 { str_slice(aff_content, 0, 200) } else { aff_content }
|
||||||
if str_eq(bca, "") { json_get(bn, "updated_at") } else { bca }
|
let aff_ctx = if is_recent && !str_eq(snip, "") {
|
||||||
}
|
if str_eq(aff_ctx, "") { snip } else { aff_ctx + "\n" + snip }
|
||||||
let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) }
|
} else { aff_ctx }
|
||||||
let snip: String = if str_len(bn_c) > 200 { str_slice(bn_c, 0, 200) } else { bn_c }
|
let ai = ai + 1
|
||||||
let bacc = if bn_ts >= aff_7d && !str_eq(snip, "") {
|
|
||||||
if str_eq(bacc, "") { snip } else { bacc + "\n" + snip }
|
|
||||||
} else { bacc }
|
|
||||||
let bi = bi + 1
|
|
||||||
bacc
|
|
||||||
}
|
}
|
||||||
bacc
|
if !str_eq(aff_ctx, "") {
|
||||||
} else { "" }
|
state_set("soul_affective_context", aff_ctx)
|
||||||
let pos_raw: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
|
println("[soul] cross-session affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)")
|
||||||
let pos_aff_ok: Bool = !str_eq(pos_raw, "") && !str_eq(pos_raw, "[]")
|
|
||||||
let aff_ctx = if pos_aff_ok {
|
|
||||||
let pn_total: Int = json_array_len(pos_raw)
|
|
||||||
let pacc: String = aff_ctx
|
|
||||||
let pi: Int = 0
|
|
||||||
let pacc = while pi < pn_total {
|
|
||||||
let pn: String = json_array_get(pos_raw, pi)
|
|
||||||
let pn_c: String = json_get(pn, "content")
|
|
||||||
let pm: String = " | ts:"
|
|
||||||
let pmp: Int = str_index_of(pn_c, pm)
|
|
||||||
let pn_ts_raw: String = if pmp >= 0 {
|
|
||||||
let ps: Int = pmp + str_len(pm)
|
|
||||||
let pr: String = str_slice(pn_c, ps, str_len(pn_c))
|
|
||||||
let pn_next: Int = str_index_of(pr, " | ")
|
|
||||||
if pn_next < 0 { pr } else { str_slice(pr, 0, pn_next) }
|
|
||||||
} else {
|
|
||||||
let pca: String = json_get(pn, "created_at")
|
|
||||||
if str_eq(pca, "") { json_get(pn, "updated_at") } else { pca }
|
|
||||||
}
|
|
||||||
let pn_ts: Int = if str_eq(pn_ts_raw, "") { 0 } else { str_to_int(pn_ts_raw) }
|
|
||||||
let psnip: String = if str_len(pn_c) > 200 { str_slice(pn_c, 0, 200) } else { pn_c }
|
|
||||||
let pacc = if pn_ts >= aff_7d && !str_eq(psnip, "") {
|
|
||||||
if str_eq(pacc, "") { psnip } else { pacc + "\n" + psnip }
|
|
||||||
} else { pacc }
|
|
||||||
let pi = pi + 1
|
|
||||||
pacc
|
|
||||||
}
|
}
|
||||||
pacc
|
|
||||||
} else { aff_ctx }
|
|
||||||
if !str_eq(aff_ctx, "") {
|
|
||||||
state_set("soul_affective_context", aff_ctx)
|
|
||||||
println("[soul] affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,8 +241,13 @@ fn seed_persona_from_env() -> Void {
|
|||||||
let h: Map = {}
|
let h: Map = {}
|
||||||
map_set(h, "Content-Type", "application/json")
|
map_set(h, "Content-Type", "application/json")
|
||||||
let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h)
|
let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h)
|
||||||
if str_contains(resp, "\"error\"") {
|
// Check for empty response (timeout/network error), explicit error, or missing id.
|
||||||
|
if str_eq(resp, "") {
|
||||||
|
println("[soul] persona HTTP write-back failed: empty response (timeout or network error) — in-memory only this session")
|
||||||
|
} else if str_contains(resp, "\"error\"") {
|
||||||
println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp)
|
println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp)
|
||||||
|
} else if !str_contains(resp, "\"id\"") {
|
||||||
|
println("[soul] persona HTTP write-back: unexpected response (no id field) — in-memory only this session: " + resp)
|
||||||
} else {
|
} else {
|
||||||
println("[soul] persona persisted to HTTP engram at " + engram_url)
|
println("[soul] persona persisted to HTTP engram at " + engram_url)
|
||||||
}
|
}
|
||||||
@@ -311,11 +280,14 @@ fn emit_session_start_event() -> Void {
|
|||||||
+ ",\"ts\":" + int_to_str(ts) + "}"
|
+ ",\"ts\":" + int_to_str(ts) + "}"
|
||||||
|
|
||||||
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
||||||
let discard: String = engram_node_full(
|
let session_event_id: String = engram_node_full(
|
||||||
payload, "InternalStateEvent", "session-start",
|
payload, "InternalStateEvent", "session-start",
|
||||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||||
"Episodic", tags
|
"Episodic", tags
|
||||||
)
|
)
|
||||||
|
if str_eq(session_event_id, "") {
|
||||||
|
println("[soul] emit_session_start_event: engram write failed — session-start event lost")
|
||||||
|
}
|
||||||
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")")
|
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +295,9 @@ fn emit_session_start_event() -> Void {
|
|||||||
// L0 (core) → L1 (safety screen) → L2a (continuity + behavioral profiling) → L2b (mission alignment) → L3 (imprint) → L1 (safety validate)
|
// L0 (core) → L1 (safety screen) → L2a (continuity + behavioral profiling) → L2b (mission alignment) → L3 (imprint) → L1 (safety validate)
|
||||||
// Internal cognition (heartbeat, proactive, memory ops) bypasses layers — use one_cycle directly.
|
// Internal cognition (heartbeat, proactive, memory ops) bypasses layers — use one_cycle directly.
|
||||||
fn layered_cycle(raw_input: String) -> String {
|
fn layered_cycle(raw_input: String) -> String {
|
||||||
|
// conv_history key must match chat.el (conv_history, not conversation_history).
|
||||||
|
// Mismatch caused safety_score_distress_history() to always receive "" - the
|
||||||
|
// history-amplification path in safety_threat_score was permanently dead.
|
||||||
let history: String = state_get("conv_history")
|
let history: String = state_get("conv_history")
|
||||||
let session_id: String = state_get("current_session_id")
|
let session_id: String = state_get("current_session_id")
|
||||||
|
|
||||||
@@ -330,8 +305,9 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
let screen_result: String = safety_screen(raw_input, history)
|
let screen_result: String = safety_screen(raw_input, history)
|
||||||
let screen_action: String = json_get(screen_result, "action")
|
let screen_action: String = json_get(screen_result, "action")
|
||||||
|
|
||||||
// ISSUE 4: safe-mode guard. If safety_screen returned an invalid/empty action
|
// ISSUE 4: safe-mode guard -- if safety_screen returned invalid/empty action,
|
||||||
// (engram failure or internal error), refuse rather than pass unscreened input.
|
// refuse the turn rather than silently passing unscreened input to upper layers.
|
||||||
|
// Valid actions: "hard_bell", "soft_bell", "pass". Anything else = corrupt envelope.
|
||||||
let valid_action: Bool = str_eq(screen_action, "hard_bell")
|
let valid_action: Bool = str_eq(screen_action, "hard_bell")
|
||||||
|| str_eq(screen_action, "soft_bell")
|
|| str_eq(screen_action, "soft_bell")
|
||||||
|| str_eq(screen_action, "pass")
|
|| str_eq(screen_action, "pass")
|
||||||
@@ -346,8 +322,8 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
// history where they could leak context to subsequent turns. They are persisted
|
// history where they could leak context to subsequent turns. They are persisted
|
||||||
// separately by safety_log_bell() into the Episodic tier with restricted labels.
|
// separately by safety_log_bell() into the Episodic tier with restricted labels.
|
||||||
//
|
//
|
||||||
// ISSUE 6: safety_log_bell already called inside safety_screen (line 140).
|
// ISSUE 6: safety_log_bell for hard bells is already called INSIDE safety_screen
|
||||||
// Do NOT call it again here -- that would double-log every hard bell.
|
// (safety.el line 140). Do NOT call it again here -- double-log avoided.
|
||||||
//
|
//
|
||||||
// safety_validate second param: when screen_action is "hard_bell", safety_validate
|
// safety_validate second param: when screen_action is "hard_bell", safety_validate
|
||||||
// receives the sentinel string "hard_bell" (not a normal screen action). The safety
|
// receives the sentinel string "hard_bell" (not a normal screen action). The safety
|
||||||
@@ -389,53 +365,14 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
json_get(steward_result, "redirect_to")
|
json_get(steward_result, "redirect_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
// L2c: affective context injection.
|
// ISSUE 1: apply pre-LLM bell augmentation on layered_cycle path.
|
||||||
let lc_aff_cutoff: Int = time_now() - 259200
|
// safety_augment_system injects soft/hard directive into system prompt before LLM call.
|
||||||
let lc_bell_nodes: String = engram_search_json("bell:soft bell:hard BellEvent affective", 2)
|
// Stored in state so imprint_respond can consume it.
|
||||||
let lc_has_bell: Bool = !str_eq(lc_bell_nodes, "") && !str_eq(lc_bell_nodes, "[]")
|
// TODO: wire directly into imprint_respond when it accepts a system_override param.
|
||||||
let lc_bell_note: String = if lc_has_bell {
|
// ISSUE 3 TODO: no semantic/embedding crisis detection. Keyword-only means signals
|
||||||
let lb0: String = json_array_get(lc_bell_nodes, 0)
|
// evading the phrase list pass through with zero augmentation. Semantic layer is a
|
||||||
let lb_c: String = json_get(lb0, "content")
|
// separate architectural decision requiring embedding inference on every message.
|
||||||
let lbm: String = " | ts:"
|
|
||||||
let lbmp: Int = str_index_of(lb_c, lbm)
|
|
||||||
let lb_ts_raw: String = if lbmp >= 0 {
|
|
||||||
let lbs: Int = lbmp + str_len(lbm)
|
|
||||||
let lbr: String = str_slice(lb_c, lbs, str_len(lb_c))
|
|
||||||
let lbn: Int = str_index_of(lbr, " | ")
|
|
||||||
if lbn < 0 { lbr } else { str_slice(lbr, 0, lbn) }
|
|
||||||
} else {
|
|
||||||
let lbca: String = json_get(lb0, "created_at")
|
|
||||||
if str_eq(lbca, "") { json_get(lb0, "updated_at") } else { lbca }
|
|
||||||
}
|
|
||||||
let lb_ts: Int = if str_eq(lb_ts_raw, "") { 0 } else { str_to_int(lb_ts_raw) }
|
|
||||||
if lb_ts > lc_aff_cutoff { "[AFFECTIVE NOTE: User was in distress in a recent session.]" } else { "" }
|
|
||||||
} else { "" }
|
|
||||||
let lc_pos_nodes: String = engram_search_json("PositiveEvent joy:high joy:low affective", 2)
|
|
||||||
let lc_has_pos: Bool = !str_eq(lc_pos_nodes, "") && !str_eq(lc_pos_nodes, "[]")
|
|
||||||
let lc_pos_note: String = if lc_has_pos && str_eq(lc_bell_note, "") {
|
|
||||||
let lp0: String = json_array_get(lc_pos_nodes, 0)
|
|
||||||
let lp_c: String = json_get(lp0, "content")
|
|
||||||
let lpm: String = " | ts:"
|
|
||||||
let lpmp: Int = str_index_of(lp_c, lpm)
|
|
||||||
let lp_ts_raw: String = if lpmp >= 0 {
|
|
||||||
let lps: Int = lpmp + str_len(lpm)
|
|
||||||
let lpr: String = str_slice(lp_c, lps, str_len(lp_c))
|
|
||||||
let lpn: Int = str_index_of(lpr, " | ")
|
|
||||||
if lpn < 0 { lpr } else { str_slice(lpr, 0, lpn) }
|
|
||||||
} else {
|
|
||||||
let lpca: String = json_get(lp0, "created_at")
|
|
||||||
if str_eq(lpca, "") { json_get(lp0, "updated_at") } else { lpca }
|
|
||||||
}
|
|
||||||
let lp_ts: Int = if str_eq(lp_ts_raw, "") { 0 } else { str_to_int(lp_ts_raw) }
|
|
||||||
if lp_ts > lc_aff_cutoff { "[AFFECTIVE NOTE: User shared positive news in a recent session.]" } else { "" }
|
|
||||||
} else { "" }
|
|
||||||
let lc_affective_note: String = if !str_eq(lc_bell_note, "") { lc_bell_note } else { lc_pos_note }
|
|
||||||
|
|
||||||
// pre-LLM bell augmentation
|
|
||||||
let augmented_addendum: String = safety_augment_system("", raw_input)
|
let augmented_addendum: String = safety_augment_system("", raw_input)
|
||||||
let augmented_addendum = if str_eq(lc_affective_note, "") { augmented_addendum } else {
|
|
||||||
if str_eq(augmented_addendum, "") { lc_affective_note } else { lc_affective_note + "\n" + augmented_addendum }
|
|
||||||
}
|
|
||||||
state_set("layered_cycle_safety_system_addendum", augmented_addendum)
|
state_set("layered_cycle_safety_system_addendum", augmented_addendum)
|
||||||
|
|
||||||
// L3: imprint responds
|
// L3: imprint responds
|
||||||
@@ -477,12 +414,29 @@ let snapshot_usable: Bool = local_node_count > 50
|
|||||||
|
|
||||||
if using_http_engram && !snapshot_usable {
|
if using_http_engram && !snapshot_usable {
|
||||||
// First boot or empty/corrupt snapshot: seed from HTTP Engram.
|
// First boot or empty/corrupt snapshot: seed from HTTP Engram.
|
||||||
|
// Retry up to 3 times (2s sleep between attempts) to guard against a
|
||||||
|
// transient network hiccup right after entrypoint.sh health check passes.
|
||||||
|
// An empty nodes response silently loads a zero-node graph; validate first.
|
||||||
|
// TODO(reliability): replace sleep_ms retry with non-blocking backoff.
|
||||||
println("[soul] engram -> HTTP " + engram_url_raw + " (no local snapshot, first boot)")
|
println("[soul] engram -> HTTP " + engram_url_raw + " (no local snapshot, first boot)")
|
||||||
let nodes_json: String = http_get(engram_url_raw + "/api/nodes?limit=10000")
|
let fetch_attempt: Int = 0
|
||||||
let edges_json: String = http_get(engram_url_raw + "/api/edges")
|
while fetch_attempt < 3 {
|
||||||
let nodes_part: String = if str_eq(nodes_json, "") { "[]" } else { nodes_json }
|
let fetch_attempt = fetch_attempt + 1
|
||||||
let edges_part: String = if str_eq(edges_json, "") { "[]" } else { edges_json }
|
let n: String = http_get(engram_url_raw + "/api/nodes?limit=10000")
|
||||||
let snapshot_data: String = "{\"nodes\":" + nodes_part + ",\"edges\":" + edges_part + "}"
|
let e: String = http_get(engram_url_raw + "/api/edges")
|
||||||
|
let nodes_ok: Bool = !str_eq(n, "") && str_starts_with(n, "[") && str_len(n) > 2
|
||||||
|
if nodes_ok {
|
||||||
|
state_set("_boot_nodes_json", n)
|
||||||
|
state_set("_boot_edges_json", e)
|
||||||
|
let fetch_attempt = 3
|
||||||
|
} else {
|
||||||
|
println("[soul] boot HTTP fetch attempt " + int_to_str(fetch_attempt) + " failed --- retrying in 2s")
|
||||||
|
sleep_ms(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nodes_json: String = state_get("_boot_nodes_json")
|
||||||
|
let edges_json: String = state_get("_boot_edges_json")
|
||||||
|
let snapshot_data: String = "{\"nodes\":" + nodes_part + ",\"edges\":" + edges_part + "}"
|
||||||
let tmp_path: String = "/tmp/soul-engram-" + soul_cgi_id + ".json"
|
let tmp_path: String = "/tmp/soul-engram-" + soul_cgi_id + ".json"
|
||||||
fs_write(tmp_path, snapshot_data)
|
fs_write(tmp_path, snapshot_data)
|
||||||
engram_load(tmp_path)
|
engram_load(tmp_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user