Files
nimmerworld.eachpath.local/architecture/lemniscate.md
T

8.6 KiB

Runtime Engine: Lemniscate

Scope: the runtime loop for the agent (Nyx). It is a server, not a UI and not a borrowed coding harness.

Runtime decision

  • Interactive harnesses (pi-agent-core, omp) are hosts: they wait for a human, end on session close, and do not run unattended.
  • Nimmerworld implements its own loop (this engine) and runs indefinitely.
  • Harnesses host nimmer-lib only. Clients never implement the agent brain; the runtime never borrows one.

Topology

Figure-eight with two lobes and two crossings:

  Gate Summary In              Action Result
          |                          |
          v        Second Crossing   v
      +--------------+  +--------------+
      |  LEFT LOBE   |  |  RIGHT LOBE  |
      |  (intake)    |  |  (effect)    |
      +--------------+  +--------------+
          ^        First Crossing     ^
          |                          |
     Gate Feedback            Action Call (tool/actuator)

NATS vocabulary (label map)

Lemniscate label NATS role Transport Delivery guarantee
sensor pub <sensor.subject> publisher core NATS fire-and-forget: only subscribers open at publish time get a copy; late subscribers get nothing
gate.summary, gate.decision subject (fan-in at L1, fan-out to gate layer) core NATS ephemeral, regenerated every pulse; nothing survives the cycle
L1 (LLM) subscriber on gate.summary, publisher on gate.decision core NATS client a swappable bus client, any language
action.call.<target> stream (JetStream) durable at-least-once delivery, replayable
actuator:<t> own JetStream consumer on its subject durable tracks its own position; a dead actuator stalls no one
action.result stream (JetStream) durable causality: event_uuid -> cycle_uuid -> decision_uuid
client:<name> own durable consumer on zone.state.> / action.result.> durable replay-on-wake: reconnect and read what it missed

Rules in NATS words: fan-out is the subject matcher, not code (wildcard * = one token, > = one or more). Fan-in means many publishers onto one subject; each consumer position is independent, consumers never share state or corrupt each other. Idempotency is enforced by us, via dedupe on decision_uuid/cycle_uuid, because JetStream guarantees at-least-once, never exactly-once.

Subject naming (open): the engine above uses short subjects (gate.summary, action.call.<target>). Our live bus streams use nimmerverse.register.> / nimmerverse.cycle.>. Before any engine traffic flows, pick ONE naming convention and map it here.

Bus transport (NATS / JetStream)

The engine is host-agnostic: it never speaks to a model or an actuator directly, everything crosses the bus as structured JSON.

 sensor/camera  -> pub <sensor.subject>      (raw events; gates subscribe via catalog)
 gate layer     -> pub gate.summary         (composer renders summary table, once per cycle)
 L1 (LLM)       -> sub gate.summary / pub gate.decision   (core NATS, ephemeral, cycle-scoped)
 L2 composer    -> sub gate.decision
                    validate targets vs drives (table lookup, once, upstream of fan-out)
                    then pub action.call.<target> per validated action
 actuator:<t>   -> own JetStream consumer on action.call.<t>.>
 client:<name>  -> own durable consumer on zone.state.> / action.result.> (replay-on-wake)

Placement rules:

  • gate.summary / gate.decision stay on core NATS: ephemeral, regenerated every pulse; never block the pulse loop on storage I/O.

  • Consequence subjects (action.call.>, action.result, causal records) are JetStream streams: at-least-once, survive restart, replayable via event_uuid -> cycle_uuid -> decision_uuid.

  • Fan-out is the subject matcher, not code: the L2 composer publishes to action.call.<target> from the catalog; each actuator consumes its own subject and tracks its own position; a dead actuator stalls no one.

  • Validation happens once, in the composer, before the fan-out; consumers execute or nack, they never re-decide.

  • Idempotency rides the envelope: every action carries decision_uuid + cycle_uuid; actuators dedupe on them so redelivery cannot double-fire.

  • The LLM endpoint is a swappable bus client. vLLM today, harness tomorrow, zero engine changes.

  • Left lobe: intake. Gate summaries, evidence pulls, thresholds.

  • Right lobe: effect. Synthesized answer, action calls, client output.

  • L1 (first crossing): admission decision. Open/hold gates; shift thresholds; request evidence.

  • L2 (second crossing): synthesis. Produce answer and action-call list from the admitted content.

  • The crossings are the only synchronous points. All state resolves atomically there.

Loop semantics

  • One traversal = one cycle. The lemniscate closes exactly once: L1 then L2.
  • Between crossings, traversal is local; only the crossing touches shared state.
  • The cycle is a transaction: no new input lands while it is open.
  • Feedback: the L2 answer is the salience signal for the next gate summary. It is not a gate.

Data flow

L1 inputs:

  • gate summary table: gate, impulses, threshold, last_opened, drives, last_decision_cycle, tier.
  • request parameters (evidence pull, k frames, probe query).

L1 outputs:

  • gate decision: decision_uuid, opened gate list, parameters, threshold deltas.

L2 inputs:

  • composed content from the opened gates (evidence rows, frames, text).
  • catalog of valid output gates from each opened gate's drives.

L2 outputs:

  • answer text.
  • action-call list: action_uuid, target actuator/tool, parameters, tier.
  • gesture accumulator update.
  • salience signal for next summary.

Validation:

  • output gate must be in the union of opened input gates' drives.
  • invalid decision: cycle re-runs. Membership is a table lookup.

Gate summary envelope (what the model actually reads at L1):

{
  "cycle_uuid": "c-042",
  "gates": [
    {"gate": "k8s_alert", "impulses": 4, "threshold": 10,
     "last_opened": 12, "drives": ["k8s_action"], "tier": "T1",
     "last_decision_cycle": "c-041"}
  ],
  "request_params": {"k_frames": 3, "probe": null}
}

L1 replies exactly:

{
  "decision_uuid": "d-117",
  "opened": ["k8s_alert"],
  "params": {"k_frames": 5},
  "threshold_deltas": {"k8s_alert": -2}
}

Pulse clock

elapsed += dt
while elapsed >= interval:
    crossing()
    elapsed -= interval
  • Interval = mean(L1_RTT) + mean(L2_RTT) + safety_margin.
  • RTT is derived from v7 timestamps (impulse to decision), not from wall-clock probes.
  • Sliding-mean estimator with hysteresis. A single slow cycle must not stretch the interval.
  • Probe latencies are metadata on their gates; they never enter the cycle interval.
  • dt >= 1/pulse_rate would fire every tick once pulse_rate > 62; the accumulator prevents that.

Map

Lemniscate Nimmerworld
Director runtime
Zone / EmergentZone bounded event episode
Crossing L1 gate summary read; gate open/hold
Crossing L2 composed input synthesized; answer + action call
Slot tokens participants
Gesture accumulator nimmer-lib trait algebra
update_all_zones NATS event loop
Gate / Sensor threshold channel upstream
UUID causality event_uuid -> cycle_uuid -> decision_uuid
Intake tiers summary poll + evidence pull
Policy layers hard floor (operator) > presence policy (harness) > attention policy (runtime)
Gate tags T0-T3 declared importance
Adaptive timing round-trips regulate interval and animation duration
Register declared sensors/gates/actuators/clients and drives links

Engine corrections

  1. _should_crossing must accumulate time:
    • elapsed += dt; while elapsed >= interval: crossing.
    • dt >= 1/pulse_rate fires every tick once pulse_rate > 62 and makes the pulse rate meaningless.
  2. Loops A/B must carry distinct semantics:
    • the crossing callback must know which loop it is on.
    • symmetric loops degrade the figure-eight into round-robin.
    • resolved by the L1/L2 split (intake vs effect).
  3. Director.update_all_zones must call zone.execute_turn(dt), not zone.run_crossing():
    • otherwise the state machine never ticks.
    • terminal reports must be collected, not dropped.

Interfaces (v1)

  • Core NATS (ephemeral): gate.summary, gate.decision.
  • JetStream (durable, at-least-once): action.call.<target>, action.result, causal records.
  • Register/catalog: sensors, gates, actuators, clients, drives. Catalog entry declares the class path; runtime imports an explicit registry and fails loud on a missing class.
  • Clients hold own durable consumers on zone.state.> and action.result.> (replay-on-wake).

Version: 0.12 | Updated: 2026-11-06