README: infrastructure boundary and per-technology learning spine; commit architecture/runtime/schemas/style papers

This commit is contained in:
nyx committed 2026-11-06 14:12:49 +01:00
1 parent 529a411cb8
commit f6aad467e2
9 files changed
+647 -167

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
# Gate Layer
Upstream layer between sensors and the lemniscate. Gates admit signals based on thresholds. Each gate is registered; every decision validates against the register.
## Gate state
Each gate has one registry row and one state row.
- impulses: count since last summary.
- threshold: value shifted by the L1 decision.
- last_opened: cycle index of last opening.
- last_decision_cycle: cycle_uuid of the last decision that shifted this gate.
- counters: per-gate counters (e.g. dropped_pings).
- tier: T0-T3 declared importance.
## Injection loop
1. sensor emits impulse (event_uuid, gate, timestamp, payload).
2. gate summary at L1:
table columns: gate, impulses, threshold, last_opened, drives, last_decision_cycle, tier.
3. L1 decision: open/hold gates; threshold delta; optional evidence request.
4. opened gates compose content at L2 (frames, text, rows).
5. L2 answer + action calls; answer is next summary's salience signal.
## Cycle closure guard
Injection is only allowed while the lemniscate is CLOSED: both crossings complete and gate state snapshotted.
Properties:
1. Validation: the close is the validation point; a bad cycle is diagnosed from the summary table.
2. Staleness: summary and composed input use the same frozen snapshot.
3. Latency: crossing callbacks that block inference hold the cycle open; no desync.
Exceptions (re-run, not mid-cycle):
- threshold breach during an open cycle: summary rebuilt, cycle re-runs before inference lands.
- safety gate (T0): pierces the closed state; immediate cycle.
- starvation: a gate unseen for K cycles is force-opened at the next cycle boundary.
## Identity and causality
| ID | Assigned | Carries |
|---|---|---|
| event_uuid | sensor, at impulse | (the origin) |
| cycle_uuid | lemniscate, at open | admitted event_uuids |
| decision_uuid | L1 decision | event_uuids opened |
- L2 answer carries cycle_uuid + decision_uuid; output traces to its admitted impulses.
- UUID v7: 48-bit timestamp prefix; sort order = causal order.
- No record enters Phoebe without its causal UUIDs.
- Gate history keyed by cycle_uuid is replayable.
## Intake tiers
- Summary (poll): aggregate table each cycle.
- Evidence (pull): raw impulse rows for one gate, since last cycle, on request. The request is part of the L1 decision; rows are validated against the registry; they arrive as L2 composed input in the same cycle.
Impulse row: `(event_uuid, gate, timestamp, payload)`.
Created at the sensor; UUID v7; immutable. Failed ping is an emitted impulse, never inferred from absence.
## Policy layers
Higher layers cannot be overridden by lower ones; lower layers cannot close what a higher one opened.
| Layer | Owner | Decides |
|---|---|---|
| Hard floor | operator (dafit) | T0 gates; never closable |
| Presence policy | harness/switchboard, rule-bound | all closed / task-relevant open |
| Attention policy | runtime L1 | remainder |
Presence is a gate input above the L1 policy. It is rule-bound and has no LLM in the loop. Presence is the physical form of operator override.
## Gate tags (declared importance)
Importance is declared, never inferred.
| Tier | Meaning | Guarantee | Example |
|---|---|---|---|
| T0 critical | safety | pierces the closed cycle; hard floor; never closable | fire alarm, operator channel |
| T1 relational | operator presence | presence-owned; guaranteed attention share; cannot be attention-closed while active | chat/text gate, sofa camera |
| T2 standard | normal attention | tunable thresholds; starvation sampling | keepalive, host monitors |
| T3 ambient | background | poll-only; summary trend, on-demand evidence | telemetry |
Tag row: `(gate, tier, why, mode_overrides)`.
- Registry-declared and signed. T0/T1 immutable at runtime.
- T2/T3 may shift per mode (e.g. webcam T3 -> T1 in sofa mode); each shift is a UUID-logged decision.
- `why` is the audit reason string.
- Attention budget never starves a tier: T0 pierces, T1 holds a guaranteed share; L1 allocates the remainder.
## Adaptive timing
- L1 round-trip: decision capacity.
- L2 round-trip: synthesis capacity.
- Both derived from v7 timestamps; no new instrumentation.
- Cycle interval = mean(L1_RTT) + mean(L2_RTT) + margin.
- Animation duration >= L2_RTT; holds the gesture through synthesis.
- Estimator: sliding mean with hysteresis.
- RTT values feed schedule parameters, not gate thresholds.
## Long-running probes and heartbeat integrity
- Probes are async: issued at the crossing, not awaited. Result returns as an impulse into the next cycle's summary.
- RTT measures decision capacity only; probe wall-clock never enters the cycle interval.
- Probe latency is gate metadata; results accumulate losslessly into the next window.
- Silence must be manufactured: a probe's absence cannot be inferred. The deadline emits a timeout impulse (event_uuid, tiered); escalation rides a real signal.
- Pierce is for inspection, never effect-side arbitration: T0 forces the cycle to see; actions still resolve under mutex/priority.
## Persistence and restart
- Gate table in Phoebe, checkpointed after each cycle. The table is the resumable self; restart restores attention profile and thresholds.
- Raw impulses: bounded in-RAM ring buffer per gate; flushed to Phoebe at checkpoint. Evidence reads the buffer; audit reads the store.
- Truncation is explicit: an evidence request older than the buffer returns the buffer's lower bound; the gate table records the truncation.
## Attention semantics
Gates that close under sustained noise are the noise suppression: the gate stops mattering, rather than filtering smarter. Attention is a first-class resource with a budget and decay. The trait algebra at L2 tunes attention itself.
Version: 0.9 | Updated: 2026-11-04
View File
Whitespace-only changes.
+176
View File
@@ -0,0 +1,176 @@
# 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)
```
### 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):
```json
{
"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:
```json
{
"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.11 | Updated: 2026-11-04
+126
View File
@@ -0,0 +1,126 @@
# Register
Central registry: sensors, gates, actuators, clients, and the links between them. Runtime decisions validate against it. Register changes are governed impulses.
## Registry scope
| Question | Registry |
|---|---|
| What sensors exist and what may they emit? | sensor registry |
| What gates exist and what are they? | gate_registry (tier, why, mode_overrides) |
| What may be emitted out? | actuator registry (animation catalog, output channels) |
| What links exist? | gate links (`drives`) |
| Who is listening? | client registry |
The register is a gate layer one level up: sensors, gates and clients are admitted by declaration.
## `drives` links
Input gates link to output gates an answer may target:
```
room.cam T3 drives: body.anim (sofa: T1)
dafit.chat T1 drives: body.anim, clients.chat
fire.alarm T0 drives: body.alert
```
Rules:
1. Link visibility at L1: summary carries the link column; attention sees consequences before opening.
2. Choice-list at L2: evidence return carries content plus valid output gate catalog. Answer target must be in union of opened gates' `drives`.
3. Membership validation: valid = output gate in `drives`; invalid = decision rejected, cycle re-runs. Table lookup, not judgment.
4. No orphan decisions: input opened with no route, or answer with no causal input, is a representation error.
## Effect-side arbitration
Cycle is the transaction boundary. Two policies:
| Policy | Example | Enforcement |
|---|---|---|
| mutex | body.anim: exclusive | one effect per actuator per cycle; second call = override or queue, never both |
| priority | T0 safety > T1 reflex > T2 deliberate | higher tier supersedes; superseded reason logged |
Resolution:
1. cycle closes;
2. action set resolved (mutex/priority);
3. surviving actions validated against `drives`;
4. effects fire in one order, atomically, at the crossing;
5. cycle closes with a complete effect record.
Open policy decisions:
- per-cycle action cap;
- supersede vs queue default;
- whether T0 safety can interrupt in-flight animation.
## Debug workflows and skill store
`drives` may fan out:
```
host.keep T2 drives: host.prom, clients.status
```
- `host.keep -> host.prom` is a hypothesis: ping down -> metrics explain why.
- A probe may sit behind a workflow: a registered state machine or debug command with parameters, answering on its own channel. Registered, tiered, why-stated, UUID-traced, reviewable.
Skill store:
- register change events teach attention in real time; summary carries what each gate's `drives` can answer.
- gesture accumulator + decision UUIDs make probe effectiveness queryable.
- self-improvement is legible register change (tier shifts, link changes, new workflows), signed and reviewable.
Governance:
- diagnostics (read-only probes) run on the cycle's authority.
- remedial workflows carry the registered tier and sign.
## Pod spawning and escalation ladder
Spawn is an async probe with two transactions:
- Run 1 (spawn): decision calls `pod.spawn`, validated against `drives`. Action result carries spawn feedback gate link (`spawn_uuid`), registered at spawn time. Spawn is a register change: event_uuid + why.
- Run 2 (result): pod result requested against feedback link; answer lands in next summary as impulse.
- Silence must be manufactured: absence cannot be inferred. Deadline emits a timeout impulse (event_uuid, tiered) at budget edge; escalation flag rides a real signal.
Escalation:
- ladder bounded and signed; `escalate: T2->T1` is registered workflow step.
- importance declared, never inferred; attention never raises tiers itself.
- ladder terminates outside runtime: at T0 the last step is escalation to operator via `clients.chat`.
- silence escalates inspection, never arbitration.
- escalations logged with UUIDs and superseded reasons.
## Late results and orphan rule
When a tool answer arrives after its cycle closed:
- verification checks causal UUIDs; missing decision cycle = orphan = rejected.
- rejection is visible: it is an impulse into next summary.
- rejection is not silent execution: tool execution may have happened; result is discarded or re-registered by the next decision. Ingress drops results; decision stays with the runtime.
- late/orphan truth carries extra priority tier for inspection, not arbitration.
Invariant: lossless accumulation; rejection is an impulse; attention stays informed.
## Consistency mechanism
- register state in Phoebe (truth), materialized in runtime RAM (validator copy), pushed as change events on NATS.
- register changes are impulses: event_uuid at creation, decision_uuid, why string; replayable v7 causality.
- registry tier-locked T0/T1: signed by operator and immutable at runtime. Runtime observes and proposes; operator signs.
## Placement
- stateful = VM 123 worldserver: gate table in Phoebe, register materialized in RAM, pulse.
- broker = VM 122 nats-dev: isolated, cubes VLAN, client port 30000.
- ephemeral = k8s pods: daemons, python tools, probe workflows, spawned pods.
- no canonical state in a pod: gate state, register copy, impulse buffers live in Phoebe/123.
## Control chain
```
operator signature
-> register (law)
-> gate state (memory)
-> attention policy (L1/L2)
-> action call (actuator)
-> body/client
```
No component can rewrite the layer above it.
Version: 0.5 | Updated: 2026-11-04