engram: declare configuration once instead of at every read site
Migrates engram to the `program` block. 18 configuration variables that each
carried their default inline at the point of use now declare it in one place,
and engram declares itself a singleton.
The read sites lose their defaults entirely: `let v = env("X")` followed by
`if str_eq(v,"") { "default" } else { v }` collapses to `config("X")`. The
guide_env_or(key, dflt) helper is deleted -- its whole job was supplying a
per-site default, which is the thing being removed.
Fixes ENGRAM_DATA_DIR, which was the clearest instance of the defect. It was
read at six sites. Five were dead: `let dir_raw = env("ENGRAM_DATA_DIR")`
immediately shadowed on the next line by `engram_resolve_data_dir()`. The sixth
was live and defaulted to /tmp/engram, contradicting the canonical resolver's
$HOME/.neuron/engram -- and its consumer is the pre-destructive reseed backup,
so with ENGRAM_DATA_DIR unset the safety copy was written to ephemeral storage
while the store it protected lived elsewhere. All six now go through
engram_resolve_data_dir().
ENGRAM_DATA_DIR is deliberately NOT declared in the program block, and the
source says why: engram_resolve_data_dir() already owns it, and a second
declaration would give it two owners that can disagree -- recreating the exact
defect being removed here. A variable belongs in the block when the block would
be its only owner. HOME stays a raw env() read; it is an environment fact, not
configuration.
singleton: "engram" matters more than it looks. Today a second engram whose
bind() fails merely returns from http_serve -- after it has already replayed
the WAL and written boot-time backup files -- and then exits 0, indistinguishable
from a clean run. That is how two instances came to share one data dir. Verified
that the second instance now refuses before any side effect: with instance 1
holding the lock (lsof pid, shell pid, and lock file contents all agreeing at
5946), the second start named that pid, exited 1, and left the data directory
untouched.
Verified by bijection on the generated C: 18 config() reads, 18 declarations,
no read without a declaration and no declaration without a read. Three bad Int
values are reported in a single run rather than costing one restart each.
ENGRAM_API_KEY keeps its permissive empty default, which disables auth -- that
is pre-existing behaviour and changing it is out of scope. The source marks
making it `required` as the obvious hardening follow-up.
This commit is contained in:
+83
-36
@@ -10,10 +10,60 @@
|
||||
// cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c
|
||||
// ./engram
|
||||
//
|
||||
// Configuration via environment:
|
||||
// ENGRAM_BIND — host:port (default :8742)
|
||||
// ENGRAM_API_KEY — bearer auth (optional)
|
||||
// ENGRAM_DATA_DIR — snapshot location (default ~/.neuron/engram)
|
||||
// Configuration is DECLARED, not scattered. See the `program` block below:
|
||||
// every knob's type and default lives there and nowhere else, is resolved from
|
||||
// the environment (env wins, declaration is the fallback) and validated before
|
||||
// any statement of this file runs. Read one with config("NAME") -> String.
|
||||
//
|
||||
// The one deliberate exception is ENGRAM_DATA_DIR — see the note in the block.
|
||||
|
||||
// ── Program declaration (cross-cutting concerns) ──────────────────────────────
|
||||
//
|
||||
// singleton: two engram processes against one data dir is data loss, not a
|
||||
// warning. The runtime takes an exclusive flock at startup and a second start
|
||||
// is refused loudly with the holder's pid.
|
||||
//
|
||||
// NOT declared here, on purpose: ENGRAM_DATA_DIR. Its resolution is owned by
|
||||
// engram_resolve_data_dir() (el_runtime.c), which defaults to $HOME/.neuron/engram
|
||||
// and fails LOUD rather than silently persisting to an ephemeral directory.
|
||||
// Declaring a default for it here as well would put the data dir's fallback in
|
||||
// two places — which is precisely the defect this migration removes (until
|
||||
// 2026-08-15 the reseed backup path carried its own "/tmp/engram" default that
|
||||
// disagreed with the resolver, so the pre-destructive safety copy landed in /tmp).
|
||||
// HOME is likewise not declared: it is a genuine environment read, not a knob.
|
||||
program "engram" {
|
||||
singleton: "engram"
|
||||
|
||||
// ── Core server ──
|
||||
env ENGRAM_BIND: String = ":8742"
|
||||
// Default "" leaves auth DISABLED (check_auth_ok short-circuits to true on an
|
||||
// empty key). That is the pre-existing behaviour and is deliberately preserved
|
||||
// here; making this `required` is the obvious hardening follow-up, but it is a
|
||||
// behaviour change and out of scope for this migration.
|
||||
env ENGRAM_API_KEY: String = ""
|
||||
|
||||
// ── Feature flags (bool-ish Strings; the predicate fns below own truthiness) ──
|
||||
env ENGRAM_STORE: String = "off"
|
||||
env ENGRAM_WAL: String = "off"
|
||||
env ENGRAM_AUTOCONNECT: String = "off"
|
||||
env ENGRAM_ISE_OFFGRAPH: String = "off"
|
||||
|
||||
// ── ISE telemetry ──
|
||||
env ENGRAM_ISE_RETENTION_MS: Int = "172800000"
|
||||
|
||||
// ── Guide (local Qwen3 via llama-server) ──
|
||||
env GUIDE_ENABLE: String = "off"
|
||||
env GUIDE_TIER_FORCE: String = ""
|
||||
env GUIDE_CACHE_DIR: String = ""
|
||||
env GUIDE_RAM_GB_4B: Int = "16"
|
||||
env GUIDE_RAM_GB_1P7B: Int = "8"
|
||||
env GUIDE_BACKEND: String = "llama-server"
|
||||
env GUIDE_HOST: String = "127.0.0.1"
|
||||
env GUIDE_PORT: Int = "8771"
|
||||
env GUIDE_LLAMA_SERVER_BIN: String = "llama-server"
|
||||
env GUIDE_NGL: Int = "99"
|
||||
env GUIDE_CTX: Int = "4096"
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -133,7 +183,7 @@ fn route_text_health(method: String, path: String, body: String) -> String {
|
||||
// engram_store_enabled() in el_runtime.c EXACTLY (1 / on / true). Default off →
|
||||
// every persistence path below is byte-for-byte the historical snapshot behavior.
|
||||
fn store_on() -> Bool {
|
||||
let v: String = env("ENGRAM_STORE")
|
||||
let v: String = config("ENGRAM_STORE")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -162,7 +212,6 @@ fn persist_canonical() -> Int {
|
||||
if store_on() {
|
||||
return engram_store_checkpoint()
|
||||
}
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
// (2026-08-10 self-review) This returned a hardcoded 1, which made every
|
||||
// caller's `let saved: Int = persist_canonical()` a dead variable — six
|
||||
@@ -176,7 +225,7 @@ fn persist_canonical() -> Int {
|
||||
// per-write full-snapshot behavior. When ON, structural mutations append O(1)
|
||||
// WAL records instead of rewriting the whole graph, with threshold compaction.
|
||||
fn wal_on() -> Bool {
|
||||
str_eq(env("ENGRAM_WAL"), "on")
|
||||
str_eq(config("ENGRAM_WAL"), "on")
|
||||
}
|
||||
|
||||
// autoconnect_on — ENGRAM_AUTOCONNECT. Will's rule: "we shouldn't be inserting
|
||||
@@ -184,7 +233,7 @@ fn wal_on() -> Bool {
|
||||
// edge (kNN over embeddings) so no content node enters the graph edgeless.
|
||||
// Default OFF -> byte-identical to prior behavior (node created, no auto edges).
|
||||
fn autoconnect_on() -> Bool {
|
||||
let v: String = env("ENGRAM_AUTOCONNECT")
|
||||
let v: String = config("ENGRAM_AUTOCONNECT")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -197,7 +246,7 @@ fn autoconnect_on() -> Bool {
|
||||
// separate state-event log tier instead of the node graph. Default OFF -> ISEs
|
||||
// remain graph nodes exactly as before (with 48h prune).
|
||||
fn ise_offgraph_on() -> Bool {
|
||||
let v: String = env("ENGRAM_ISE_OFFGRAPH")
|
||||
let v: String = config("ENGRAM_ISE_OFFGRAPH")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -358,7 +407,6 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
||||
// process ever booted with a partial/empty store, the first read request
|
||||
// clobbered the good snapshot. Read routes must never write the canonical path.)
|
||||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let snap_path: String = dir + "/.scan-export.json"
|
||||
engram_save(snap_path)
|
||||
@@ -519,7 +567,6 @@ fn route_forget(method: String, path: String, body: String) -> String {
|
||||
|
||||
fn route_save(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
// (2026-08-10 self-review) engram_save returns 0 on an empty path and the
|
||||
@@ -603,7 +650,6 @@ fn route_drift(method: String, path: String, body: String) -> String {
|
||||
|
||||
fn route_load(method: String, path: String, body: String) -> String {
|
||||
let p_raw: String = json_get_string(body, "path")
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||
// (2026-08-10 self-review) This was a stub response over the single most
|
||||
@@ -674,7 +720,6 @@ fn route_embed_backfill(method: String, path: String, body: String) -> String {
|
||||
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
|
||||
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
|
||||
fn route_sync(method: String, path: String, body: String) -> String {
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
// 2026-07-21 self-review: export to a scratch path, never the canonical
|
||||
// snapshot.json — read routes must not be able to clobber the good snapshot.
|
||||
@@ -750,8 +795,12 @@ fn route_reseed_nodes(method: String, path: String, body: String) -> String {
|
||||
if str_eq(p, "") { return err_json("path is required") }
|
||||
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
|
||||
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
// (2026-08-15) This site carried its own "/tmp/engram" fallback, which
|
||||
// DISAGREED with engram_resolve_data_dir() ($HOME/.neuron/engram, fail-loud).
|
||||
// The consumer is the pre-destructive backup below, so with ENGRAM_DATA_DIR
|
||||
// unset the safety copy taken before a reseed landed in an ephemeral /tmp
|
||||
// while the store it was protecting lived elsewhere. One owner, one answer.
|
||||
let dir: String = engram_resolve_data_dir()
|
||||
let backup: String = dir + "/.reseed-backup.json"
|
||||
|
||||
let replace_raw: String = json_get_raw(body, "replace")
|
||||
@@ -843,8 +892,7 @@ fn route_emit_ise(method: String, path: String, body: String) -> String {
|
||||
sal, imp, conf,
|
||||
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
||||
)
|
||||
let ret_raw: String = env("ENGRAM_ISE_RETENTION_MS")
|
||||
let ret_ms: Int = if str_eq(ret_raw, "") { 172800000 } else { str_to_int(ret_raw) }
|
||||
let ret_ms: Int = str_to_int(config("ENGRAM_ISE_RETENTION_MS"))
|
||||
let pruned: Int = engram_prune_telemetry(ret_ms)
|
||||
"{\"ok\":true,\"id\":\"" + id + "\",\"pruned\":" + int_to_str(pruned) + "}"
|
||||
}
|
||||
@@ -1093,14 +1141,12 @@ fn route_correspondence_beat(method: String, path: String, body: String) -> Stri
|
||||
// turns native thinking ON: the response carries reasoning_content (the thinking)
|
||||
// alongside content (the answer).
|
||||
|
||||
fn guide_env_or(key: String, dflt: String) -> String {
|
||||
let v: String = env(key)
|
||||
if str_eq(v, "") { return dflt }
|
||||
return v
|
||||
}
|
||||
// (2026-08-15) guide_env_or(key, dflt) lived here. Its whole job was supplying a
|
||||
// per-call-site default, which is now the program block's job — every GUIDE_* knob
|
||||
// is declared once at the top of this file and read straight through config().
|
||||
|
||||
fn guide_enabled() -> Bool {
|
||||
let v: String = env("GUIDE_ENABLE")
|
||||
let v: String = config("GUIDE_ENABLE")
|
||||
if str_eq(v, "1") { return true }
|
||||
if str_eq(v, "on") { return true }
|
||||
if str_eq(v, "true") { return true }
|
||||
@@ -1145,15 +1191,15 @@ fn guide_probe_metal() -> Bool {
|
||||
|
||||
// ── 2. Tier selection (config-driven thresholds, spec-autoselected) ────────────
|
||||
fn guide_threshold_4b() -> Int {
|
||||
return str_to_int(guide_env_or("GUIDE_RAM_GB_4B", "16"))
|
||||
return str_to_int(config("GUIDE_RAM_GB_4B"))
|
||||
}
|
||||
fn guide_threshold_1p7b() -> Int {
|
||||
return str_to_int(guide_env_or("GUIDE_RAM_GB_1P7B", "8"))
|
||||
return str_to_int(config("GUIDE_RAM_GB_1P7B"))
|
||||
}
|
||||
|
||||
// GUIDE_TIER_FORCE overrides the spec autoselect (used to prove cheaply on 0.6b).
|
||||
fn guide_select_tier(ram_gb: Int) -> String {
|
||||
let forced: String = env("GUIDE_TIER_FORCE")
|
||||
let forced: String = config("GUIDE_TIER_FORCE")
|
||||
if !str_eq(forced, "") { return forced }
|
||||
if ram_gb >= guide_threshold_4b() { return "4b" }
|
||||
if ram_gb >= guide_threshold_1p7b() { return "1.7b" }
|
||||
@@ -1173,8 +1219,10 @@ fn guide_file(tier: String) -> String {
|
||||
}
|
||||
|
||||
fn guide_cache_dir() -> String {
|
||||
let c: String = env("GUIDE_CACHE_DIR")
|
||||
let c: String = config("GUIDE_CACHE_DIR")
|
||||
if !str_eq(c, "") { return c }
|
||||
// HOME stays a raw env() read: it is the ambient environment, not a knob of
|
||||
// this program, and it is deliberately absent from the program block.
|
||||
let home: String = env("HOME")
|
||||
if !str_eq(home, "") { return home + "/.neuron/guide/models" }
|
||||
return engram_resolve_data_dir() + "/guide-models"
|
||||
@@ -1215,9 +1263,9 @@ fn guide_fetch(tier: String) -> Bool {
|
||||
}
|
||||
|
||||
// ── 4/5. Backend abstraction + BIND as an engageable interlocutor ──────────────
|
||||
fn guide_backend() -> String { return guide_env_or("GUIDE_BACKEND", "llama-server") }
|
||||
fn guide_host() -> String { return guide_env_or("GUIDE_HOST", "127.0.0.1") }
|
||||
fn guide_port() -> String { return guide_env_or("GUIDE_PORT", "8771") }
|
||||
fn guide_backend() -> String { return config("GUIDE_BACKEND") }
|
||||
fn guide_host() -> String { return config("GUIDE_HOST") }
|
||||
fn guide_port() -> String { return config("GUIDE_PORT") }
|
||||
fn guide_base_url() -> String { return "http://" + guide_host() + ":" + guide_port() }
|
||||
|
||||
// guide_healthy — is the guide present and answering? llama-server's /health
|
||||
@@ -1235,9 +1283,9 @@ fn guide_healthy() -> Bool {
|
||||
fn guide_load(tier: String) -> Bool {
|
||||
if guide_healthy() { return true }
|
||||
let path: String = guide_model_path(tier)
|
||||
let bin: String = guide_env_or("GUIDE_LLAMA_SERVER_BIN", "llama-server")
|
||||
let ngl: String = guide_env_or("GUIDE_NGL", "99")
|
||||
let ctx: String = guide_env_or("GUIDE_CTX", "4096")
|
||||
let bin: String = config("GUIDE_LLAMA_SERVER_BIN")
|
||||
let ngl: String = config("GUIDE_NGL")
|
||||
let ctx: String = config("GUIDE_CTX")
|
||||
let logf: String = guide_cache_dir() + "/llama-server." + guide_port() + ".log"
|
||||
let cmd: String = bin + " -m '" + path + "' --host " + guide_host() + " --port " + guide_port() + " -c " + ctx + " -ngl " + ngl + " --jinja >> '" + logf + "' 2>&1"
|
||||
let pid: String = exec_bg(cmd)
|
||||
@@ -1633,7 +1681,7 @@ fn route_supersede(method: String, path: String, body: String) -> String {
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn check_auth_ok(method: String, body: String) -> Bool {
|
||||
let key: String = env("ENGRAM_API_KEY")
|
||||
let key: String = config("ENGRAM_API_KEY")
|
||||
if str_eq(key, "") { return true }
|
||||
// Read-only methods don't require auth. Until http_serve surfaces
|
||||
// request headers we can't accept a Bearer token cleanly; mutating
|
||||
@@ -1896,8 +1944,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
|
||||
// ── Entry ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
let bind_raw: String = env("ENGRAM_BIND")
|
||||
let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw }
|
||||
let bind_str: String = config("ENGRAM_BIND")
|
||||
let port: Int = parse_port(bind_str)
|
||||
|
||||
// On startup, try to load any existing snapshot (best effort).
|
||||
|
||||
Reference in New Issue
Block a user