// loop.el — Six-tier runtime heartbeat for the Neuron daemon. // // Implements the self-pacing cognitive loop described in the architecture // doc. The loop runs in its own OS thread (spawned via `spawn_thread`) // and communicates with the rest of the daemon through global shared // state (`state_get` / `state_set`). // // The six tiers: // resting — 30 min (low signal, diffuse) // watching — 10 min (ambient monitoring) // working — 15 sec (background task in progress) // active — 500 ms (conversation in progress) // critical — 10 ms (bell fired) // realtime — busy loop (physical actuator pinned) // // Tier transition rules: // 1. Bell is sacred — escalates to Critical immediately, never dropped. // 2. Escalation is immediate — loop re-enters without finishing sleep. // 3. Step-down is earned — 4 consecutive idle ticks before stepping down. // 4. Floor is configurable — never drops below `loop_min_tier`. // // State keys this file reads / writes: // loop_tier, loop_min_tier, loop_ticks, loop_bell_fires, // loop_tier_changes, loop_idle_ticks, loop_signal, loop_override_tier // // Cognitive substrate: // loop manages WHEN to tick; agent.el manages WHAT to do on each tick. // loop_do_tick() calls agent_tick() after confirming neuronrs is live. import "agent.el" import "plugins/host.el" // ── Tier constants and ordering ─────────────────────────────────────────────── fn loop_tier_rank(tier: String) -> Int { if str_eq(tier, "resting") { return 0 } if str_eq(tier, "watching") { return 1 } if str_eq(tier, "working") { return 2 } if str_eq(tier, "active") { return 3 } if str_eq(tier, "critical") { return 4 } if str_eq(tier, "realtime") { return 5 } return 1 } fn loop_tier_from_rank(rank: Int) -> String { if rank <= 0 { return "resting" } if rank == 1 { return "watching" } if rank == 2 { return "working" } if rank == 3 { return "active" } if rank == 4 { return "critical" } return "realtime" } // Sleep interval in milliseconds for each tier. // Returns 0 for realtime (busy loop, no sleep at all). fn loop_tier_interval(tier: String) -> Int { if str_eq(tier, "resting") { return 1800000 } if str_eq(tier, "watching") { return 600000 } if str_eq(tier, "working") { return 15000 } if str_eq(tier, "active") { return 500 } if str_eq(tier, "critical") { return 10 } if str_eq(tier, "realtime") { return 0 } return 600000 } // ── Signal handling ─────────────────────────────────────────────────────────── // Apply a signal to the current tier and return the new tier. // // Bell is special: it always escalates to at least Critical, regardless // of `min_tier`. Other escalations clamp to max(needed, min_tier) so a // configured floor cannot trap us above an explicit step-down. fn loop_apply_signal(current: String, signal: String, min_tier: String) -> String { let cur_rank: Int = loop_tier_rank(current) let min_rank: Int = loop_tier_rank(min_tier) // Bell — sacred. Always go to at least Critical. if str_eq(signal, "bell") { let crit_rank: Int = 4 if cur_rank >= crit_rank { return current } return "critical" } // Realtime escalation pins us at the top. if str_eq(signal, "realtime") { return "realtime" } // Release-realtime drops us back to Critical. if str_eq(signal, "release-realtime") { if str_eq(current, "realtime") { return "critical" } return current } // Active escalates to at least Active. if str_eq(signal, "active") { let need: Int = 3 if cur_rank >= need { return current } return "active" } // Task escalates to at least Working. if str_eq(signal, "task") { let need: Int = 2 if cur_rank >= need { return current } return "working" } // Sleep is an explicit step-down request — drop one tier // (respecting the floor). if str_eq(signal, "sleep") { return loop_step_down(current, min_tier) } // Idle / drain just contribute to the idle counter; the tier // itself does not change here. return current } // Step down one tier, but never below the configured min_tier. fn loop_step_down(current: String, min_tier: String) -> String { let cur_rank: Int = loop_tier_rank(current) let min_rank: Int = loop_tier_rank(min_tier) if cur_rank <= min_rank { return min_tier } let new_rank: Int = cur_rank - 1 if new_rank < min_rank { return min_tier } return loop_tier_from_rank(new_rank) } // ── Bell-aware sleep ────────────────────────────────────────────────────────── // Tail-recursive sleep that wakes early if a bell signal arrives. // Sleeps in 100ms chunks, peeking at `loop_signal` between each chunk. fn loop_sleep_chunked(remaining_ms: Int) -> Void { if remaining_ms <= 0 { // done } else { let pending: String = state_get("loop_signal") if str_eq(pending, "bell") { // Bell detected mid-sleep — return immediately so the // outer loop can re-enter and escalate. } else { let chunk: Int = if remaining_ms < 100 { remaining_ms } else { 100 } sleep_ms(chunk) loop_sleep_chunked(remaining_ms - chunk) } } } // Public sleep entry point. realtime (interval == 0) is a no-op. fn loop_sleep(remaining_ms: Int) -> Void { if remaining_ms > 0 { loop_sleep_chunked(remaining_ms) } } // ── Tick ────────────────────────────────────────────────────────────────────── // Perform one cognitive tick. // // First pings neuronrs /health to confirm the substrate is live. // If live, delegates to agent_tick() — the cognitive work is in agent.el. // loop manages WHEN; agent manages WHAT. // // Side effect: writes "1" or "0" to `loop_last_tick_idle` so the // caller can see whether the tick was idle (no live counterpart). fn loop_do_tick(tier: String) -> Void { let url: String = "http://localhost:7770/health" let resp: String = http_get(url) let live: Bool = str_contains(resp, "ok") if live { state_set("loop_last_tick_idle", "0") // Delegate to the agent cognitive substrate. agent_tick({}) } else { state_set("loop_last_tick_idle", "1") } // Drain the plugin event bus on every tick, regardless of substrate liveness. // Plugins interact through events only — host_tick routes events to all // plugins that subscribed to them. No hooks, no direct callbacks. host_tick({}) println("[loop] tick tier=" + tier + " live=" + bool_to_str(live)) } // ── Counters ────────────────────────────────────────────────────────────────── fn loop_incr_state_int(key: String) -> Void { let raw: String = state_get(key) let n: Int = if str_eq(raw, "") { 0 } else { str_to_int(raw) } state_set(key, int_to_str(n + 1)) } fn loop_set_int(key: String, value: Int) -> Void { state_set(key, int_to_str(value)) } // ── Core loop ───────────────────────────────────────────────────────────────── // One iteration of the heartbeat. Tail-recursive: each tick re-enters // itself with the (possibly updated) tier and idle counter. fn loop_run(tier: String, idle_count: Int) -> Void { // 1. Read floor and pending signals. let min_tier: String = state_get("loop_min_tier") let floor: String = if str_eq(min_tier, "") { "resting" } else { min_tier } let pending: String = state_get("loop_signal") let override_tier: String = state_get("loop_override_tier") // 2. Apply override (explicit set tier — wins over signal). let after_override: String = if str_eq(override_tier, "") { tier } else { override_tier } if !str_eq(override_tier, "") { state_set("loop_override_tier", "") loop_incr_state_int("loop_tier_changes") } // 3. Apply signal. let after_signal: String = if str_eq(pending, "") { after_override } else { loop_apply_signal(after_override, pending, floor) } let signal_changed: Bool = !str_eq(pending, "") if signal_changed { state_set("loop_signal", "") if str_eq(pending, "bell") { loop_incr_state_int("loop_bell_fires") } if !str_eq(after_signal, after_override) { loop_incr_state_int("loop_tier_changes") } } // 4. Persist current tier. state_set("loop_tier", after_signal) // 5. Sleep for this tier's interval (bell-aware, chunked). let interval_ms: Int = loop_tier_interval(after_signal) loop_sleep(interval_ms) // 6. If a bell arrived during sleep, re-enter immediately without // ticking — the next iteration will re-read the signal and // escalate. let after_sleep_signal: String = state_get("loop_signal") if str_eq(after_sleep_signal, "bell") { loop_run(after_signal, idle_count) } else { // 7. Run the cognitive tick. loop_do_tick(after_signal) loop_incr_state_int("loop_ticks") // 8. Update idle counter and step down if earned. let last_idle: String = state_get("loop_last_tick_idle") let was_idle: Bool = str_eq(last_idle, "1") let new_idle_count: Int = if was_idle { idle_count + 1 } else { 0 } // Idle / drain signals also push the counter forward, but // `loop_apply_signal` does not change the tier for them. let idle_signal: Bool = str_eq(pending, "idle") || str_eq(pending, "drain") let bumped_idle_count: Int = if idle_signal { new_idle_count + 1 } else { new_idle_count } // Realtime is pinned — only `release-realtime` can lower it. let pinned: Bool = str_eq(after_signal, "realtime") if !pinned && bumped_idle_count >= 4 { let stepped: String = loop_step_down(after_signal, floor) if !str_eq(stepped, after_signal) { loop_incr_state_int("loop_tier_changes") } loop_set_int("loop_idle_ticks", 0) loop_run(stepped, 0) } else { loop_set_int("loop_idle_ticks", bumped_idle_count) loop_run(after_signal, bumped_idle_count) } } } // Entry point — called by `spawn_thread("loop_main")`. fn loop_main() -> Void { let start_tier: String = state_get("loop_tier") let initial: String = if str_eq(start_tier, "") { "watching" } else { start_tier } println("[loop] starting at tier=" + initial) loop_run(initial, 0) } // ── Status JSON ─────────────────────────────────────────────────────────────── fn loop_status_field(key: String, default: String) -> String { let v: String = state_get(key) if str_eq(v, "") { return default } return v } fn loop_status_json() -> String { let tier: String = loop_status_field("loop_tier", "watching") let min_tier: String = loop_status_field("loop_min_tier", "resting") let ticks: String = loop_status_field("loop_ticks", "0") let bell_fires: String = loop_status_field("loop_bell_fires", "0") let tier_changes: String = loop_status_field("loop_tier_changes", "0") let idle_ticks: String = loop_status_field("loop_idle_ticks", "0") let signal: String = loop_status_field("loop_signal", "") let parts: String = "{\"current_tier\":\"" + tier + "\"" let p2: String = parts + ",\"min_tier\":\"" + min_tier + "\"" let p3: String = p2 + ",\"ticks\":" + ticks let p4: String = p3 + ",\"bell_fires\":" + bell_fires let p5: String = p4 + ",\"tier_changes\":" + tier_changes let p6: String = p5 + ",\"idle_ticks\":" + idle_ticks let p7: String = p6 + ",\"pending_signal\":\"" + signal + "\"}" return p7 }