From 4a6f2ec654ed3a6f1dd19c22706ea68156a6f404 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 23 Jul 2026 11:47:10 -0700 Subject: [PATCH 01/14] Prototype deterministic intelligent terrarium core --- experiments/intelligent-terrarium/KEEPER.md | 239 ++++++++++++++++++ .../intelligent-terrarium/package.json | 9 + .../intelligent-terrarium/src/canonical.mjs | 40 +++ .../intelligent-terrarium/src/cgroup.mjs | 36 +++ experiments/intelligent-terrarium/src/cli.mjs | 76 ++++++ .../intelligent-terrarium/src/core.mjs | 225 +++++++++++++++++ .../intelligent-terrarium/src/prng.mjs | 46 ++++ .../intelligent-terrarium/src/repository.mjs | 146 +++++++++++ .../intelligent-terrarium/src/sleep.mjs | 108 ++++++++ .../intelligent-terrarium/test/core.test.mjs | 49 ++++ .../test/repository.test.mjs | 65 +++++ 11 files changed, 1039 insertions(+) create mode 100644 experiments/intelligent-terrarium/KEEPER.md create mode 100644 experiments/intelligent-terrarium/package.json create mode 100644 experiments/intelligent-terrarium/src/canonical.mjs create mode 100644 experiments/intelligent-terrarium/src/cgroup.mjs create mode 100644 experiments/intelligent-terrarium/src/cli.mjs create mode 100644 experiments/intelligent-terrarium/src/core.mjs create mode 100644 experiments/intelligent-terrarium/src/prng.mjs create mode 100644 experiments/intelligent-terrarium/src/repository.mjs create mode 100644 experiments/intelligent-terrarium/src/sleep.mjs create mode 100644 experiments/intelligent-terrarium/test/core.test.mjs create mode 100644 experiments/intelligent-terrarium/test/repository.test.mjs diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md new file mode 100644 index 0000000000..807b508ba5 --- /dev/null +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -0,0 +1,239 @@ +# Intelligent Terrarium — keeper recap + +Updated: 2026-07-23 (America/Los_Angeles) + +This is the living design and decision log for a persistent, generative 3D +intelligence hosted experimentally on `jastow` (`jas-nzxt`). Nothing here is a +production deployment. The working branch is `agent/intelligent-terrarium` in +the sparse worktree `/Users/jas/aesthetic-computer/.worktrees/intelligent-terrarium`. + +## Outcome + +Build the first slice as one bounded, authoritative `terrariumd` process plus +one browser visitor. The authority owns simulation, identity-bound actions, +semantic sound events, and persistence. The browser owns rendering and sound +synthesis, never world truth. Durable growth is replayable text data in a +separate Git repository; model weights, caches, tokens, and derived binaries +never enter autobiographical history. + +```mermaid +flowchart TD + V[AC visitor
WebGL + gamepad] -->|token + input| A[terrariumd authority] + A -->|snapshots + sonic events| V + A --> M[bounded active mind] + M --> J[seed + event journal] + J --> S[sleep checkpoint] + S --> G[local Git commit] +``` + +## Discovery + +### Aesthetic Computer seams to reuse + +- `session-server/world-manager.mjs` is the current generic authoritative 3D + pattern: fixed simulation, per-client snapshots, WS lifecycle, UDP fast path, + and browser interpolation. Terrarium should reuse its separation of authority + and transport, but can begin at 10 Hz simulation / 5 Hz snapshots rather than + arena's movement-oriented 60 / 30 Hz. +- `disks/arena.mjs`, `lib/3d.mjs`, and the Three.js-backed `Form` API already + supply WebGL scenes, cameras, geometry, VR/controller plumbing, and remote + interpolation. The eventual AC piece should stay hidden from `list` until the + access boundary is reviewed. +- AC pieces can request the logged-in access token with `api.authorize()`. + `session-server`'s newer `fight:auth` flow validates that token at Auth0 and + resolves the handle server-side. This is the required model. The current + `arena:hello {handle}` flow trusts a client string and is not sufficient for + terrarium admission or authorship. +- AC's synth supports stereo `pan` and per-event volume. No browser 3D panner is + currently shared with pieces. For the first slice, convert authoritative 3D + source coordinates to listener-relative equal-power pan and distance rolloff; + keep the wire event semantic so HRTF/WebAudio panning can replace the renderer + later without changing the mind or journal. +- `docs/AGENT-MEMORY-LOCAL-FIRST.md` demonstrates encrypted append-only event + records and checkpoint lineage. Terrarium uses the append/checkpoint ideas, + but its world autobiography is deliberately human-reviewable Git data rather + than opaque agent transcripts. + +### `jastow` read-only inventory + +Observed 2026-07-23 around 11:36 PDT: + +- Fedora 43, Linux 7.1.4, 12 CPU threads, 15 GiB RAM, 8 GiB swap. +- 1.9 GiB RAM used, 13 GiB available, zero swap use, and zero current CPU, + memory, or I/O PSI pressure. Root/home had about 912 GiB free. +- RTX 3070, NVIDIA driver 580.173.02. At the sample it was 100% utilized, + 3.33 GiB VRAM used, and 4.51 GiB free. `matador-miner` owned about 3.30 GiB + VRAM and about two CPU cores. Do not stop or reconfigure it without a keeper + decision; initial inference must assume the GPU is unavailable. +- Present: `nvidia-smi`, Python 3.14.6, Node 22.21.1, Podman 5.8.4, Docker + 29.6.2. +- Absent from PATH: CUDA `nvcc`, Ollama, llama.cpp CLIs/server, vLLM, Conda, + and Micromamba. The system Python has no NumPy, PyTorch, Transformers, + llama-cpp-python, ONNX Runtime, sentence-transformers, or FAISS. No local + container images, running containers, or common model caches were found. +- `/home/me/aesthetic-computer` is on `main` with unrelated untracked + `docs/native-fedora.md` and `scripts/fedora-native-setup.sh`; leave both alone. + +Neo itself had less than 1 GiB free, so a full second monorepo checkout failed. +The keeper worktree is sparse (root files plus `experiments/`) and consumes only +about 6 MiB. No unrelated dirty files were changed. + +## Architecture invariants + +1. **One writer:** `terrariumd` is the only writer of world state and journal + sequence numbers. Clients submit intentions, never mutations. +2. **Identity is verified:** admission is `token -> Auth0 userinfo -> account + sub -> current AC handle`. Client-provided handles are display hints only. + Tokens, email, IP addresses, and Auth0 payloads are never journaled. +3. **Deterministic core:** a versioned seed and PRNG cursor plus ordered events + reproduce a checkpoint hash. Wall-clock time is converted at the boundary + into explicit events; core simulation never reads it implicitly. +4. **Bounded mind:** every queue, visitor set, spatial index, recall set, prompt, + and context window has a fixed maximum. The whole process group runs under a + cgroup hard limit, not a hopeful dashboard threshold. +5. **Semantic sound:** the authority emits compact events such as + `{id, tick, kind, source:[x,y,z], voice, pitch, intensity, radius, duration, + cause}`. Clients deduplicate by `id` and render locally. No live audio stream + is part of world truth. +6. **Git is sleep, not the hot database:** append journal segments during wake; + on sleep, fsync and close the segment, write an atomic checkpoint and + autobiographical summary, verify replay, then commit only if content changed. + No automatic push. +7. **Crash safety:** the last valid commit plus any fully written, checksummed + journal records is sufficient for recovery. A partially written tail is + quarantined, never guessed through. + +### Durable state repository on `jastow` + +Proposed location, to create only after a keeper decision: +`/home/me/intelligent-terrarium-state`. Keep it separate from +`/home/me/aesthetic-computer` so autonomous sleep commits cannot trigger AC +hooks, mingle with application history, or stage unrelated work. + +```text +seed.json identity, schema, PRNG seed +journal/2026-07-23/000001.ndjson ordered, checksummed facts and actions +checkpoints/latest.json compact replay acceleration +autobiography/2026-07-23.md bounded human-readable sleep reflection +visitors/handles.json consent/access facts, no tokens or PII +manifest.json schema versions and state hash +``` + +Journal segments are canonical; checkpoints and prose are derived and verified +before commit. Commit messages use sequence bounds and state hash, for example +`sleep: events 000001-000184 state 7e2c9a1b`. The Git author is the terrarium +service identity, not a visiting handle. + +## Resident-memory profiles + +The limits apply to the complete `terrariumd` process group, including any +inference child. GPU VRAM is reported separately and is never treated as a way +to evade the RAM cap. + +| Budget | Hard controls | Active mind | Expected steady RSS | +|---|---|---|---| +| **1 GB** | `MemoryHigh=900M`, `MemoryMax=1024M`; Node old-space 256 MiB; bounded 4 MiB journal buffer; stop cleanly on allocation pressure | Deterministic recurrent drives, salience/novelty scoring, bounded episodic recall, spatial relation graph, and generative behavior policy. No resident language model. | 300–600 MiB; acceptance requires `<900 MiB` peak over the demo | +| **2 GB** | `MemoryHigh=1800M`, `MemoryMax=2048M`; same core bounds; inference has explicit model/context caps and one request at a time | 1 GB mind plus an optional approved user-space llama.cpp runner using a roughly 0.5–1B Q4 model, at most 2k context, quantized/bounded KV cache, and no unbounded embedding index | 1.2–1.8 GiB; acceptance requires `<1.8 GiB` peak | + +The 1 GB profile is the product floor and remains intelligent without an LLM: +it perceives events, maintains drives and relationships, recalls salient +episodes, chooses behaviors, composes semantic sounds, and learns bounded +weights that become autobiography. The 2 GB model is a slow reflection/wording +organ, never the simulation authority. Given current GPU saturation, it should +run CPU-only at low priority first. Acquiring/building llama.cpp or model weights +is an explicit later decision because no suitable runtime is installed. + +Measure `memory.current`, `memory.peak`, `/proc//smaps_rollup`, event-loop +lag, journal backlog, and inference queue depth. Reject startup when a requested +profile cannot establish its hard cgroup limit. + +## Staged prototype + +### Stage 0 — deterministic headless kernel + +Work only under this sparse `experiments/intelligent-terrarium/` tree. Implement +a dependency-light Node kernel with seeded PRNG, bounded state, event schema, +NDJSON journal, replay hash, and scripted visitors. Tests prove identical seed + +events produces the same state and sonic events. + +### Stage 1 — sleep/wake Git cycle + +Use a temporary test repository first. Exercise clean sleep, no-op sleep, +restart/replay, invalid tail, and process death between checkpoint creation and +commit. Git operations use explicit allowlisted paths; never `git add -A`. + +### Stage 2 — loopback browser visitor + +Serve snapshots and sonic events on `127.0.0.1` only, by an ephemeral manual +process. Render a small Three.js terrarium, listener-relative stereo audio, +keyboard, and standard Gamepad API input. Use an SSH local forward for neo-side +testing; no LAN/public listener or persistent service. + +### Stage 3 — real AC identity and hidden piece + +Add a development-only AC piece and a token-first handshake modeled on +`fight:auth`. Resolve the current handle server-side, bind actions to the +verified account, rate-limit intentions, and reject replayed/expired sessions. +This stage touches served paths and therefore requires a keeper decision before +any commit, push, or deploy. + +### Stage 4 — 2 GB reflection experiment + +After an explicit tooling/model decision, benchmark one small quantized model +CPU-only under the 2 GB cgroup. Feed it a bounded selected-memory digest, accept +only schema-validated proposals, and journal both proposal and deterministic +authority decision. Fall back to the 1 GB policy on timeout, malformed output, +or pressure. + +### Stage 5 — visitable service / Xbox gate + +Only after threat review and explicit deployment approval: choose the durable +gateway, TLS origin, service supervision, backup/restore, handle access policy, +and miner/GPU coexistence. Validate Edge/Xbox controller navigation and WebAudio +unlock behavior. No inbound service or production session-server change happens +before this gate. + +## First demo acceptance criteria + +The first thin slice is accepted only when all of these pass: + +1. A fresh seed produces a visible evolving terrarium for 15 minutes under the + **1 GB** hard profile with `memory.peak < 900 MiB`, no swap growth, no + unbounded queue growth, and no effect on the existing miner process. +2. Two local browser visitors see the same authoritative entity positions and + event sequence. Keyboard and an Xbox-compatible controller can move a + visitor camera; neither client can set world state directly. +3. A server event at the listener's left/right and near/far positions is heard + on the correct side with monotonic distance attenuation, and the same event + ID never sounds twice after a reconnect. +4. The authenticated development path accepts a real logged-in AC handle, + rejects a missing/invalid token, and proves that sending another handle in + the payload cannot spoof authorship. Logs and Git contain no token or PII. +5. After at least one visitor interaction and one autonomous behavior, `sleep` + creates exactly one local state-repository commit containing an allowlisted + journal segment, checkpoint, manifest hash, and short autobiography. It does + not push. +6. Killing and restarting from that commit yields the identical state hash, + next event sequence, and next seeded autonomous behavior. A deliberately + truncated journal tail is detected and quarantined with the last valid state + preserved. +7. The authority binds only to loopback for the demo. `ss -lntp` confirms no new + LAN/public listener, and no production or `jastow` AC checkout file changes. + +## Keeper decisions still required + +- Permission to create `/home/me/intelligent-terrarium-state` and run an + ephemeral loopback-only process on `jastow`. +- Whether/when to install or build a user-space inference runtime and acquire a + specific quantized model. No system package installation is assumed. +- The future authenticated gateway and who may visit/interact versus observe. +- Any coordination with `matador-miner`; the default is strict coexistence and + no GPU use. +- Any served-path commit, push, deployment, persistent service, firewall, DNS, + or public exposure. + +## Next keeper action + +Implement Stage 0 locally in this sparse worktree with deterministic replay and +memory-accounting tests only. It needs no remote writes, model installation, +network listener, production path, or deployment. diff --git a/experiments/intelligent-terrarium/package.json b/experiments/intelligent-terrarium/package.json new file mode 100644 index 0000000000..28c72ea040 --- /dev/null +++ b/experiments/intelligent-terrarium/package.json @@ -0,0 +1,9 @@ +{ + "name": "intelligent-terrarium-experiment", + "private": true, + "type": "module", + "scripts": { + "test": "node --test test/*.test.mjs", + "demo": "node src/cli.mjs demo" + } +} diff --git a/experiments/intelligent-terrarium/src/canonical.mjs b/experiments/intelligent-terrarium/src/canonical.mjs new file mode 100644 index 0000000000..29bc0dbe96 --- /dev/null +++ b/experiments/intelligent-terrarium/src/canonical.mjs @@ -0,0 +1,40 @@ +import { createHash } from "node:crypto"; +import { mkdir, rename, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; + +function normalize(value) { + if (value === null || typeof value === "string" || typeof value === "boolean") { + return value; + } + if (typeof value === "number") { + if (!Number.isFinite(value)) throw new TypeError("canonical JSON rejects non-finite numbers"); + return Object.is(value, -0) ? 0 : value; + } + if (Array.isArray(value)) return value.map(normalize); + if (typeof value === "object") { + return Object.fromEntries( + Object.keys(value).sort().map((key) => [key, normalize(value[key])]), + ); + } + throw new TypeError(`canonical JSON rejects ${typeof value}`); +} + +export function canonical(value) { + return JSON.stringify(normalize(value)); +} + +export function hash(value) { + const bytes = typeof value === "string" ? value : canonical(value); + return createHash("sha256").update(bytes).digest("hex"); +} + +export function clone(value) { + return JSON.parse(canonical(value)); +} + +export async function atomicWrite(path, contents) { + await mkdir(dirname(path), { recursive: true }); + const temp = `${path}.tmp-${process.pid}`; + await writeFile(temp, contents, { mode: 0o600 }); + await rename(temp, path); +} diff --git a/experiments/intelligent-terrarium/src/cgroup.mjs b/experiments/intelligent-terrarium/src/cgroup.mjs new file mode 100644 index 0000000000..a51d4fa3fd --- /dev/null +++ b/experiments/intelligent-terrarium/src/cgroup.mjs @@ -0,0 +1,36 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +async function readValue(path) { + try { + const value = (await readFile(path, "utf8")).trim(); + return value === "max" ? "max" : Number(value); + } catch { + return null; + } +} + +export async function memoryCgroup() { + if (process.platform !== "linux") return null; + const cgroup = await readFile("/proc/self/cgroup", "utf8"); + const unified = cgroup.split("\n").find((line) => line.startsWith("0::")); + if (!unified) return null; + const relative = unified.slice(3); + const root = join("/sys/fs/cgroup", relative); + return { + path: relative, + current: await readValue(join(root, "memory.current")), + peak: await readValue(join(root, "memory.peak")), + high: await readValue(join(root, "memory.high")), + max: await readValue(join(root, "memory.max")), + swapMax: await readValue(join(root, "memory.swap.max")), + }; +} + +export async function requireMemoryMax(limit) { + const memory = await memoryCgroup(); + if (!memory || memory.max === "max" || !Number.isFinite(memory.max) || memory.max > limit) { + throw new Error(`required cgroup memory.max <= ${limit}; observed ${memory?.max ?? "unavailable"}`); + } + return memory; +} diff --git a/experiments/intelligent-terrarium/src/cli.mjs b/experiments/intelligent-terrarium/src/cli.mjs new file mode 100644 index 0000000000..9cfe438d96 --- /dev/null +++ b/experiments/intelligent-terrarium/src/cli.mjs @@ -0,0 +1,76 @@ +#!/usr/bin/env node +import { mkdir, stat } from "node:fs/promises"; +import { resolve } from "node:path"; +import { memoryCgroup, requireMemoryMax } from "./cgroup.mjs"; +import { outputHash, Terrarium } from "./core.mjs"; +import { StateRepository, verifyRepository } from "./repository.mjs"; +import { sleepCommit } from "./sleep.mjs"; + +function option(name, fallback) { + const index = process.argv.indexOf(name); + return index === -1 ? fallback : process.argv[index + 1]; +} + +async function demo() { + const root = resolve(option("--root", "./terrarium-state")); + const ticks = Number(option("--ticks", "600")); + const requiredMax = Number(option("--require-memory-max", "0")); + if (requiredMax) await requireMemoryMax(requiredMax); + await mkdir(root, { recursive: true }); + let repository; + try { + await stat(resolve(root, "seed.json")); + repository = await StateRepository.open(root, { segmentId: "demo" }); + } catch { + repository = await StateRepository.create(root, { seed: "intelligent-terrarium-demo-v1", profile: "1gb" }); + repository.segmentPath = resolve(root, "journal", "segments", "demo.ndjson"); + } + + if (!repository.terrarium.state.visitors["@alex"]) { + await repository.transact("visitor-enter", { handle: "@alex", position: { x: 1, y: 1.7, z: -2 } }); + await repository.transact("visitor-signal", { handle: "@alex", signal: "hello terrarium" }); + } + const outputs = []; + let remaining = ticks; + while (remaining > 0) { + const amount = Math.min(10, remaining); + const result = await repository.transact("advance", { ticks: amount }); + outputs.push(...result.outputs); + remaining -= amount; + } + const sleep = await sleepCommit(repository); + const verification = await verifyRepository(root); + const candidateA = Terrarium.fromSnapshot(repository.terrarium.snapshot()); + const candidateB = Terrarium.fromSnapshot(repository.terrarium.snapshot()); + const nextA = candidateA.apply({ seq: candidateA.state.lastSeq + 1, kind: "advance", payload: { ticks: 53 } }); + const nextB = candidateB.apply({ seq: candidateB.state.lastSeq + 1, kind: "advance", payload: { ticks: 53 } }); + if (outputHash(nextA) !== outputHash(nextB) || candidateA.stateHash() !== candidateB.stateHash()) { + throw new Error("next seeded behavior is not deterministic"); + } + const memory = await memoryCgroup(); + console.log(JSON.stringify({ + ok: verification.stateHash === repository.stateHash(), + root, + stateHash: verification.stateHash, + lastSeq: verification.lastSeq, + headRecordHash: verification.headRecordHash, + commit: sleep.commit, + sleepStatus: sleep.status, + sonicEvents: outputs.length, + sonicOutputHash: outputHash(outputs), + nextBehaviorHash: outputHash(nextA), + rss: process.memoryUsage().rss, + memory, + listener: "none", + }, null, 2)); +} + +const command = process.argv[2]; +if (command === "demo") { + await demo(); +} else if (command === "verify") { + console.log(JSON.stringify(await verifyRepository(resolve(option("--root", "./terrarium-state"))), null, 2)); +} else { + console.error("usage: cli.mjs demo|verify [--root PATH] [--ticks N] [--require-memory-max BYTES]"); + process.exitCode = 2; +} diff --git a/experiments/intelligent-terrarium/src/core.mjs b/experiments/intelligent-terrarium/src/core.mjs new file mode 100644 index 0000000000..7eb3a55404 --- /dev/null +++ b/experiments/intelligent-terrarium/src/core.mjs @@ -0,0 +1,225 @@ +import { canonical, clone, hash } from "./canonical.mjs"; +import { Prng } from "./prng.mjs"; + +export const LIMITS = Object.freeze({ + entities: 12, + visitors: 16, + episodes: 256, + signalLength: 160, + advanceTicks: 600, +}); + +const WORLD_RADIUS = 12; +const round = (value) => Math.round(value * 1e6) / 1e6; +const clamp = (value, low, high) => Math.max(low, Math.min(high, value)); + +export function normalizeHandle(value) { + const handle = String(value || "").trim().toLowerCase(); + const normalized = handle.startsWith("@") ? handle : `@${handle}`; + if (!/^@[a-z0-9][a-z0-9._-]{0,62}$/.test(normalized)) { + throw new TypeError("invalid verified handle"); + } + return normalized; +} + +function position(value = {}) { + return { + x: round(clamp(Number(value.x) || 0, -WORLD_RADIUS, WORLD_RADIUS)), + y: round(clamp(Number(value.y) || 0, 0, 8)), + z: round(clamp(Number(value.z) || 0, -WORLD_RADIUS, WORLD_RADIUS)), + }; +} + +function pushEpisode(state, episode) { + state.mind.episodes.push(episode); + if (state.mind.episodes.length > LIMITS.episodes) { + state.mind.episodes.splice(0, state.mind.episodes.length - LIMITS.episodes); + } +} + +function genesis(seed) { + const rng = new Prng(seed); + const entities = []; + for (let index = 0; index < LIMITS.entities; index += 1) { + entities.push({ + id: `spore-${String(index + 1).padStart(2, "0")}`, + species: ["moss", "bell", "mote"][index % 3], + x: round(rng.signed() * 8), + y: round(0.4 + rng.float() * 3), + z: round(rng.signed() * 8), + vx: round(rng.signed() * 0.04), + vy: round(rng.signed() * 0.01), + vz: round(rng.signed() * 0.04), + energy: round(0.4 + rng.float() * 0.5), + }); + } + return { + schema: 1, + seed: String(seed), + tick: 0, + lastSeq: 0, + sonicCount: 0, + rng: rng.toJSON(), + entities, + visitors: {}, + mind: { + drives: { curiosity: 0.5, rest: 0.2, social: 0.1 }, + weights: { approach: 0.5, sing: 0.5, wander: 0.5 }, + episodes: [], + }, + }; +} + +export class Terrarium { + constructor(seed, snapshot) { + this.state = snapshot ? clone(snapshot) : genesis(seed); + } + + static fromSnapshot(snapshot) { + return new Terrarium(snapshot.seed, snapshot); + } + + snapshot() { + return clone(this.state); + } + + stateHash() { + return hash(this.state); + } + + apply(record) { + const expected = this.state.lastSeq + 1; + if (record.seq !== expected) throw new Error(`event sequence ${record.seq}; expected ${expected}`); + const payload = clone(record.payload || {}); + let outputs = []; + + switch (record.kind) { + case "advance": + outputs = this.#advance(payload.ticks); + break; + case "visitor-enter": + this.#visitorEnter(payload); + break; + case "visitor-move": + this.#visitorMove(payload); + break; + case "visitor-signal": + outputs = this.#visitorSignal(payload); + break; + case "visitor-leave": + this.#visitorLeave(payload); + break; + default: + throw new Error(`unknown event kind: ${record.kind}`); + } + + this.state.lastSeq = record.seq; + return outputs; + } + + #visitorEnter(payload) { + const handle = normalizeHandle(payload.handle); + if (!this.state.visitors[handle] && Object.keys(this.state.visitors).length >= LIMITS.visitors) { + throw new Error("visitor limit reached"); + } + this.state.visitors[handle] = { handle, ...position(payload.position), enteredAtTick: this.state.tick }; + pushEpisode(this.state, { tick: this.state.tick, kind: "met", actor: handle }); + this.state.mind.drives.social = round(clamp(this.state.mind.drives.social + 0.08, 0, 1)); + } + + #visitorMove(payload) { + const handle = normalizeHandle(payload.handle); + const visitor = this.state.visitors[handle]; + if (!visitor) throw new Error("visitor is not present"); + Object.assign(visitor, position(payload.position)); + } + + #visitorSignal(payload) { + const handle = normalizeHandle(payload.handle); + const visitor = this.state.visitors[handle]; + if (!visitor) throw new Error("visitor is not present"); + const signal = String(payload.signal || "").trim().slice(0, LIMITS.signalLength); + if (!signal) throw new Error("empty visitor signal"); + const salience = round(clamp(0.25 + signal.length / 240, 0.25, 0.9)); + pushEpisode(this.state, { tick: this.state.tick, kind: "signal", actor: handle, signal, salience }); + this.state.mind.weights.approach = round(clamp(this.state.mind.weights.approach + salience * 0.01, 0, 1)); + return [this.#sonic({ + entity: this.state.entities[0], + cause: `visitor-signal:${handle}`, + voice: "answer", + pitch: 220 + signal.length, + intensity: 0.45 + salience * 0.25, + })]; + } + + #visitorLeave(payload) { + const handle = normalizeHandle(payload.handle); + if (this.state.visitors[handle]) { + delete this.state.visitors[handle]; + pushEpisode(this.state, { tick: this.state.tick, kind: "parted", actor: handle }); + } + } + + #advance(value) { + const ticks = Number(value); + if (!Number.isInteger(ticks) || ticks < 1 || ticks > LIMITS.advanceTicks) { + throw new TypeError(`advance ticks must be 1..${LIMITS.advanceTicks}`); + } + const outputs = []; + const rng = Prng.fromJSON(this.state.rng); + for (let step = 0; step < ticks; step += 1) { + this.state.tick += 1; + for (let index = 0; index < this.state.entities.length; index += 1) { + const entity = this.state.entities[index]; + if ((this.state.tick + index) % 17 === 0) { + entity.vx = round(clamp(entity.vx + rng.signed() * 0.025, -0.09, 0.09)); + entity.vy = round(clamp(entity.vy + rng.signed() * 0.008, -0.025, 0.025)); + entity.vz = round(clamp(entity.vz + rng.signed() * 0.025, -0.09, 0.09)); + } + entity.x = round(entity.x + entity.vx); + entity.y = round(entity.y + entity.vy); + entity.z = round(entity.z + entity.vz); + if (Math.abs(entity.x) > WORLD_RADIUS) { entity.x = round(clamp(entity.x, -WORLD_RADIUS, WORLD_RADIUS)); entity.vx = -entity.vx; } + if (entity.y < 0.2 || entity.y > 5) { entity.y = round(clamp(entity.y, 0.2, 5)); entity.vy = -entity.vy; } + if (Math.abs(entity.z) > WORLD_RADIUS) { entity.z = round(clamp(entity.z, -WORLD_RADIUS, WORLD_RADIUS)); entity.vz = -entity.vz; } + entity.energy = round(clamp(entity.energy + rng.signed() * 0.002, 0.05, 1)); + if ((this.state.tick + index * 7) % 53 === 0) { + outputs.push(this.#sonic({ + entity, + cause: "autonomous-pulse", + voice: entity.species, + pitch: 120 + index * 19 + Math.round(entity.energy * 90), + intensity: 0.2 + entity.energy * 0.45, + })); + } + } + this.state.mind.drives.curiosity = round(clamp(this.state.mind.drives.curiosity + rng.signed() * 0.003, 0, 1)); + this.state.mind.drives.rest = round(clamp(this.state.mind.drives.rest + 0.0004, 0, 1)); + this.state.mind.drives.social = round(clamp(this.state.mind.drives.social - 0.0002, 0, 1)); + } + this.state.rng = rng.toJSON(); + return outputs; + } + + #sonic({ entity, cause, voice, pitch, intensity }) { + this.state.sonicCount += 1; + const event = { + id: `sonic-${String(this.state.sonicCount).padStart(8, "0")}`, + tick: this.state.tick, + kind: "sonic", + source: [entity.x, entity.y, entity.z], + voice, + pitch: round(clamp(pitch, 40, 4000)), + intensity: round(clamp(intensity, 0, 1)), + radius: round(4 + intensity * 14), + duration: round(0.08 + intensity * 0.45), + cause, + }; + pushEpisode(this.state, { tick: this.state.tick, kind: "sang", actor: entity.id, cause, sonicId: event.id }); + return event; + } +} + +export function outputHash(outputs) { + return hash(canonical(outputs)); +} diff --git a/experiments/intelligent-terrarium/src/prng.mjs b/experiments/intelligent-terrarium/src/prng.mjs new file mode 100644 index 0000000000..09373898e6 --- /dev/null +++ b/experiments/intelligent-terrarium/src/prng.mjs @@ -0,0 +1,46 @@ +import { createHash } from "node:crypto"; + +function seedWords(seed) { + const bytes = createHash("sha256").update(String(seed)).digest(); + const words = []; + for (let i = 0; i < 4; i += 1) words.push(bytes.readUInt32LE(i * 4) || (0x9e3779b9 + i)); + return words; +} + +export class Prng { + constructor(seedOrState) { + this.words = Array.isArray(seedOrState) ? seedOrState.map((word) => word >>> 0) : seedWords(seedOrState); + if (this.words.length !== 4) throw new TypeError("PRNG state must contain four words"); + this.calls = 0; + } + + static fromJSON(state) { + const prng = new Prng(state.words); + prng.calls = state.calls >>> 0; + return prng; + } + + nextUint() { + let [x, y, z, w] = this.words; + const t = (x ^ (x << 11)) >>> 0; + x = y; + y = z; + z = w; + w = (w ^ (w >>> 19) ^ t ^ (t >>> 8)) >>> 0; + this.words = [x, y, z, w]; + this.calls = (this.calls + 1) >>> 0; + return w; + } + + float() { + return this.nextUint() / 0x100000000; + } + + signed() { + return this.float() * 2 - 1; + } + + toJSON() { + return { words: [...this.words], calls: this.calls }; + } +} diff --git a/experiments/intelligent-terrarium/src/repository.mjs b/experiments/intelligent-terrarium/src/repository.mjs new file mode 100644 index 0000000000..cf3e0033f9 --- /dev/null +++ b/experiments/intelligent-terrarium/src/repository.mjs @@ -0,0 +1,146 @@ +import { appendFile, mkdir, readFile, readdir, stat } from "node:fs/promises"; +import { basename, join, relative } from "node:path"; +import { atomicWrite, canonical, clone, hash } from "./canonical.mjs"; +import { Terrarium } from "./core.mjs"; + +const ZERO_HASH = "0".repeat(64); + +async function exists(path) { + try { await stat(path); return true; } catch { return false; } +} + +async function filesBelow(root) { + if (!(await exists(root))) return []; + const output = []; + for (const entry of await readdir(root, { withFileTypes: true })) { + const path = join(root, entry.name); + if (entry.isDirectory()) output.push(...await filesBelow(path)); + else if (entry.isFile()) output.push(path); + } + return output.sort(); +} + +async function recoverTail(root, path) { + const bytes = await readFile(path); + let offset = 0; + let validOffset = 0; + while (offset < bytes.length) { + const newline = bytes.indexOf(0x0a, offset); + const end = newline === -1 ? bytes.length : newline; + const line = bytes.subarray(offset, end).toString("utf8"); + if (!line.trim()) { + validOffset = newline === -1 ? end : end + 1; + offset = validOffset; + continue; + } + try { + JSON.parse(line); + validOffset = newline === -1 ? end : end + 1; + offset = validOffset; + } catch (error) { + const laterNewline = newline !== -1 && newline < bytes.length - 1; + if (laterNewline) throw new Error(`invalid journal record before tail in ${path}: ${error.message}`); + const tail = bytes.subarray(offset); + const quarantineDir = join(root, "quarantine"); + await mkdir(quarantineDir, { recursive: true }); + const quarantine = join(quarantineDir, `${basename(path)}.${hash(tail.toString("hex")).slice(0, 12)}.tail`); + await atomicWrite(quarantine, tail); + await atomicWrite(path, bytes.subarray(0, validOffset)); + return { recoveredBytes: tail.length, quarantine: relative(root, quarantine) }; + } + } + return null; +} + +function recordHash(record) { + const { recordHash: ignored, ...body } = record; + return hash(body); +} + +export class StateRepository { + constructor(root, seedDocument, terrarium, records, headRecordHash, segmentPath, recoveries) { + this.root = root; + this.seedDocument = seedDocument; + this.terrarium = terrarium; + this.records = records; + this.headRecordHash = headRecordHash; + this.segmentPath = segmentPath; + this.recoveries = recoveries; + } + + static async create(root, { seed, terrariumId = "intelligent-terrarium", profile = "1gb" } = {}) { + await mkdir(root, { recursive: true }); + const seedPath = join(root, "seed.json"); + if (!(await exists(seedPath))) { + const document = { schema: 1, terrariumId, seed: String(seed), profile }; + await atomicWrite(seedPath, `${canonical(document)}\n`); + } + return this.open(root); + } + + static async open(root, { segmentId = "wake" } = {}) { + const seedDocument = JSON.parse(await readFile(join(root, "seed.json"), "utf8")); + const journalRoot = join(root, "journal"); + const journalFiles = (await filesBelow(journalRoot)).filter((path) => path.endsWith(".ndjson")); + const recoveries = []; + for (const file of journalFiles) { + const recovery = await recoverTail(root, file); + if (recovery) recoveries.push(recovery); + } + + const terrarium = new Terrarium(seedDocument.seed); + const records = []; + let previousHash = ZERO_HASH; + for (const file of journalFiles) { + const contents = await readFile(file, "utf8"); + for (const line of contents.split("\n")) { + if (!line.trim()) continue; + const record = JSON.parse(line); + if (record.schema !== 1) throw new Error(`unsupported journal schema ${record.schema}`); + if (record.prevHash !== previousHash) throw new Error(`journal hash chain broken at event ${record.seq}`); + if (record.recordHash !== recordHash(record)) throw new Error(`journal record hash mismatch at event ${record.seq}`); + terrarium.apply(record); + if (terrarium.stateHash() !== record.stateHash) throw new Error(`state hash mismatch at event ${record.seq}`); + previousHash = record.recordHash; + records.push(record); + } + } + + const segmentPath = join(journalRoot, "segments", `${segmentId}.ndjson`); + await mkdir(join(journalRoot, "segments"), { recursive: true }); + return new StateRepository(root, seedDocument, terrarium, records, previousHash, segmentPath, recoveries); + } + + async transact(kind, payload) { + const candidate = Terrarium.fromSnapshot(this.terrarium.snapshot()); + const record = { + schema: 1, + seq: candidate.state.lastSeq + 1, + kind, + payload: clone(payload || {}), + prevHash: this.headRecordHash, + }; + const outputs = candidate.apply(record); + record.stateHash = candidate.stateHash(); + record.recordHash = recordHash(record); + await appendFile(this.segmentPath, `${canonical(record)}\n`, { encoding: "utf8", mode: 0o600, flush: true }); + this.terrarium = candidate; + this.records.push(record); + this.headRecordHash = record.recordHash; + return { record: clone(record), outputs: clone(outputs) }; + } + + stateHash() { + return this.terrarium.stateHash(); + } +} + +export async function verifyRepository(root) { + const repository = await StateRepository.open(root, { segmentId: "verify" }); + return { + stateHash: repository.stateHash(), + lastSeq: repository.terrarium.state.lastSeq, + headRecordHash: repository.headRecordHash, + recoveries: repository.recoveries, + }; +} diff --git a/experiments/intelligent-terrarium/src/sleep.mjs b/experiments/intelligent-terrarium/src/sleep.mjs new file mode 100644 index 0000000000..2bdfd6c4a1 --- /dev/null +++ b/experiments/intelligent-terrarium/src/sleep.mjs @@ -0,0 +1,108 @@ +import { execFileSync } from "node:child_process"; +import { mkdir, readFile, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { atomicWrite, canonical, hash } from "./canonical.mjs"; +import { verifyRepository } from "./repository.mjs"; + +const ALLOWLIST = [ + ".gitignore", + "seed.json", + "journal", + "checkpoints", + "autobiography", + "visitors", + "manifest.json", +]; + +async function exists(path) { + try { await stat(path); return true; } catch { return false; } +} + +function git(root, args, options = {}) { + return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: options.stdio || "pipe" }).trim(); +} + +function gitMaybe(root, args) { + try { return git(root, args); } catch { return ""; } +} + +function isAllowed(path) { + return ALLOWLIST.some((allowed) => path === allowed || path.startsWith(`${allowed}/`)); +} + +async function ensureGit(root) { + if (!(await exists(join(root, ".git")))) git(root, ["init", "-b", "main"]); + if (!gitMaybe(root, ["config", "--get", "user.name"])) git(root, ["config", "user.name", "Intelligent Terrarium"]); + if (!gitMaybe(root, ["config", "--get", "user.email"])) git(root, ["config", "user.email", "terrarium@localhost"]); + git(root, ["config", "commit.gpgsign", "false"]); + const ignorePath = join(root, ".gitignore"); + if (!(await exists(ignorePath))) await atomicWrite(ignorePath, ".runtime/\nquarantine/\n"); +} + +function autobiography(state, stateHash) { + const handles = Object.keys(state.visitors).sort(); + const lastEpisodes = state.mind.episodes.slice(-8); + return [ + "# Sleep reflection", + "", + `- Event sequence: ${state.lastSeq}`, + `- Simulation tick: ${state.tick}`, + `- State hash: \`${stateHash}\``, + `- Visitors present at sleep: ${handles.length ? handles.join(", ") : "none"}`, + `- Semantic sounds composed: ${state.sonicCount}`, + `- Drives: curiosity ${state.mind.drives.curiosity}, rest ${state.mind.drives.rest}, social ${state.mind.drives.social}`, + "", + "## Recent remembered facts", + "", + ...(lastEpisodes.length ? lastEpisodes.map((episode) => `- tick ${episode.tick}: ${episode.kind} — ${episode.actor}`) : ["- none"]), + "", + ].join("\n"); +} + +export async function sleepCommit(repository, { day = "2026-07-23" } = {}) { + const root = repository.root; + await ensureGit(root); + const manifestPath = join(root, "manifest.json"); + if (await exists(manifestPath)) { + const previous = JSON.parse(await readFile(manifestPath, "utf8")); + if (previous.lastSeq === repository.terrarium.state.lastSeq && previous.stateHash === repository.stateHash()) { + return { status: "no-change", commit: git(root, ["rev-parse", "HEAD"]), ...previous }; + } + } + + const state = repository.terrarium.snapshot(); + const stateHash = repository.stateHash(); + const checkpoint = { schema: 1, lastSeq: state.lastSeq, stateHash, state }; + const manifest = { + schema: 1, + lastSeq: state.lastSeq, + tick: state.tick, + stateHash, + headRecordHash: repository.headRecordHash, + seedHash: hash(repository.seedDocument), + profile: repository.seedDocument.profile, + }; + await mkdir(join(root, "checkpoints"), { recursive: true }); + await mkdir(join(root, "autobiography"), { recursive: true }); + await mkdir(join(root, "visitors"), { recursive: true }); + await atomicWrite(join(root, "checkpoints", "latest.json"), `${canonical(checkpoint)}\n`); + await atomicWrite(join(root, "manifest.json"), `${canonical(manifest)}\n`); + await atomicWrite(join(root, "visitors", "handles.json"), `${canonical({ handles: Object.keys(state.visitors).sort() })}\n`); + await atomicWrite(join(root, "autobiography", `${day}.md`), autobiography(state, stateHash)); + + const verification = await verifyRepository(root); + if (verification.stateHash !== stateHash || verification.lastSeq !== state.lastSeq) { + throw new Error("sleep replay verification failed"); + } + + const stagedBefore = git(root, ["diff", "--cached", "--name-only"]).split("\n").filter(Boolean); + if (stagedBefore.some((path) => !isAllowed(path))) throw new Error("refusing to commit pre-staged path outside state allowlist"); + git(root, ["add", "--", ...ALLOWLIST]); + const staged = git(root, ["diff", "--cached", "--name-only"]).split("\n").filter(Boolean); + if (staged.some((path) => !isAllowed(path))) throw new Error("refusing to commit path outside state allowlist"); + if (!staged.length) return { status: "no-change", commit: git(root, ["rev-parse", "HEAD"]), ...manifest }; + + const message = `sleep: events 1-${String(state.lastSeq).padStart(6, "0")} state ${stateHash.slice(0, 8)}`; + git(root, ["commit", "--no-gpg-sign", "-m", message]); + return { status: "committed", commit: git(root, ["rev-parse", "HEAD"]), staged, ...manifest }; +} diff --git a/experiments/intelligent-terrarium/test/core.test.mjs b/experiments/intelligent-terrarium/test/core.test.mjs new file mode 100644 index 0000000000..9dec7557eb --- /dev/null +++ b/experiments/intelligent-terrarium/test/core.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { LIMITS, outputHash, Terrarium } from "../src/core.mjs"; +import { StateRepository } from "../src/repository.mjs"; + +async function scripted(root) { + const repository = await StateRepository.create(root, { seed: "same-seed" }); + await repository.transact("visitor-enter", { handle: "@alex", position: { x: -3, y: 1.6, z: 2 } }); + const signal = await repository.transact("visitor-signal", { handle: "@alex", signal: "listen" }); + const advance = await repository.transact("advance", { ticks: 240 }); + await repository.transact("visitor-move", { handle: "@alex", position: { x: 4, y: 1.6, z: -1 } }); + return { repository, outputs: [...signal.outputs, ...advance.outputs] }; +} + +test("same seed and events reproduce state and semantic sound", async () => { + const parent = await mkdtemp(join(tmpdir(), "terrarium-core-")); + const first = await scripted(join(parent, "a")); + const second = await scripted(join(parent, "b")); + assert.equal(first.repository.stateHash(), second.repository.stateHash()); + assert.equal(first.repository.headRecordHash, second.repository.headRecordHash); + assert.equal(outputHash(first.outputs), outputHash(second.outputs)); + assert.ok(first.outputs.length > 1); + for (const event of first.outputs) { + assert.equal(event.kind, "sonic"); + assert.equal(event.source.length, 3); + assert.ok(event.intensity >= 0 && event.intensity <= 1); + assert.ok(event.radius > 0); + } +}); + +test("active episodic memory stays bounded", () => { + const terrarium = new Terrarium("bounded"); + let seq = 1; + terrarium.apply({ seq: seq++, kind: "visitor-enter", payload: { handle: "@alex" } }); + for (let index = 0; index < LIMITS.episodes + 80; index += 1) { + terrarium.apply({ seq: seq++, kind: "visitor-signal", payload: { handle: "@alex", signal: `signal-${index}` } }); + } + assert.equal(terrarium.state.mind.episodes.length, LIMITS.episodes); + assert.equal(Object.keys(terrarium.state.visitors).length, 1); +}); + +test("unverified-looking handles and oversized advances are rejected", () => { + const terrarium = new Terrarium("validation"); + assert.throws(() => terrarium.apply({ seq: 1, kind: "visitor-enter", payload: { handle: "alex/../../token" } }), /invalid verified handle/); + assert.throws(() => terrarium.apply({ seq: 1, kind: "advance", payload: { ticks: LIMITS.advanceTicks + 1 } }), /advance ticks/); +}); diff --git a/experiments/intelligent-terrarium/test/repository.test.mjs b/experiments/intelligent-terrarium/test/repository.test.mjs new file mode 100644 index 0000000000..d1fc014b19 --- /dev/null +++ b/experiments/intelligent-terrarium/test/repository.test.mjs @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import { appendFile, mkdtemp, readFile, readdir, writeFile } from "node:fs/promises"; +import { execFileSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { StateRepository, verifyRepository } from "../src/repository.mjs"; +import { sleepCommit } from "../src/sleep.mjs"; + +test("journal replays and quarantines a truncated final record", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-recovery-")); + const repository = await StateRepository.create(root, { seed: "recover-me" }); + await repository.transact("advance", { ticks: 90 }); + const expected = repository.stateHash(); + await appendFile(repository.segmentPath, '{"schema":1,"seq":2'); + + const reopened = await StateRepository.open(root); + assert.equal(reopened.stateHash(), expected); + assert.equal(reopened.recoveries.length, 1); + assert.ok(reopened.recoveries[0].recoveredBytes > 0); + const quarantine = await readdir(join(root, "quarantine")); + assert.equal(quarantine.length, 1); + assert.doesNotMatch(await readFile(repository.segmentPath, "utf8"), /"seq":2$/); +}); + +test("tampered journal content fails hash verification", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-tamper-")); + const repository = await StateRepository.create(root, { seed: "tamper-proof" }); + await repository.transact("advance", { ticks: 25 }); + const original = await readFile(repository.segmentPath, "utf8"); + await writeFile(repository.segmentPath, original.replace('"ticks":25', '"ticks":26')); + await assert.rejects(StateRepository.open(root), /record hash mismatch|state hash mismatch/); +}); + +test("sleep commits only allowlisted state and no-op sleep makes no commit", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-sleep-")); + const repository = await StateRepository.create(root, { seed: "sleep-cycle" }); + await writeFile(join(root, "do-not-stage.txt"), "unrelated\n"); + await repository.transact("visitor-enter", { handle: "@alex", position: { x: 1, y: 1, z: 1 } }); + await repository.transact("advance", { ticks: 120 }); + + const first = await sleepCommit(repository, { day: "2026-07-23" }); + assert.equal(first.status, "committed"); + assert.equal(first.stateHash, repository.stateHash()); + const committed = execFileSync("git", ["show", "--pretty=", "--name-only", "HEAD"], { cwd: root, encoding: "utf8" }); + assert.doesNotMatch(committed, /do-not-stage/); + assert.match(execFileSync("git", ["status", "--short"], { cwd: root, encoding: "utf8" }), /\?\? do-not-stage\.txt/); + + const countBefore = execFileSync("git", ["rev-list", "--count", "HEAD"], { cwd: root, encoding: "utf8" }).trim(); + const second = await sleepCommit(repository, { day: "2026-07-23" }); + const countAfter = execFileSync("git", ["rev-list", "--count", "HEAD"], { cwd: root, encoding: "utf8" }).trim(); + assert.equal(second.status, "no-change"); + assert.equal(countBefore, countAfter); + + await repository.transact("advance", { ticks: 10 }); + const third = await sleepCommit(repository, { day: "2026-07-23" }); + assert.equal(third.status, "committed"); + assert.notEqual(third.commit, first.commit); + assert.deepEqual(await verifyRepository(root), { + stateHash: repository.stateHash(), + lastSeq: repository.terrarium.state.lastSeq, + headRecordHash: repository.headRecordHash, + recoveries: [], + }); +}); -- 2.51.2 From d1528bd4bccdb5c560a5b97aa5aa68988fd2ec4a Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 23 Jul 2026 12:06:31 -0700 Subject: [PATCH 02/14] Add proddable Mediorgan loopback visitor --- experiments/intelligent-terrarium/KEEPER.md | 14 +- .../intelligent-terrarium/package.json | 3 +- .../intelligent-terrarium/src/core.mjs | 58 ++++++ .../intelligent-terrarium/src/mediorgan.mjs | 64 ++++++ .../intelligent-terrarium/src/server.mjs | 197 ++++++++++++++++++ .../test/mediorgan.test.mjs | 68 ++++++ .../test/spatial-audio.test.mjs | 19 ++ .../intelligent-terrarium/web/client.mjs | 157 ++++++++++++++ .../intelligent-terrarium/web/index.html | 48 +++++ .../web/spatial-audio.mjs | 25 +++ 10 files changed, 650 insertions(+), 3 deletions(-) create mode 100644 experiments/intelligent-terrarium/src/mediorgan.mjs create mode 100644 experiments/intelligent-terrarium/src/server.mjs create mode 100644 experiments/intelligent-terrarium/test/mediorgan.test.mjs create mode 100644 experiments/intelligent-terrarium/test/spatial-audio.test.mjs create mode 100644 experiments/intelligent-terrarium/web/client.mjs create mode 100644 experiments/intelligent-terrarium/web/index.html create mode 100644 experiments/intelligent-terrarium/web/spatial-audio.mjs diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md index 807b508ba5..290b2f1aeb 100644 --- a/experiments/intelligent-terrarium/KEEPER.md +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -16,6 +16,13 @@ synthesis, never world truth. Durable growth is replayable text data in a separate Git repository; model weights, caches, tokens, and derived binaries never enter autobiographical history. +The mind's bounded subsystems are **organs**: sensory, spatial, drive, memory, +action, voice, and sleep. The **mediorgan** is their externally touchable +membrane. An authenticated visitor can prod a named organ with a bounded text, +gesture, proximity, sound, or media stimulus. The prod receives a causal ID and +spatial origin and is journaled, but it is never a direct state mutation; the +target organ decides whether and how the body responds. + ```mermaid flowchart TD V[AC visitor
WebGL + gamepad] -->|token + input| A[terrariumd authority] @@ -95,11 +102,14 @@ about 6 MiB. No unrelated dirty files were changed. `{id, tick, kind, source:[x,y,z], voice, pitch, intensity, radius, duration, cause}`. Clients deduplicate by `id` and render locally. No live audio stream is part of world truth. -6. **Git is sleep, not the hot database:** append journal segments during wake; +6. **Proddable organs, authoritative body:** every outside prod crosses the + mediorgan, which resolves identity, enforces modality/size/rate bounds, and + records causality before routing. Organs can ignore or transform prods. +7. **Git is sleep, not the hot database:** append journal segments during wake; on sleep, fsync and close the segment, write an atomic checkpoint and autobiographical summary, verify replay, then commit only if content changed. No automatic push. -7. **Crash safety:** the last valid commit plus any fully written, checksummed +8. **Crash safety:** the last valid commit plus any fully written, checksummed journal records is sufficient for recovery. A partially written tail is quarantined, never guessed through. diff --git a/experiments/intelligent-terrarium/package.json b/experiments/intelligent-terrarium/package.json index 28c72ea040..af01d7e447 100644 --- a/experiments/intelligent-terrarium/package.json +++ b/experiments/intelligent-terrarium/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "test": "node --test test/*.test.mjs", - "demo": "node src/cli.mjs demo" + "demo": "node src/cli.mjs demo", + "visit": "node src/server.mjs" } } diff --git a/experiments/intelligent-terrarium/src/core.mjs b/experiments/intelligent-terrarium/src/core.mjs index 7eb3a55404..bf299be043 100644 --- a/experiments/intelligent-terrarium/src/core.mjs +++ b/experiments/intelligent-terrarium/src/core.mjs @@ -9,6 +9,15 @@ export const LIMITS = Object.freeze({ advanceTicks: 600, }); +export const ORGAN_NAMES = Object.freeze([ + "sensory", + "spatial", + "drive", + "memory", + "action", + "voice", +]); + const WORLD_RADIUS = 12; const round = (value) => Math.round(value * 1e6) / 1e6; const clamp = (value, low, high) => Math.max(low, Math.min(high, value)); @@ -106,6 +115,9 @@ export class Terrarium { case "visitor-signal": outputs = this.#visitorSignal(payload); break; + case "organ-prod": + outputs = this.#organProd(payload); + break; case "visitor-leave": this.#visitorLeave(payload); break; @@ -160,6 +172,52 @@ export class Terrarium { } } + #organProd(payload) { + const handle = normalizeHandle(payload.handle); + const visitor = this.state.visitors[handle]; + if (!visitor) throw new Error("visitor is not present"); + const target = String(payload.target || ""); + if (!ORGAN_NAMES.includes(target)) throw new Error("unknown target organ"); + const modality = String(payload.modality || "text"); + if (!["text", "gesture", "proximity", "sound", "media"].includes(modality)) { + throw new Error("unknown prod modality"); + } + const prodId = String(payload.prodId || "").slice(0, 96); + if (!prodId) throw new Error("prod requires a causal id"); + const stimulus = typeof payload.stimulus === "string" + ? payload.stimulus.slice(0, LIMITS.signalLength) + : clone(payload.stimulus || {}); + const summary = typeof stimulus === "string" ? stimulus : canonical(stimulus).slice(0, LIMITS.signalLength); + pushEpisode(this.state, { + tick: this.state.tick, + kind: "prodded", + actor: handle, + organ: target, + modality, + prodId, + stimulus: summary, + }); + + if (target === "spatial") Object.assign(visitor, position(payload.position || stimulus?.position)); + if (target === "drive" || target === "sensory") { + this.state.mind.drives.curiosity = round(clamp(this.state.mind.drives.curiosity + 0.03, 0, 1)); + } + if (target === "action") { + this.state.mind.weights.approach = round(clamp(this.state.mind.weights.approach + 0.02, 0, 1)); + } + if (target === "voice" || modality === "sound") { + const entity = this.state.entities[this.state.tick % this.state.entities.length]; + return [this.#sonic({ + entity, + cause: `prod:${prodId}:${target}`, + voice: target === "voice" ? "mediorgan-answer" : "felt-sound", + pitch: 180 + summary.length * 3, + intensity: 0.48, + })]; + } + return []; + } + #advance(value) { const ticks = Number(value); if (!Number.isInteger(ticks) || ticks < 1 || ticks > LIMITS.advanceTicks) { diff --git a/experiments/intelligent-terrarium/src/mediorgan.mjs b/experiments/intelligent-terrarium/src/mediorgan.mjs new file mode 100644 index 0000000000..aa78604fd1 --- /dev/null +++ b/experiments/intelligent-terrarium/src/mediorgan.mjs @@ -0,0 +1,64 @@ +import { randomUUID, timingSafeEqual } from "node:crypto"; +import { canonical } from "./canonical.mjs"; +import { normalizeHandle, ORGAN_NAMES } from "./core.mjs"; + +const MODALITIES = new Set(["text", "gesture", "proximity", "sound", "media"]); + +function sameSecret(a, b) { + const left = Buffer.from(a); + const right = Buffer.from(b); + return left.length === right.length && timingSafeEqual(left, right); +} + +export class Mediorgan { + constructor(repository, { capabilities = {}, maxProdsPerSecond = 12 } = {}) { + this.repository = repository; + this.capabilities = new Map(Object.entries(capabilities).map(([token, handle]) => [token, normalizeHandle(handle)])); + this.maxProdsPerSecond = maxProdsPerSecond; + this.rate = new Map(); + } + + authenticate(header) { + const match = /^Bearer ([^\s]+)$/.exec(String(header || "")); + if (!match) return null; + for (const [token, handle] of this.capabilities) { + if (sameSecret(token, match[1])) return handle; + } + return null; + } + + async ensurePresent(handle) { + if (!this.repository.terrarium.state.visitors[handle]) { + return this.repository.transact("visitor-enter", { handle, position: { x: 0, y: 1.7, z: 6 } }); + } + return null; + } + + async prod(handle, request) { + this.#takeRate(handle); + const target = String(request?.target || ""); + const modality = String(request?.modality || "text"); + if (!ORGAN_NAMES.includes(target)) throw new TypeError("unknown target organ"); + if (!MODALITIES.has(modality)) throw new TypeError("unknown prod modality"); + if (canonical(request?.stimulus ?? "").length > 4096) throw new TypeError("prod stimulus is too large"); + await this.ensurePresent(handle); + const payload = { + handle, + prodId: randomUUID(), + target, + modality, + stimulus: request?.stimulus ?? "", + }; + if (request?.position !== undefined) payload.position = request.position; + return this.repository.transact("organ-prod", payload); + } + + #takeRate(handle) { + const second = Math.floor(Date.now() / 1000); + const previous = this.rate.get(handle); + const current = previous?.second === second ? previous : { second, count: 0 }; + current.count += 1; + this.rate.set(handle, current); + if (current.count > this.maxProdsPerSecond) throw new Error("prod rate limit reached"); + } +} diff --git a/experiments/intelligent-terrarium/src/server.mjs b/experiments/intelligent-terrarium/src/server.mjs new file mode 100644 index 0000000000..3cde41dd52 --- /dev/null +++ b/experiments/intelligent-terrarium/src/server.mjs @@ -0,0 +1,197 @@ +#!/usr/bin/env node +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { dirname, extname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Mediorgan } from "./mediorgan.mjs"; +import { StateRepository } from "./repository.mjs"; +import { sleepCommit } from "./sleep.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const defaultWebRoot = resolve(here, "../web"); +const LOOPBACK = new Set(["127.0.0.1", "::1"]); + +function json(response, status, value) { + const data = Buffer.from(JSON.stringify(value)); + response.writeHead(status, { + "content-type": "application/json; charset=utf-8", + "content-length": data.length, + "cache-control": "no-store", + "x-content-type-options": "nosniff", + }); + response.end(data); +} + +async function requestBody(request, limit = 8192) { + const chunks = []; + let size = 0; + for await (const chunk of request) { + size += chunk.length; + if (size > limit) throw new Error("request body too large"); + chunks.push(chunk); + } + return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); +} + +function publicState(repository) { + const state = repository.terrarium.state; + return { + tick: state.tick, + lastSeq: state.lastSeq, + stateHash: repository.stateHash(), + entities: state.entities.map(({ id, species, x, y, z, energy }) => ({ id, species, x, y, z, energy })), + visitors: Object.values(state.visitors).map(({ handle, x, y, z }) => ({ handle, x, y, z })), + organs: ["sensory", "spatial", "drive", "memory", "action", "voice"], + membrane: "mediorgan", + }; +} + +export async function createTerrariumServer({ + root, + host = "127.0.0.1", + port = 0, + capabilities = {}, + tickMs = 100, + webRoot = defaultWebRoot, +} = {}) { + if (!LOOPBACK.has(host)) throw new Error("terrarium server refuses non-loopback binding"); + let repository; + try { + repository = await StateRepository.open(root, { segmentId: "visit" }); + } catch { + repository = await StateRepository.create(root, { seed: "intelligent-terrarium-visit-v1", profile: "1gb" }); + repository.segmentPath = join(root, "journal", "segments", "visit.ndjson"); + } + const mediorgan = new Mediorgan(repository, { capabilities }); + const streams = new Set(); + let writeTail = Promise.resolve(); + + function exclusive(operation) { + const result = writeTail.then(operation); + writeTail = result.catch(() => {}); + return result; + } + + function broadcast(message) { + const line = `${JSON.stringify(message)}\n`; + for (const response of streams) { + if (!response.write(line)) { + response.end(); + streams.delete(response); + } + } + } + + async function authenticate(request, response) { + const handle = mediorgan.authenticate(request.headers.authorization); + if (!handle) { + json(response, 401, { error: "valid development capability required" }); + return null; + } + await exclusive(() => mediorgan.ensurePresent(handle)); + return handle; + } + + const server = createServer(async (request, response) => { + try { + const url = new URL(request.url, `http://${request.headers.host || "127.0.0.1"}`); + if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/client.mjs" || url.pathname === "/spatial-audio.mjs")) { + const filename = url.pathname === "/" ? "index.html" : url.pathname.slice(1); + const content = await readFile(join(webRoot, filename)); + const contentType = extname(filename) === ".html" ? "text/html; charset=utf-8" : "text/javascript; charset=utf-8"; + response.writeHead(200, { + "content-type": contentType, + "content-length": content.length, + "cache-control": "no-store", + "content-security-policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'", + "x-content-type-options": "nosniff", + "x-frame-options": "DENY", + }); + response.end(content); + return; + } + if (request.method === "GET" && url.pathname === "/api/state") { + const handle = await authenticate(request, response); + if (!handle) return; + json(response, 200, { handle, state: publicState(repository) }); + return; + } + if (request.method === "GET" && url.pathname === "/api/stream") { + const handle = await authenticate(request, response); + if (!handle) return; + response.writeHead(200, { + "content-type": "application/x-ndjson; charset=utf-8", + "cache-control": "no-store", + connection: "keep-alive", + "x-content-type-options": "nosniff", + }); + response.write(`${JSON.stringify({ type: "welcome", handle, state: publicState(repository) })}\n`); + streams.add(response); + request.on("close", () => streams.delete(response)); + return; + } + if (request.method === "POST" && url.pathname === "/api/prod") { + const handle = await authenticate(request, response); + if (!handle) return; + const prodRequest = await requestBody(request); + const result = await exclusive(() => mediorgan.prod(handle, prodRequest)); + broadcast({ type: "snapshot", state: publicState(repository) }); + for (const event of result.outputs) broadcast({ type: "sonic", event }); + json(response, 202, { accepted: true, prodId: result.record.payload.prodId, eventSeq: result.record.seq, outputs: result.outputs }); + return; + } + json(response, 404, { error: "not found" }); + } catch (error) { + json(response, /rate limit|too large/.test(error.message) ? 429 : 400, { error: error.message }); + } + }); + + await new Promise((resolveListen, reject) => { + server.once("error", reject); + server.listen(port, host, resolveListen); + }); + const interval = tickMs > 0 ? setInterval(async () => { + try { + const result = await exclusive(() => repository.transact("advance", { ticks: 1 })); + broadcast({ type: "snapshot", state: publicState(repository) }); + for (const event of result.outputs) broadcast({ type: "sonic", event }); + } catch (error) { + console.error("terrarium tick failed:", error.message); + } + }, tickMs) : null; + interval?.unref(); + + return { + server, + repository, + mediorgan, + address: server.address(), + async stop({ sleep = false } = {}) { + if (interval) clearInterval(interval); + for (const response of streams) response.end(); + await new Promise((resolveClose) => server.close(resolveClose)); + await writeTail; + return sleep ? sleepCommit(repository) : null; + }, + }; +} + +function option(name, fallback) { + const index = process.argv.indexOf(name); + return index === -1 ? fallback : process.argv[index + 1]; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const host = option("--host", "127.0.0.1"); + const port = Number(option("--port", "8787")); + const root = resolve(option("--root", "./terrarium-state")); + const capabilities = JSON.parse(process.env.TERRARIUM_DEV_CAPS || "{}"); + const app = await createTerrariumServer({ root, host, port, capabilities }); + console.log(JSON.stringify({ listening: `http://${app.address.address}:${app.address.port}`, binding: app.address, root })); + const shutdown = async () => { + await app.stop({ sleep: true }); + process.exit(0); + }; + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); +} diff --git a/experiments/intelligent-terrarium/test/mediorgan.test.mjs b/experiments/intelligent-terrarium/test/mediorgan.test.mjs new file mode 100644 index 0000000000..a5e8a20226 --- /dev/null +++ b/experiments/intelligent-terrarium/test/mediorgan.test.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { createTerrariumServer } from "../src/server.mjs"; + +test("server refuses every non-loopback binding", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-bind-")); + await assert.rejects(createTerrariumServer({ root, host: "0.0.0.0" }), /refuses non-loopback/); +}); + +test("mediorgan authenticates, journals a bounded prod, and never journals capability", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-mediorgan-")); + const capability = "local-test-capability-never-journal"; + const app = await createTerrariumServer({ root, capabilities: { [capability]: "@alex" }, tickMs: 0 }); + const base = `http://${app.address.address}:${app.address.port}`; + assert.equal(app.address.address, "127.0.0.1"); + assert.equal((await fetch(`${base}/api/state`)).status, 401); + + const headers = { Authorization: `Bearer ${capability}`, "Content-Type": "application/json" }; + const stateResponse = await fetch(`${base}/api/state`, { headers }); + assert.equal(stateResponse.status, 200); + assert.equal((await stateResponse.json()).handle, "@alex"); + const prodResponse = await fetch(`${base}/api/prod`, { + method: "POST", + headers, + body: JSON.stringify({ target: "voice", modality: "text", stimulus: "hello organ", position: { x: -2, y: 1, z: 1 } }), + }); + assert.equal(prodResponse.status, 202); + const accepted = await prodResponse.json(); + assert.match(accepted.prodId, /^[0-9a-f-]{36}$/); + assert.equal(accepted.outputs[0].kind, "sonic"); + assert.match(accepted.outputs[0].cause, new RegExp(accepted.prodId)); + const journal = await readFile(app.repository.segmentPath, "utf8"); + assert.match(journal, /"kind":"organ-prod"/); + assert.match(journal, /"handle":"@alex"/); + assert.doesNotMatch(journal, new RegExp(capability)); + await app.stop(); +}); + +test("loopback visitor page exposes a WebGL terrarium and mediorgan controls", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-page-")); + const app = await createTerrariumServer({ root, capabilities: {}, tickMs: 0 }); + const html = await (await fetch(`http://${app.address.address}:${app.address.port}/`)).text(); + assert.match(html, /]+terrarium/); + assert.match(html, /Connect mediorgan/); + assert.match(html, /Prod organ/); + assert.match(html, /Xbox-compatible controller/); + await app.stop(); +}); + +test("concurrent outside prods remain a single ordered journal", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-concurrent-")); + const capability = "local-concurrency-capability"; + const app = await createTerrariumServer({ root, capabilities: { [capability]: "@alex" }, tickMs: 0 }); + const base = `http://${app.address.address}:${app.address.port}`; + const responses = await Promise.all(Array.from({ length: 8 }, (_, index) => fetch(`${base}/api/prod`, { + method: "POST", + headers: { Authorization: `Bearer ${capability}`, "Content-Type": "application/json" }, + body: JSON.stringify({ target: "memory", modality: "text", stimulus: `prod-${index}` }), + }))); + assert.deepEqual(responses.map((response) => response.status), Array(8).fill(202)); + const sequences = await Promise.all(responses.map(async (response) => (await response.json()).eventSeq)); + assert.equal(new Set(sequences).size, sequences.length); + assert.deepEqual([...sequences].sort((a, b) => a - b), Array.from({ length: 8 }, (_, index) => index + 2)); + await app.stop(); +}); diff --git a/experiments/intelligent-terrarium/test/spatial-audio.test.mjs b/experiments/intelligent-terrarium/test/spatial-audio.test.mjs new file mode 100644 index 0000000000..2b81d15150 --- /dev/null +++ b/experiments/intelligent-terrarium/test/spatial-audio.test.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { SonicDeduper, spatialize } from "../web/spatial-audio.mjs"; + +test("semantic coordinates render left/right and attenuate monotonically", () => { + const listener = { x: 0, y: 0, z: 0, yaw: 0 }; + assert.ok(spatialize([-3, 0, 0], listener).pan < 0); + assert.ok(spatialize([3, 0, 0], listener).pan > 0); + assert.ok(spatialize([1, 0, 0], listener, 12).gain > spatialize([8, 0, 0], listener, 12).gain); +}); + +test("sonic event IDs are accepted once across reconnect replay", () => { + const deduper = new SonicDeduper(2); + assert.equal(deduper.accept("one"), true); + assert.equal(deduper.accept("one"), false); + assert.equal(deduper.accept("two"), true); + assert.equal(deduper.accept("three"), true); + assert.equal(deduper.accept("one"), true); +}); diff --git a/experiments/intelligent-terrarium/web/client.mjs b/experiments/intelligent-terrarium/web/client.mjs new file mode 100644 index 0000000000..df8d7046a1 --- /dev/null +++ b/experiments/intelligent-terrarium/web/client.mjs @@ -0,0 +1,157 @@ +import { SonicDeduper, spatialize } from "./spatial-audio.mjs"; + +const canvas = document.querySelector("#terrarium"); +const status = document.querySelector("#status"); +const connectButton = document.querySelector("#connect"); +const prodButton = document.querySelector("#prod"); +const capabilityInput = document.querySelector("#capability"); +const organInput = document.querySelector("#organ"); +const modalityInput = document.querySelector("#modality"); +const stimulusInput = document.querySelector("#stimulus"); +const gl = canvas.getContext("webgl2", { antialias: true, alpha: false }); +const camera = { x: 0, y: 2.2, z: 14, yaw: 0 }; +const keys = new Set(); +const deduper = new SonicDeduper(); +let capability = ""; +let world = { tick: 0, entities: [], visitors: [] }; +let audio = null; +let lastSpatialProd = 0; + +function shader(type, source) { + const value = gl.createShader(type); + gl.shaderSource(value, source); + gl.compileShader(value); + if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(value)); + return value; +} + +const program = gl.createProgram(); +gl.attachShader(program, shader(gl.VERTEX_SHADER, `#version 300 es + in vec3 position; in float energy; uniform vec2 view; uniform vec3 camera; uniform float yaw; + out float glow; + void main() { + vec3 p = position - camera; + float c = cos(yaw), s = sin(yaw); + vec3 q = vec3(p.x*c - p.z*s, p.y, p.x*s + p.z*c); + float depth = max(1.0, -q.z); + gl_Position = vec4(q.x / depth / view.x, q.y / depth / view.y, 1.0 - 2.0 / depth, 1.0); + gl_PointSize = clamp(50.0 / depth + energy * 10.0, 5.0, 34.0); + glow = energy; + }`)); +gl.attachShader(program, shader(gl.FRAGMENT_SHADER, `#version 300 es + precision highp float; in float glow; out vec4 color; + void main() { + float d = length(gl_PointCoord - vec2(.5)); if (d > .5) discard; + float a = smoothstep(.5, .1, d); color = vec4(.25 + glow*.3, .75 + glow*.2, .45 + glow*.35, a); + }`)); +gl.linkProgram(program); +if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program)); +const buffer = gl.createBuffer(); +const positionLocation = gl.getAttribLocation(program, "position"); +const energyLocation = gl.getAttribLocation(program, "energy"); + +function draw() { + canvas.width = Math.max(1, Math.floor(canvas.clientWidth * devicePixelRatio)); + canvas.height = Math.max(1, Math.floor(canvas.clientHeight * devicePixelRatio)); + gl.viewport(0, 0, canvas.width, canvas.height); + gl.clearColor(0.025, 0.075, 0.05, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.useProgram(program); + const values = new Float32Array(world.entities.flatMap((entity) => [entity.x, entity.y, entity.z, entity.energy])); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, values, gl.DYNAMIC_DRAW); + gl.enableVertexAttribArray(positionLocation); + gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 16, 0); + gl.enableVertexAttribArray(energyLocation); + gl.vertexAttribPointer(energyLocation, 1, gl.FLOAT, false, 16, 12); + gl.uniform2f(gl.getUniformLocation(program, "view"), canvas.height / canvas.width, 1); + gl.uniform3f(gl.getUniformLocation(program, "camera"), camera.x, camera.y, camera.z); + gl.uniform1f(gl.getUniformLocation(program, "yaw"), camera.yaw); + gl.drawArrays(gl.POINTS, 0, world.entities.length); +} + +function playSonic(event) { + if (!deduper.accept(event.id) || !audio) return; + const rendered = spatialize(event.source, camera, event.radius); + const oscillator = audio.createOscillator(); + const gain = audio.createGain(); + const panner = audio.createStereoPanner(); + oscillator.frequency.value = event.pitch; + oscillator.type = event.voice.includes("moss") ? "sine" : "triangle"; + panner.pan.value = rendered.pan; + gain.gain.setValueAtTime(Math.max(0.0001, rendered.gain * event.intensity * 0.18), audio.currentTime); + gain.gain.exponentialRampToValueAtTime(0.0001, audio.currentTime + event.duration); + oscillator.connect(gain).connect(panner).connect(audio.destination); + oscillator.start(); + oscillator.stop(audio.currentTime + event.duration); + status.dataset.lastSonicPan = rendered.pan.toFixed(3); + status.dataset.lastSonicGain = rendered.gain.toFixed(3); +} + +async function connect() { + capability = capabilityInput.value.trim(); + audio ||= new AudioContext(); + await audio.resume(); + const response = await fetch("/api/stream", { headers: { Authorization: `Bearer ${capability}` } }); + if (!response.ok) throw new Error(`mediorgan refused (${response.status})`); + prodButton.disabled = false; + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let pending = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + pending += decoder.decode(value, { stream: true }); + const lines = pending.split("\n"); + pending = lines.pop(); + for (const line of lines) { + if (!line) continue; + const message = JSON.parse(line); + if (message.state) world = message.state; + if (message.type === "sonic") playSonic(message.event); + status.textContent = `mediorgan connected as ${message.handle || world.visitors[0]?.handle || "visitor"} • tick ${world.tick} • ${world.entities.length} organisms`; + status.dataset.connected = "true"; + draw(); + } + } +} + +async function prod() { + const response = await fetch("/api/prod", { + method: "POST", + headers: { Authorization: `Bearer ${capability}`, "Content-Type": "application/json" }, + body: JSON.stringify({ target: organInput.value, modality: modalityInput.value, stimulus: stimulusInput.value, position: camera }), + }); + if (!response.ok) throw new Error((await response.json()).error); + const result = await response.json(); + status.dataset.lastProdId = result.prodId; +} + +connectButton.addEventListener("click", () => connect().catch((error) => { status.textContent = error.message; })); +prodButton.addEventListener("click", () => prod().catch((error) => { status.textContent = error.message; })); +window.addEventListener("keydown", (event) => keys.add(event.key.toLowerCase())); +window.addEventListener("keyup", (event) => keys.delete(event.key.toLowerCase())); + +function move() { + let forward = (keys.has("w") || keys.has("arrowup") ? 1 : 0) - (keys.has("s") || keys.has("arrowdown") ? 1 : 0); + let turn = (keys.has("d") || keys.has("arrowright") ? 1 : 0) - (keys.has("a") || keys.has("arrowleft") ? 1 : 0); + const pad = navigator.getGamepads?.()[0]; + if (pad) { + forward -= Math.abs(pad.axes[1]) > 0.15 ? pad.axes[1] : 0; + turn += Math.abs(pad.axes[0]) > 0.15 ? pad.axes[0] : 0; + } + camera.yaw += turn * 0.025; + camera.x -= Math.sin(camera.yaw) * forward * 0.08; + camera.z -= Math.cos(camera.yaw) * forward * 0.08; + if (capability && performance.now() - lastSpatialProd > 250 && (Math.abs(forward) + Math.abs(turn) > 0.05)) { + lastSpatialProd = performance.now(); + fetch("/api/prod", { + method: "POST", + headers: { Authorization: `Bearer ${capability}`, "Content-Type": "application/json" }, + body: JSON.stringify({ target: "spatial", modality: "gesture", stimulus: { position: camera }, position: camera }), + }).catch(() => {}); + } + draw(); + requestAnimationFrame(move); +} +requestAnimationFrame(move); diff --git a/experiments/intelligent-terrarium/web/index.html b/experiments/intelligent-terrarium/web/index.html new file mode 100644 index 0000000000..461b115a2b --- /dev/null +++ b/experiments/intelligent-terrarium/web/index.html @@ -0,0 +1,48 @@ + + + + + + Intelligent Terrarium — Mediorgan + + + + +
+

Intelligent Terrarium

+

mediorgan asleep

+
+ + +
+
+ + +
+
+ + +
+

Move with WASD / arrows or an Xbox-compatible controller. A prod is a stimulus through the mediorgan, never a direct state write.

+
+ + + diff --git a/experiments/intelligent-terrarium/web/spatial-audio.mjs b/experiments/intelligent-terrarium/web/spatial-audio.mjs new file mode 100644 index 0000000000..521b74692b --- /dev/null +++ b/experiments/intelligent-terrarium/web/spatial-audio.mjs @@ -0,0 +1,25 @@ +export function spatialize(source, listener, radius = 12) { + const dx = Number(source[0]) - Number(listener.x || 0); + const dy = Number(source[1]) - Number(listener.y || 0); + const dz = Number(source[2]) - Number(listener.z || 0); + const yaw = Number(listener.yaw || 0); + const right = dx * Math.cos(yaw) - dz * Math.sin(yaw); + const distance = Math.hypot(dx, dy, dz); + const pan = Math.max(-1, Math.min(1, right / Math.max(1, Math.hypot(dx, dz)))); + const gain = Math.max(0, Math.min(1, 1 - distance / Math.max(0.001, radius))); + return { pan, gain, distance }; +} + +export class SonicDeduper { + constructor(limit = 256) { + this.limit = limit; + this.ids = new Set(); + } + + accept(id) { + if (this.ids.has(id)) return false; + this.ids.add(id); + if (this.ids.size > this.limit) this.ids.delete(this.ids.values().next().value); + return true; + } +} -- 2.51.2 From 711d9b33a5367e7a6437d3bbb5ec740a6a504f41 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 23 Jul 2026 12:08:36 -0700 Subject: [PATCH 03/14] Record intelligent terrarium keeper evidence --- experiments/intelligent-terrarium/KEEPER.md | 55 ++++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md index 290b2f1aeb..db397725c4 100644 --- a/experiments/intelligent-terrarium/KEEPER.md +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -115,8 +115,8 @@ about 6 MiB. No unrelated dirty files were changed. ### Durable state repository on `jastow` -Proposed location, to create only after a keeper decision: -`/home/me/intelligent-terrarium-state`. Keep it separate from +Approved and created location: `/home/me/intelligent-terrarium-state`. It is +kept separate from `/home/me/aesthetic-computer` so autonomous sleep commits cannot trigger AC hooks, mingle with application history, or stage unrelated work. @@ -175,7 +175,7 @@ commit. Git operations use explicit allowlisted paths; never `git add -A`. ### Stage 2 — loopback browser visitor Serve snapshots and sonic events on `127.0.0.1` only, by an ephemeral manual -process. Render a small Three.js terrarium, listener-relative stereo audio, +process. Render a small WebGL2 terrarium, listener-relative stereo audio, keyboard, and standard Gamepad API input. Use an SSH local forward for neo-side testing; no LAN/public listener or persistent service. @@ -232,8 +232,6 @@ The first thin slice is accepted only when all of these pass: ## Keeper decisions still required -- Permission to create `/home/me/intelligent-terrarium-state` and run an - ephemeral loopback-only process on `jastow`. - Whether/when to install or build a user-space inference runtime and acquire a specific quantized model. No system package installation is assumed. - The future authenticated gateway and who may visit/interact versus observe. @@ -244,6 +242,47 @@ The first thin slice is accepted only when all of these pass: ## Next keeper action -Implement Stage 0 locally in this sparse worktree with deterministic replay and -memory-accounting tests only. It needs no remote writes, model installation, -network listener, production path, or deployment. +Hold at the Stage 3 boundary for a keeper decision. Real AC identity requires a +hidden development piece and token-verifying gateway changes in served paths. +No model installation, push, deployment, persistent service, or public listener +is authorized. + +## 2026-07-23 implementation evidence + +Keeper approval arrived through the private `alex` Loopboy inbox for Stages +0–1, an isolated branch commit, creation of the state repository, and an +ephemeral loopback-only 1 GB process. Stage 2 was permitted only after 0–1 were +green; that gate passed. + +- Stage 0–1 code commit: + `4a6f2ec654ed3a6f1dd19c22706ea68156a6f404`. +- Stage 2 Mediorgan/visitor commit: + `d1528bd4bccdb5c560a5b97aa5aa68988fd2ec4a`. +- Local and Fedora suites: 12/12 Node tests pass. Coverage includes seeded + determinism, bounded memory, journal replay/tamper detection, truncated-tail + quarantine, allowlisted/no-op sleep commits, loopback refusal, capability + exclusion from journals, concurrent prod serialization, stereo geometry, and + sonic deduplication. +- Stage 0–1 Fedora run: 9,000 ticks, 902 records, state + `e469e1009e673571d539189621e3ab238ed388881a0a7cd8a11ec564a280fba7`, + cgroup `MemoryPeak=30277632`, `MemoryHigh=943718400`, + `MemoryMax=1073741824`, `MemorySwapMax=0`. State commit: + `27009678b7bf8f9d1ddbe481fd7d9edcef8491d7`. +- Stage 2 Fedora run: exact listener `127.0.0.1:18787`; unauthenticated state + request returned 401; a `voice` organ prod via `media` returned a causal + semantic sound. Peak cgroup charge was 26,771,456 bytes under the same hard + limit. Shutdown removed the listener and committed state as + `f8b09a1ac95c6db20aeb743434b9cc943c48e9ad`, with replay state + `7e21bc07cbd52b6291304d524041ee43191f7a11254990f9636a7482b6222c12`. +- The listener-set hash was identical before and after both remote experiments. + `matador-miner` remained PID 2928 at 3,298 MiB VRAM before, during, and after. + The tower AC checkout retained only its two pre-existing untracked files. +- No package/model was installed; no branch or state was pushed; no production + or served path was changed. + +The first-demo acceptance list is not yet wholly satisfied. The code exercises +WebGL2, keyboard/Gamepad input, WebAudio stereo rendering, and two-client stream +semantics, but the in-app browser-control bridge was unavailable, so visual and +physical-controller QA remain unchecked. The real AC Auth0/handle path is also +Stage 3, and the 9,000-tick memory run was accelerated rather than a wall-clock +15-minute soak. -- 2.51.2 From 3ae5151c1cb3036d09e3816f4e2c52ab06b379d9 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 23 Jul 2026 12:14:38 -0700 Subject: [PATCH 04/14] Serialize terrarium authority mutations --- experiments/intelligent-terrarium/KEEPER.md | 23 +++++++++++ .../intelligent-terrarium/src/repository.mjs | 11 +++++ .../intelligent-terrarium/src/server.mjs | 11 +++-- .../test/mediorgan.test.mjs | 40 +++++++++++++++++-- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md index db397725c4..984a3a66bc 100644 --- a/experiments/intelligent-terrarium/KEEPER.md +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -286,3 +286,26 @@ semantics, but the in-app browser-control bridge was unavailable, so visual and physical-controller QA remain unchecked. The real AC Auth0/handle path is also Stage 3, and the 9,000-tick memory run was accelerated rather than a wall-clock 15-minute soak. + +### Stage 2 one-writer review + +The private keeper review identified the repository append race that would +exist if ticks and outside prods could enter `StateRepository.transact` +concurrently. The authority now has two deliberate layers: the server queue +serializes whole mutations such as a Mediorgan prod, while the repository owns +an intrinsic append queue so no caller can clone the same pre-append head. +Shutdown drains both queues before sleep. The timer callback does not return an +unobserved promise; tick failures terminate in the injected `onTickError` +handler and later ticks continue. + +The expanded local suite passes 13/13. Its race case mixes the 1 ms autonomous +tick interval with eight simultaneous authenticated prods, then proves sequence +numbers are contiguous, every `prevHash` names the preceding record, all prods +land exactly once, and a fresh replay has the same final state/head hashes. +An injected tick failure is observed without stopping subsequent ticks. Both +tests also passed 20/20 focused stress repetitions. + +Alex's current execution boundary prohibits browser automation and all GUI +control, so the remaining visual, WebAudio-device, and physical Xbox-controller +checks were not attempted. Pure spatial-audio geometry/deduplication tests stay +green; interactive QA remains an explicit acceptance blocker. diff --git a/experiments/intelligent-terrarium/src/repository.mjs b/experiments/intelligent-terrarium/src/repository.mjs index cf3e0033f9..7b9a5e7ce9 100644 --- a/experiments/intelligent-terrarium/src/repository.mjs +++ b/experiments/intelligent-terrarium/src/repository.mjs @@ -66,6 +66,7 @@ export class StateRepository { this.headRecordHash = headRecordHash; this.segmentPath = segmentPath; this.recoveries = recoveries; + this.writeTail = Promise.resolve(); } static async create(root, { seed, terrariumId = "intelligent-terrarium", profile = "1gb" } = {}) { @@ -112,6 +113,12 @@ export class StateRepository { } async transact(kind, payload) { + const result = this.writeTail.then(() => this.#appendTransaction(kind, payload)); + this.writeTail = result.catch(() => {}); + return result; + } + + async #appendTransaction(kind, payload) { const candidate = Terrarium.fromSnapshot(this.terrarium.snapshot()); const record = { schema: 1, @@ -130,6 +137,10 @@ export class StateRepository { return { record: clone(record), outputs: clone(outputs) }; } + async idle() { + await this.writeTail; + } + stateHash() { return this.terrarium.stateHash(); } diff --git a/experiments/intelligent-terrarium/src/server.mjs b/experiments/intelligent-terrarium/src/server.mjs index 3cde41dd52..26b1299677 100644 --- a/experiments/intelligent-terrarium/src/server.mjs +++ b/experiments/intelligent-terrarium/src/server.mjs @@ -53,6 +53,7 @@ export async function createTerrariumServer({ capabilities = {}, tickMs = 100, webRoot = defaultWebRoot, + onTickError = (error) => console.error("terrarium tick failed:", error.message), } = {}) { if (!LOOPBACK.has(host)) throw new Error("terrarium server refuses non-loopback binding"); let repository; @@ -150,14 +151,11 @@ export async function createTerrariumServer({ server.once("error", reject); server.listen(port, host, resolveListen); }); - const interval = tickMs > 0 ? setInterval(async () => { - try { - const result = await exclusive(() => repository.transact("advance", { ticks: 1 })); + const interval = tickMs > 0 ? setInterval(() => { + void exclusive(() => repository.transact("advance", { ticks: 1 })).then((result) => { broadcast({ type: "snapshot", state: publicState(repository) }); for (const event of result.outputs) broadcast({ type: "sonic", event }); - } catch (error) { - console.error("terrarium tick failed:", error.message); - } + }).catch(onTickError); }, tickMs) : null; interval?.unref(); @@ -171,6 +169,7 @@ export async function createTerrariumServer({ for (const response of streams) response.end(); await new Promise((resolveClose) => server.close(resolveClose)); await writeTail; + await repository.idle(); return sleep ? sleepCommit(repository) : null; }, }; diff --git a/experiments/intelligent-terrarium/test/mediorgan.test.mjs b/experiments/intelligent-terrarium/test/mediorgan.test.mjs index a5e8a20226..ff2562e414 100644 --- a/experiments/intelligent-terrarium/test/mediorgan.test.mjs +++ b/experiments/intelligent-terrarium/test/mediorgan.test.mjs @@ -3,6 +3,7 @@ import { mkdtemp, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; +import { verifyRepository } from "../src/repository.mjs"; import { createTerrariumServer } from "../src/server.mjs"; test("server refuses every non-loopback binding", async () => { @@ -50,10 +51,10 @@ test("loopback visitor page exposes a WebGL terrarium and mediorgan controls", a await app.stop(); }); -test("concurrent outside prods remain a single ordered journal", async () => { +test("concurrent ticks and outside prods remain one contiguous replayable journal", async () => { const root = await mkdtemp(join(tmpdir(), "terrarium-concurrent-")); const capability = "local-concurrency-capability"; - const app = await createTerrariumServer({ root, capabilities: { [capability]: "@alex" }, tickMs: 0 }); + const app = await createTerrariumServer({ root, capabilities: { [capability]: "@alex" }, tickMs: 1 }); const base = `http://${app.address.address}:${app.address.port}`; const responses = await Promise.all(Array.from({ length: 8 }, (_, index) => fetch(`${base}/api/prod`, { method: "POST", @@ -63,6 +64,39 @@ test("concurrent outside prods remain a single ordered journal", async () => { assert.deepEqual(responses.map((response) => response.status), Array(8).fill(202)); const sequences = await Promise.all(responses.map(async (response) => (await response.json()).eventSeq)); assert.equal(new Set(sequences).size, sequences.length); - assert.deepEqual([...sequences].sort((a, b) => a - b), Array.from({ length: 8 }, (_, index) => index + 2)); + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); + await app.stop(); + const onlineHash = app.repository.stateHash(); + const records = app.repository.records; + assert.deepEqual(records.map(({ seq }) => seq), Array.from({ length: records.length }, (_, index) => index + 1)); + assert.equal(records[0].prevHash, "0".repeat(64)); + for (let index = 1; index < records.length; index += 1) { + assert.equal(records[index].prevHash, records[index - 1].recordHash); + } + assert.ok(records.some(({ kind }) => kind === "advance")); + assert.equal(records.filter(({ kind }) => kind === "organ-prod").length, 8); + const replay = await verifyRepository(root); + assert.equal(replay.lastSeq, records.length); + assert.equal(replay.stateHash, onlineHash); + assert.equal(replay.headRecordHash, records.at(-1).recordHash); +}); + +test("tick failures are observed without an unhandled rejection", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-tick-error-")); + const errors = []; + const app = await createTerrariumServer({ root, tickMs: 1, onTickError: (error) => errors.push(error) }); + const transact = app.repository.transact.bind(app.repository); + let rejectNextTick = true; + app.repository.transact = (kind, payload) => { + if (kind === "advance" && rejectNextTick) { + rejectNextTick = false; + return Promise.reject(new Error("injected tick failure")); + } + return transact(kind, payload); + }; + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); await app.stop(); + assert.equal(errors.length, 1); + assert.match(errors[0].message, /injected tick failure/); + assert.ok(app.repository.records.some(({ kind }) => kind === "advance")); }); -- 2.51.2 From 066b61c156aafdc259ac17b7fff752731c5f9034 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 23 Jul 2026 12:24:25 -0700 Subject: [PATCH 05/14] Add verified AC identity seam --- experiments/intelligent-terrarium/KEEPER.md | 55 +++++-- .../intelligent-terrarium/src/identity.mjs | 102 +++++++++++++ .../intelligent-terrarium/src/mediorgan.mjs | 5 +- .../intelligent-terrarium/src/server.mjs | 61 +++++++- .../test/identity.test.mjs | 140 ++++++++++++++++++ .../disks/terrarium-dev.mjs | 96 ++++++++++++ 6 files changed, 443 insertions(+), 16 deletions(-) create mode 100644 experiments/intelligent-terrarium/src/identity.mjs create mode 100644 experiments/intelligent-terrarium/test/identity.test.mjs create mode 100644 system/public/aesthetic.computer/disks/terrarium-dev.mjs diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md index 984a3a66bc..64f6177d42 100644 --- a/experiments/intelligent-terrarium/KEEPER.md +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -181,11 +181,19 @@ testing; no LAN/public listener or persistent service. ### Stage 3 — real AC identity and hidden piece -Add a development-only AC piece and a token-first handshake modeled on -`fight:auth`. Resolve the current handle server-side, bind actions to the -verified account, rate-limit intentions, and reject replayed/expired sessions. -This stage touches served paths and therefore requires a keeper decision before -any commit, push, or deploy. +Implemented on the isolated branch after an explicit keeper decision. The +hidden `terrarium-dev` piece calls AC's `authorize()` and sends the bearer token +only to the fixed loopback authority. The server follows the existing AC seam: +Auth0 `/userinfo` verifies the token, then `/handle/` resolves the +public handle. Only that server-resolved handle authors a prod. The payload has +no authorship field, unknown spoofed identity fields are discarded, and audit +records contain outcome/status but no token, subject, email, or handle. + +Real identity is opt-in at process start with `TERRARIUM_AC_AUTH=1`; the earlier +development capability remains available. CORS permits the same authority +origin and the two local AC development origins only. The process still +refuses every non-loopback bind. The piece deliberately omits `meta()` so it is +reachable as `terrarium-dev[:port]` without appearing in list/autocomplete. ### Stage 4 — 2 GB reflection experiment @@ -242,10 +250,12 @@ The first thin slice is accepted only when all of these pass: ## Next keeper action -Hold at the Stage 3 boundary for a keeper decision. Real AC identity requires a -hidden development piece and token-verifying gateway changes in served paths. -No model installation, push, deployment, persistent service, or public listener -is authorized. +Hold after Stage 3. A real logged-in-token exercise and interactive piece QA +remain blocked by the current no-browser/no-GUI execution boundary; the tests +use faithful local Auth0/handle response doubles and never read a real token. +Stage 4 also requires a keeper choice of user-space runtime/model before any +download or installation. No model installation, push, deployment, persistent +service, or public listener is authorized. ## 2026-07-23 implementation evidence @@ -309,3 +319,30 @@ Alex's current execution boundary prohibits browser automation and all GUI control, so the remaining visual, WebAudio-device, and physical Xbox-controller checks were not attempted. Pure spatial-audio geometry/deduplication tests stay green; interactive QA remains an explicit acceptance blocker. + +### Stage 3 identity evidence + +Stage 3 adds the dependency-free `identity.mjs` verifier and the hidden +`system/public/aesthetic.computer/disks/terrarium-dev.mjs` client. The CLI wires +the verifier only when `TERRARIUM_AC_AUTH=1`; it reports the auth mode but never +the bearer value or identity claims. No production/session-server code was +changed. + +The complete local suite passes 18/18. Identity cases prove: + +- a valid Auth0 response resolves its subject through the server-side AC handle + endpoint and returns only normalized `@alex`; +- missing, malformed, locally expired JWT-shaped, and Auth0-rejected opaque + tokens fail with 401-class identity errors; +- a request claiming `@mallory` plus spoofed subject/email/token fields is + journaled only as the verified `@alex`, and none of the forbidden values + occur in the journal or structured audit fixture; +- a fixed-clock two-prods-per-second profile accepts exactly two actions and + returns 429 for the third, with exactly two `organ-prod` records; and +- allowed local CORS preflight succeeds, a foreign origin is rejected, the + piece contains no `meta()` export, and its endpoint is fixed to loopback. + +Both the client and identity modules pass Node syntax checks. No live Auth0 +request, real bearer token, GUI/browser action, non-loopback listener, remote +machine access, push, deployment, or live hook was used for this stage. A real +logged-in handle test is therefore still required for first-demo criterion 4. diff --git a/experiments/intelligent-terrarium/src/identity.mjs b/experiments/intelligent-terrarium/src/identity.mjs new file mode 100644 index 0000000000..ed4c54ef57 --- /dev/null +++ b/experiments/intelligent-terrarium/src/identity.mjs @@ -0,0 +1,102 @@ +import { normalizeHandle } from "./core.mjs"; + +const DEFAULT_USERINFO_URL = "https://aesthetic.us.auth0.com/userinfo"; +const DEFAULT_AC_ORIGIN = "https://aesthetic.computer"; + +export class IdentityError extends Error { + constructor(code, statusCode, publicMessage) { + super(publicMessage); + this.name = "IdentityError"; + this.code = code; + this.statusCode = statusCode; + this.publicMessage = publicMessage; + } +} + +function bearerToken(header) { + if (!header) throw new IdentityError("missing_token", 401, "authorization token required"); + const match = /^Bearer ([^\s]+)$/.exec(String(header)); + if (!match || match[1].length > 8192) { + throw new IdentityError("invalid_token", 401, "invalid or expired authorization token"); + } + return match[1]; +} + +function rejectExpiredJwt(token, now) { + const parts = token.split("."); + if (parts.length !== 3) return; + try { + const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")); + if (Number.isFinite(claims.exp) && claims.exp * 1000 <= now()) { + throw new IdentityError("expired_token", 401, "invalid or expired authorization token"); + } + } catch (error) { + if (error instanceof IdentityError) throw error; + // Auth0 userinfo remains authoritative for opaque or unusual token shapes. + } +} + +async function responseJson(response) { + try { + return await response.json(); + } catch { + return null; + } +} + +export function createACIdentityVerifier({ + fetchImpl = globalThis.fetch, + userInfoUrl = DEFAULT_USERINFO_URL, + acOrigin = DEFAULT_AC_ORIGIN, + now = Date.now, +} = {}) { + if (typeof fetchImpl !== "function") throw new TypeError("identity verifier requires fetch"); + + return { + async verifyAuthorization(authorization) { + const token = bearerToken(authorization); + rejectExpiredJwt(token, now); + + let userInfoResponse; + try { + userInfoResponse = await fetchImpl(userInfoUrl, { + headers: { Authorization: `Bearer ${token}` }, + cache: "no-store", + }); + } catch { + throw new IdentityError("identity_unavailable", 503, "identity verification unavailable"); + } + if (userInfoResponse.status === 401 || userInfoResponse.status === 403) { + throw new IdentityError("invalid_or_expired_token", 401, "invalid or expired authorization token"); + } + if (!userInfoResponse.ok) { + throw new IdentityError("identity_unavailable", 503, "identity verification unavailable"); + } + const claims = await responseJson(userInfoResponse); + const sub = typeof claims?.sub === "string" ? claims.sub : ""; + if (!sub || sub.length > 512 || /[\u0000-\u001f]/.test(sub)) { + throw new IdentityError("invalid_identity", 401, "authorization identity is invalid"); + } + + let handleResponse; + try { + const handleUrl = new URL(`/handle/${encodeURIComponent(sub)}`, acOrigin); + handleResponse = await fetchImpl(handleUrl, { cache: "no-store" }); + } catch { + throw new IdentityError("handle_unavailable", 503, "handle resolution unavailable"); + } + if (!handleResponse.ok) { + if (handleResponse.status === 404) { + throw new IdentityError("handle_required", 403, "an Aesthetic Computer handle is required"); + } + throw new IdentityError("handle_unavailable", 503, "handle resolution unavailable"); + } + const handleDocument = await responseJson(handleResponse); + try { + return normalizeHandle(handleDocument?.handle); + } catch { + throw new IdentityError("handle_required", 403, "an Aesthetic Computer handle is required"); + } + }, + }; +} diff --git a/experiments/intelligent-terrarium/src/mediorgan.mjs b/experiments/intelligent-terrarium/src/mediorgan.mjs index aa78604fd1..ab4b7c5c81 100644 --- a/experiments/intelligent-terrarium/src/mediorgan.mjs +++ b/experiments/intelligent-terrarium/src/mediorgan.mjs @@ -11,10 +11,11 @@ function sameSecret(a, b) { } export class Mediorgan { - constructor(repository, { capabilities = {}, maxProdsPerSecond = 12 } = {}) { + constructor(repository, { capabilities = {}, maxProdsPerSecond = 12, now = Date.now } = {}) { this.repository = repository; this.capabilities = new Map(Object.entries(capabilities).map(([token, handle]) => [token, normalizeHandle(handle)])); this.maxProdsPerSecond = maxProdsPerSecond; + this.now = now; this.rate = new Map(); } @@ -54,7 +55,7 @@ export class Mediorgan { } #takeRate(handle) { - const second = Math.floor(Date.now() / 1000); + const second = Math.floor(this.now() / 1000); const previous = this.rate.get(handle); const current = previous?.second === second ? previous : { second, count: 0 }; current.count += 1; diff --git a/experiments/intelligent-terrarium/src/server.mjs b/experiments/intelligent-terrarium/src/server.mjs index 26b1299677..a0b23b3549 100644 --- a/experiments/intelligent-terrarium/src/server.mjs +++ b/experiments/intelligent-terrarium/src/server.mjs @@ -3,6 +3,7 @@ import { createServer } from "node:http"; import { readFile } from "node:fs/promises"; import { dirname, extname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { createACIdentityVerifier } from "./identity.mjs"; import { Mediorgan } from "./mediorgan.mjs"; import { StateRepository } from "./repository.mjs"; import { sleepCommit } from "./sleep.mjs"; @@ -51,9 +52,14 @@ export async function createTerrariumServer({ host = "127.0.0.1", port = 0, capabilities = {}, + identityVerifier = null, + maxProdsPerSecond = 12, + now = Date.now, tickMs = 100, webRoot = defaultWebRoot, onTickError = (error) => console.error("terrarium tick failed:", error.message), + audit = () => {}, + allowedOrigins = ["http://127.0.0.1:8888", "http://localhost:8888"], } = {}) { if (!LOOPBACK.has(host)) throw new Error("terrarium server refuses non-loopback binding"); let repository; @@ -63,8 +69,9 @@ export async function createTerrariumServer({ repository = await StateRepository.create(root, { seed: "intelligent-terrarium-visit-v1", profile: "1gb" }); repository.segmentPath = join(root, "journal", "segments", "visit.ndjson"); } - const mediorgan = new Mediorgan(repository, { capabilities }); + const mediorgan = new Mediorgan(repository, { capabilities, maxProdsPerSecond, now }); const streams = new Set(); + const originAllowlist = new Set(allowedOrigins); let writeTail = Promise.resolve(); function exclusive(operation) { @@ -83,19 +90,55 @@ export async function createTerrariumServer({ } } + function safeAudit(value) { + try { audit(value); } catch { /* Audit sinks cannot affect authority. */ } + } + async function authenticate(request, response) { - const handle = mediorgan.authenticate(request.headers.authorization); + const authorization = request.headers.authorization; + let handle = mediorgan.authenticate(authorization); + let method = "development-capability"; + if (!handle && identityVerifier) { + method = "ac-token"; + try { + handle = await identityVerifier.verifyAuthorization(authorization); + } catch (error) { + safeAudit({ kind: "authentication", method, outcome: "rejected", status: error.statusCode || 401 }); + throw error; + } + } if (!handle) { + safeAudit({ kind: "authentication", method, outcome: "rejected", status: 401 }); json(response, 401, { error: "valid development capability required" }); return null; } await exclusive(() => mediorgan.ensurePresent(handle)); + safeAudit({ kind: "authentication", method, outcome: "accepted", status: 200 }); return handle; } const server = createServer(async (request, response) => { try { const url = new URL(request.url, `http://${request.headers.host || "127.0.0.1"}`); + const origin = request.headers.origin; + if (origin) { + if (origin !== url.origin && !originAllowlist.has(origin)) { + json(response, 403, { error: "origin is not allowed" }); + return; + } + response.setHeader("access-control-allow-origin", origin); + response.setHeader("vary", "origin"); + } + if (request.method === "OPTIONS" && url.pathname.startsWith("/api/")) { + response.writeHead(204, { + "access-control-allow-methods": "GET, POST, OPTIONS", + "access-control-allow-headers": "authorization, content-type", + "access-control-max-age": "600", + "content-length": "0", + }); + response.end(); + return; + } if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/client.mjs" || url.pathname === "/spatial-audio.mjs")) { const filename = url.pathname === "/" ? "index.html" : url.pathname.slice(1); const content = await readFile(join(webRoot, filename)); @@ -143,7 +186,8 @@ export async function createTerrariumServer({ } json(response, 404, { error: "not found" }); } catch (error) { - json(response, /rate limit|too large/.test(error.message) ? 429 : 400, { error: error.message }); + const status = error.statusCode || (/rate limit|too large/.test(error.message) ? 429 : 400); + json(response, status, { error: error.publicMessage || error.message }); } }); @@ -185,8 +229,15 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { const port = Number(option("--port", "8787")); const root = resolve(option("--root", "./terrarium-state")); const capabilities = JSON.parse(process.env.TERRARIUM_DEV_CAPS || "{}"); - const app = await createTerrariumServer({ root, host, port, capabilities }); - console.log(JSON.stringify({ listening: `http://${app.address.address}:${app.address.port}`, binding: app.address, root })); + const acAuth = process.env.TERRARIUM_AC_AUTH === "1"; + const identityVerifier = acAuth ? createACIdentityVerifier() : null; + const app = await createTerrariumServer({ root, host, port, capabilities, identityVerifier }); + console.log(JSON.stringify({ + listening: `http://${app.address.address}:${app.address.port}`, + binding: app.address, + auth: acAuth ? "ac+development-capability" : "development-capability", + root, + })); const shutdown = async () => { await app.stop({ sleep: true }); process.exit(0); diff --git a/experiments/intelligent-terrarium/test/identity.test.mjs b/experiments/intelligent-terrarium/test/identity.test.mjs new file mode 100644 index 0000000000..915dad88ef --- /dev/null +++ b/experiments/intelligent-terrarium/test/identity.test.mjs @@ -0,0 +1,140 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import test from "node:test"; +import { createACIdentityVerifier, IdentityError } from "../src/identity.mjs"; +import { createTerrariumServer } from "../src/server.mjs"; + +const jsonResponse = (value, status = 200) => new Response(JSON.stringify(value), { + status, + headers: { "content-type": "application/json" }, +}); + +test("AC verifier resolves only the server-side handle from Auth0 identity", async () => { + const calls = []; + const verifier = createACIdentityVerifier({ + fetchImpl: async (url, options = {}) => { + calls.push({ url: String(url), authorization: options.headers?.Authorization || null }); + if (String(url).includes("/userinfo")) { + return jsonResponse({ sub: "auth0|alex private", email: "private@example.test", name: "Private Name" }); + } + return jsonResponse({ handle: "Alex" }); + }, + }); + assert.equal(await verifier.verifyAuthorization("Bearer real-ac-token"), "@alex"); + assert.deepEqual(calls, [ + { url: "https://aesthetic.us.auth0.com/userinfo", authorization: "Bearer real-ac-token" }, + { url: "https://aesthetic.computer/handle/auth0%7Calex%20private", authorization: null }, + ]); +}); + +test("AC verifier rejects missing, malformed, expired, and upstream-rejected tokens", async () => { + let fetches = 0; + const verifier = createACIdentityVerifier({ + now: () => 2_000_000, + fetchImpl: async () => { + fetches += 1; + return jsonResponse({}, 401); + }, + }); + await assert.rejects(verifier.verifyAuthorization(), (error) => error instanceof IdentityError && error.code === "missing_token"); + await assert.rejects(verifier.verifyAuthorization("Token nope"), (error) => error.code === "invalid_token"); + const expired = [ + Buffer.from("{}").toString("base64url"), + Buffer.from(JSON.stringify({ exp: 1 })).toString("base64url"), + "signature", + ].join("."); + await assert.rejects(verifier.verifyAuthorization(`Bearer ${expired}`), (error) => error.code === "expired_token"); + await assert.rejects(verifier.verifyAuthorization("Bearer upstream-expired"), (error) => error.code === "invalid_or_expired_token"); + assert.equal(fetches, 1); +}); + +test("verified identity authors prods and spoofed identity fields never persist", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-ac-identity-")); + const secret = "auth0-secret-token-never-persist"; + const privateSub = "auth0|private-sub-never-persist"; + const privateEmail = "private-email@example.test"; + const audit = []; + const identityVerifier = createACIdentityVerifier({ + fetchImpl: async (url, options = {}) => { + if (String(url).includes("/userinfo")) { + if (options.headers?.Authorization !== `Bearer ${secret}`) return jsonResponse({}, 401); + return jsonResponse({ sub: privateSub, email: privateEmail }); + } + return jsonResponse({ handle: "alex" }); + }, + }); + const app = await createTerrariumServer({ root, identityVerifier, tickMs: 0, audit: (entry) => audit.push(entry) }); + const base = `http://${app.address.address}:${app.address.port}`; + assert.equal((await fetch(`${base}/api/state`)).status, 401); + assert.equal((await fetch(`${base}/api/state`, { headers: { Authorization: "Bearer rejected" } })).status, 401); + + const preflight = await fetch(`${base}/api/prod`, { + method: "OPTIONS", + headers: { Origin: "http://localhost:8888" }, + }); + assert.equal(preflight.status, 204); + assert.equal(preflight.headers.get("access-control-allow-origin"), "http://localhost:8888"); + assert.equal((await fetch(`${base}/api/state`, { headers: { Origin: "https://hostile.example" } })).status, 403); + + const response = await fetch(`${base}/api/prod`, { + method: "POST", + headers: { Authorization: `Bearer ${secret}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + handle: "@mallory", + sub: "spoofed-private-sub", + email: "spoofed@example.test", + token: "payload-token-never-persist", + target: "voice", + modality: "text", + stimulus: "hello mediorgan", + }), + }); + assert.equal(response.status, 202); + await app.stop(); + + const journal = await readFile(app.repository.segmentPath, "utf8"); + assert.match(journal, /"handle":"@alex"/); + for (const forbidden of [secret, privateSub, privateEmail, "@mallory", "spoofed-private-sub", "spoofed@example.test", "payload-token-never-persist"]) { + assert.doesNotMatch(journal, new RegExp(forbidden.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + assert.doesNotMatch(JSON.stringify(audit), new RegExp(forbidden.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + } + assert.ok(audit.some(({ outcome }) => outcome === "accepted")); + assert.ok(audit.some(({ outcome }) => outcome === "rejected")); +}); + +test("verified prods obey a deterministic per-handle rate bound", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-ac-rate-")); + const identityVerifier = { verifyAuthorization: async () => "@alex" }; + const app = await createTerrariumServer({ + root, + identityVerifier, + maxProdsPerSecond: 2, + now: () => 1_000_000, + tickMs: 0, + }); + const url = `http://${app.address.address}:${app.address.port}/api/prod`; + const request = () => fetch(url, { + method: "POST", + headers: { Authorization: "Bearer test", "Content-Type": "application/json" }, + body: JSON.stringify({ target: "sensory", modality: "gesture", stimulus: "tap" }), + }); + assert.equal((await request()).status, 202); + assert.equal((await request()).status, 202); + assert.equal((await request()).status, 429); + await app.stop(); + assert.equal(app.repository.records.filter(({ kind }) => kind === "organ-prod").length, 2); +}); + +test("terrarium-dev is a hidden authorize-only loopback client seam", async () => { + const source = await readFile(resolve("../../system/public/aesthetic.computer/disks/terrarium-dev.mjs"), "utf8"); + const serverSource = await readFile(resolve("src/server.mjs"), "utf8"); + assert.match(source, /await authorize\(\)/); + assert.match(source, /Authorization: `Bearer \$\{token\}`/); + assert.match(source, /http:\/\/127\.0\.0\.1/); + assert.doesNotMatch(source, /function meta\s*\(/); + assert.doesNotMatch(source, /JSON\.stringify\(\{[^}]*handle/s); + assert.match(serverSource, /TERRARIUM_AC_AUTH === "1"/); + assert.match(serverSource, /createACIdentityVerifier\(\)/); +}); diff --git a/system/public/aesthetic.computer/disks/terrarium-dev.mjs b/system/public/aesthetic.computer/disks/terrarium-dev.mjs new file mode 100644 index 0000000000..16465a3965 --- /dev/null +++ b/system/public/aesthetic.computer/disks/terrarium-dev.mjs @@ -0,0 +1,96 @@ +// Terrarium Dev, 26.07.23.12.10 +// Hidden development seam for authenticated Mediorgan visits on loopback. + +const ORGANS = ["sensory", "spatial", "drive", "memory", "action", "voice"]; + +let endpoint = "http://127.0.0.1:8787"; +let token = null; +let verifiedHandle = null; +let status = "connecting"; +let lastResponse = ""; +let hudRef = null; +let needsPaintRef = null; + +function endpointFor(colon = []) { + const candidate = Number(colon[0]); + const port = Number.isInteger(candidate) && candidate >= 1024 && candidate <= 65535 ? candidate : 8787; + return `http://127.0.0.1:${port}`; +} + +async function request(path, options = {}) { + const headers = { ...(options.headers || {}), Authorization: `Bearer ${token}` }; + const response = await fetch(`${endpoint}${path}`, { ...options, headers, cache: "no-store" }); + const body = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(body.error || `request failed (${response.status})`); + return body; +} + +async function boot({ authorize, colon, hud, needsPaint }) { + endpoint = endpointFor(colon); + hudRef = hud; + needsPaintRef = needsPaint; + hudRef.label("terrarium dev", "yellow"); + try { + token = await authorize(); + if (!token) throw new Error("log in first"); + const result = await request("/api/state"); + verifiedHandle = result.handle; + status = "connected"; + lastResponse = `${result.state.entities.length} lives · tick ${result.state.tick}`; + hudRef.label("terrarium dev", "lime"); + } catch (error) { + status = "unavailable"; + lastResponse = error.message; + hudRef.label("terrarium dev", "orange"); + } + needsPaintRef?.(); +} + +async function prod(target, modality = "gesture") { + if (!token || status !== "connected") return; + status = `prodding ${target}`; + needsPaintRef?.(); + try { + const result = await request("/api/prod", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ target, modality, stimulus: "terrarium-dev-piece" }), + }); + lastResponse = `${target} answered · event ${result.eventSeq}`; + status = "connected"; + } catch (error) { + status = "unavailable"; + lastResponse = error.message; + } + needsPaintRef?.(); +} + +function paint({ ink, screen }) { + ink(9, 12, 20).box(0, 0, screen.width, screen.height); + ink(235, 120, 190).write("MEDIORGAN", { center: "x", y: 22 }, screen); + ink(190).write(verifiedHandle || "unverified visitor", { center: "x", y: 43 }, screen); + ink(status === "connected" ? 120 : 230, status === "connected" ? 230 : 170, 150) + .write(status, { center: "x", y: 61 }, screen); + ink(150).write("keys 1–6 prod an organ", { center: "x", y: 87 }, screen); + ORGANS.forEach((organ, index) => { + ink(110 + index * 18, 180, 220).write(`${index + 1} ${organ}`, { center: "x", y: 108 + index * 15 }, screen); + }); + if (lastResponse) ink(125).write(lastResponse.slice(0, 54), { center: "x", y: screen.height - 22 }, screen); +} + +function act({ event: e }) { + if (!e.is("keyboard:down")) return; + const index = Number(e.key) - 1; + if (index >= 0 && index < ORGANS.length) void prod(ORGANS[index]); +} + +function leave() { + token = null; + verifiedHandle = null; + status = "left"; + lastResponse = ""; +} + +// Deliberately no meta() export: this development piece stays out of list and +// autocomplete while remaining directly reachable as `terrarium-dev`. +export { boot, paint, act, leave }; -- 2.51.2 From 4d61cdfabdeb054df0d9d84620ff188f0fc64f19 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 23 Jul 2026 14:23:10 -0700 Subject: [PATCH 06/14] Add bounded reflection organ and spatial score --- experiments/intelligent-terrarium/KEEPER.md | 104 ++++++++++-- .../intelligent-terrarium/package.json | 1 + .../provenance/stage4.json | 36 ++++ .../score-assets/join-preview.svg | 1 + experiments/intelligent-terrarium/score.html | 150 +++++++++++++++++ experiments/intelligent-terrarium/src/cli.mjs | 53 +++++- .../intelligent-terrarium/src/core.mjs | 38 +++++ .../intelligent-terrarium/src/llama-cli.mjs | 124 ++++++++++++++ .../src/reflection-policy.mjs | 32 ++++ .../intelligent-terrarium/src/reflection.mjs | 157 ++++++++++++++++++ .../intelligent-terrarium/src/repository.mjs | 20 ++- .../test/identity.test.mjs | 3 +- .../test/mediorgan.test.mjs | 63 +++++++ .../test/reflection.test.mjs | 137 +++++++++++++++ .../test/repository.test.mjs | 13 ++ .../intelligent-terrarium/test/score.test.mjs | 16 ++ 16 files changed, 922 insertions(+), 26 deletions(-) create mode 100644 experiments/intelligent-terrarium/provenance/stage4.json create mode 100644 experiments/intelligent-terrarium/score-assets/join-preview.svg create mode 100644 experiments/intelligent-terrarium/score.html create mode 100644 experiments/intelligent-terrarium/src/llama-cli.mjs create mode 100644 experiments/intelligent-terrarium/src/reflection-policy.mjs create mode 100644 experiments/intelligent-terrarium/src/reflection.mjs create mode 100644 experiments/intelligent-terrarium/test/reflection.test.mjs create mode 100644 experiments/intelligent-terrarium/test/score.test.mjs diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md index 64f6177d42..ba46d52995 100644 --- a/experiments/intelligent-terrarium/KEEPER.md +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -143,15 +143,14 @@ to evade the RAM cap. | Budget | Hard controls | Active mind | Expected steady RSS | |---|---|---|---| | **1 GB** | `MemoryHigh=900M`, `MemoryMax=1024M`; Node old-space 256 MiB; bounded 4 MiB journal buffer; stop cleanly on allocation pressure | Deterministic recurrent drives, salience/novelty scoring, bounded episodic recall, spatial relation graph, and generative behavior policy. No resident language model. | 300–600 MiB; acceptance requires `<900 MiB` peak over the demo | -| **2 GB** | `MemoryHigh=1800M`, `MemoryMax=2048M`; same core bounds; inference has explicit model/context caps and one request at a time | 1 GB mind plus an optional approved user-space llama.cpp runner using a roughly 0.5–1B Q4 model, at most 2k context, quantized/bounded KV cache, and no unbounded embedding index | 1.2–1.8 GiB; acceptance requires `<1.8 GiB` peak | +| **2 GB** | `MemoryHigh=1800M`, `MemoryMax=2048M`; same core bounds; inference has explicit model/context caps and one request at a time | 1 GB mind plus the approved CPU-only Qwen3 0.6B Q8_0 reflection organ, at most 2k context, quantized/bounded KV cache, and no unbounded embedding index | Measured smaps RSS is about 807 MiB; acceptance requires `<1.8 GiB` peak | The 1 GB profile is the product floor and remains intelligent without an LLM: it perceives events, maintains drives and relationships, recalls salient episodes, chooses behaviors, composes semantic sounds, and learns bounded weights that become autobiography. The 2 GB model is a slow reflection/wording -organ, never the simulation authority. Given current GPU saturation, it should -run CPU-only at low priority first. Acquiring/building llama.cpp or model weights -is an explicit later decision because no suitable runtime is installed. +organ, never the simulation authority. Its approved runner is CPU-only and low +priority, so the existing GPU miner remains independent. Measure `memory.current`, `memory.peak`, `/proc//smaps_rollup`, event-loop lag, journal backlog, and inference queue depth. Reject startup when a requested @@ -197,11 +196,12 @@ reachable as `terrarium-dev[:port]` without appearing in list/autocomplete. ### Stage 4 — 2 GB reflection experiment -After an explicit tooling/model decision, benchmark one small quantized model -CPU-only under the 2 GB cgroup. Feed it a bounded selected-memory digest, accept -only schema-validated proposals, and journal both proposal and deterministic -authority decision. Fall back to the 1 GB policy on timeout, malformed output, -or pressure. +Implemented after explicit approval. A pinned user-space llama.cpp and official +Qwen3 0.6B Q8_0 run CPU-only under the 2 GB cgroup. The organ receives a bounded, +secret-free selected-memory digest and may submit one strict proposal. The +deterministic authority independently accepts or rejects it and journals the +decision. Timeout, malformed output, disabled inference, or pressure always +falls back to the 1 GB policy. ### Stage 5 — visitable service / Xbox gate @@ -240,8 +240,6 @@ The first thin slice is accepted only when all of these pass: ## Keeper decisions still required -- Whether/when to install or build a user-space inference runtime and acquire a - specific quantized model. No system package installation is assumed. - The future authenticated gateway and who may visit/interact versus observe. - Any coordination with `matador-miner`; the default is strict coexistence and no GPU use. @@ -250,12 +248,12 @@ The first thin slice is accepted only when all of these pass: ## Next keeper action -Hold after Stage 3. A real logged-in-token exercise and interactive piece QA -remain blocked by the current no-browser/no-GUI execution boundary; the tests -use faithful local Auth0/handle response doubles and never read a real token. -Stage 4 also requires a keeper choice of user-space runtime/model before any -download or installation. No model installation, push, deployment, persistent -service, or public listener is authorized. +Hold after Stage 4. The local fullscreen score may remain as a display-only +rehearsal, but its QR is deliberately marked as a non-live Stage 5 preview. +A real logged-in-token exercise, WebAudio-device check, physical Xbox-controller +check, and any reachable authenticated gateway still require keeper decisions. +No push, deployment, persistent authority service, or public listener is +authorized. ## 2026-07-23 implementation evidence @@ -346,3 +344,75 @@ Both the client and identity modules pass Node syntax checks. No live Auth0 request, real bearer token, GUI/browser action, non-loopback listener, remote machine access, push, deployment, or live hook was used for this stage. A real logged-in handle test is therefore still required for first-demo criterion 4. + +### Stage 4 reflection and graphic-score evidence + +Keeper approval arrived through the private `alex` Loopboy inbox for a strictly +user-space CPU experiment on `jastow`. The final source and display remain in +the isolated worktree/runtime; the durable autobiography remains the separate +state repository. Nothing was pushed or deployed. + +- llama.cpp is pinned to official `ggml-org/llama.cpp` commit + `c0bc8591e8815c63cb01dd3f051a8b0df02501c9`. It was compiled with the + existing CMake 3.31.11 and GCC 15.2.1: CUDA, Vulkan, SYCL, OpenCL, BLAS, + curl, UI, tests, and examples are off. This revision couples the CLI to the + server source target, so `LLAMA_BUILD_SERVER=ON` was needed to produce the + CLI; no server was launched and no socket was opened. An initial configure + touched only the ignored user cache before UI was explicitly disabled; no + system or npm package was installed. +- The official Apache-2.0 `Qwen/Qwen3-0.6B-GGUF` Q8_0 file is 639,446,688 + bytes with SHA-256 + `9465e63a22add5354d9bb4b99e90117043c7124007664907259bd16d043bb031`. + Exact URL, hashes, build flags, and cache paths are recorded in + `provenance/stage4.json`; caches are excluded from autobiographical Git. +- The reflection request is capped at 2,048 context tokens, 128 output tokens + (96 in the measured run), 6,000 prompt characters, 30 seconds, four CPU + threads, and one request at a time. The selected-memory digest contains only + hashes, tick/count aggregates, drives, weights, and episode-kind counts. + Tokens, handles, stimuli, email, paths, and raw memories do not enter the + prompt or journal. +- The pinned llama.cpp numeric-range grammar path threw in its sampler, so the + model is not trusted to enforce its own grammar. The runner removes its exact + echoed prompt, and the application accepts only the strict four-field schema. + The authority re-evaluates the policy during both append and replay; a model + cannot authorize `broadcast`, add a field, exceed `±0.25`, or forge a prior + decision. +- The complete Neo suite passes 26/26. The complete Fedora suite passes 25/25 + under `MemoryMax=2 GiB`, `MemoryHigh=1800 MiB`, `MemorySwapMax=0`, + `CPUQuota=200%`, and `Nice=15`; its only environment-specific input is the + path to the hidden AC piece. Coverage includes malformed/timeout/disabled + fallback, deterministic rejection, prompt-echo separation, one-request + serialization, secret exclusion, and journal ordering by sequence across + segment filenames. +- The strict 1 GB policy fallback ran under `MemoryMax=1 GiB`, + `MemoryHigh=900 MiB`, zero swap, and low priority. It journaled `disabled` + fallback deterministically; cgroup peak was 31,932,416 bytes and Node RSS was + 78,917,632 bytes. The 1 GB floor still never loads the model. +- The final 2 GB coexistence run completed in 6.71 seconds while + `matador-miner` remained active at 100% GPU use. Qwen generated at 13.3 + tokens/second after an 88.9 tokens/second prompt pass. `/proc` smaps peak RSS + was 825,940 KiB (about 807 MiB), below both policy thresholds; cgroup peak was + 227,233,792 bytes because model file pages were already warm and charged + elsewhere. This is not represented as a cold-cache peak. Swap stayed zero. +- The deterministic authority accepted + `{action:"attune", target:"sensory", intensity:0.25}` at final sequence 912. + Sleep created local state commit + `7b801ff08c998574d23f353934e4204752c61bf4`; fresh replay produced state + `1e0174be53f69e8497683676e7e6cc9be4c589765fcb9eb5bda22c79b6c94764` + and head + `8b660c4aebad3236fa1fe8609d2504e0dedc68f439032aec7fed4270898af12e`. + Two earlier retries also completed and slept cleanly as sequences 910–911. +- A scan of journal, checkpoints, autobiography, manifest, and visitor facts + found no bearer/token, email-like identifier, model/cache path, or GGUF name. + The listening-socket hash was identical before and after inference. The miner + had independently exited during the earlier build window and later returned; + no keeper command signalled, stopped, restarted, or reconfigured it. The + final run proves live coexistence after its return. +- `score.html` is a self-contained 1920×1080 spatial graphic measure: the six + organs are colored staves, outside prods ripple through the Mediorgan + membrane, reflection breathes inside the body, and quiet proof marks show the + journal, replay, resident envelope, and miner boundary. Its QR encodes the + intended `https://aesthetic.computer/terrarium-dev` route but visibly says the + client gate is only a preview pending Stage 5. It opens from `file://` in a + fullscreen Chromium kiosk on the existing `jastow` GNOME session; it opens no + port and depends on no remote script. diff --git a/experiments/intelligent-terrarium/package.json b/experiments/intelligent-terrarium/package.json index af01d7e447..3ed4ca5d36 100644 --- a/experiments/intelligent-terrarium/package.json +++ b/experiments/intelligent-terrarium/package.json @@ -5,6 +5,7 @@ "scripts": { "test": "node --test test/*.test.mjs", "demo": "node src/cli.mjs demo", + "reflect": "node src/cli.mjs reflect", "visit": "node src/server.mjs" } } diff --git a/experiments/intelligent-terrarium/provenance/stage4.json b/experiments/intelligent-terrarium/provenance/stage4.json new file mode 100644 index 0000000000..b889cbd8f5 --- /dev/null +++ b/experiments/intelligent-terrarium/provenance/stage4.json @@ -0,0 +1,36 @@ +{ + "schema": 1, + "recordedAt": "2026-07-23", + "host": "jastow", + "llamaCpp": { + "repository": "https://github.com/ggml-org/llama.cpp.git", + "commit": "c0bc8591e8815c63cb01dd3f051a8b0df02501c9", + "cache": "/home/me/.cache/intelligent-terrarium/tools/llama.cpp-c0bc8591e8815c63cb01dd3f051a8b0df02501c9", + "cmake": "3.31.11", + "gcc": "15.2.1", + "buildFlags": { + "CMAKE_BUILD_TYPE": "Release", + "GGML_CUDA": false, + "GGML_VULKAN": false, + "GGML_SYCL": false, + "GGML_OPENCL": false, + "GGML_BLAS": false, + "GGML_NATIVE": true, + "LLAMA_CURL": false, + "LLAMA_BUILD_SERVER": true, + "LLAMA_BUILD_UI": false, + "LLAMA_BUILD_TESTS": false, + "LLAMA_BUILD_EXAMPLES": false, + "LLAMA_BUILD_TOOLS": true + } + }, + "model": { + "repository": "Qwen/Qwen3-0.6B-GGUF", + "file": "Qwen3-0.6B-Q8_0.gguf", + "url": "https://huggingface.co/Qwen/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q8_0.gguf", + "license": "Apache-2.0", + "bytes": 639446688, + "sha256": "9465e63a22add5354d9bb4b99e90117043c7124007664907259bd16d043bb031", + "cache": "/home/me/.cache/intelligent-terrarium/models/Qwen3-0.6B-Q8_0.gguf" + } +} diff --git a/experiments/intelligent-terrarium/score-assets/join-preview.svg b/experiments/intelligent-terrarium/score-assets/join-preview.svg new file mode 100644 index 0000000000..7acd43b81a --- /dev/null +++ b/experiments/intelligent-terrarium/score-assets/join-preview.svg @@ -0,0 +1 @@ + diff --git a/experiments/intelligent-terrarium/score.html b/experiments/intelligent-terrarium/score.html new file mode 100644 index 0000000000..7387868c34 --- /dev/null +++ b/experiments/intelligent-terrarium/score.html @@ -0,0 +1,150 @@ + + + + + + Intelligent Terrarium — Mediorgan Score + + + + +
+
JAS-NZXT · LOCAL REHEARSAL · 23 JULY 2026
+

Intelligent Terrarium
Mediorgan score

+
outside prods cross the membrane as stimulus · organs listen and answer · one authority writes the body
+
+ +
+
+ SENSORYSPATIALDRIVE + MEMORYACTIONVOICE +
+
QWEN3 0.6B Q8_0 · LLAMA.CPP CPU · CTX 2048 · OUTPUT ≤ 128
+
+ + + diff --git a/experiments/intelligent-terrarium/src/cli.mjs b/experiments/intelligent-terrarium/src/cli.mjs index 9cfe438d96..83ccb44b45 100644 --- a/experiments/intelligent-terrarium/src/cli.mjs +++ b/experiments/intelligent-terrarium/src/cli.mjs @@ -3,6 +3,8 @@ import { mkdir, stat } from "node:fs/promises"; import { resolve } from "node:path"; import { memoryCgroup, requireMemoryMax } from "./cgroup.mjs"; import { outputHash, Terrarium } from "./core.mjs"; +import { createLlamaCliInfer } from "./llama-cli.mjs"; +import { ReflectionOrgan } from "./reflection.mjs"; import { StateRepository, verifyRepository } from "./repository.mjs"; import { sleepCommit } from "./sleep.mjs"; @@ -65,12 +67,61 @@ async function demo() { }, null, 2)); } +async function reflect() { + const root = resolve(option("--root", "./terrarium-state")); + const requiredMax = Number(option("--require-memory-max", "0")); + if (requiredMax) await requireMemoryMax(requiredMax); + const repository = await StateRepository.open(root, { segmentId: "reflection" }); + const before = repository.stateHash(); + const binary = option("--llama-cli", process.env.TERRARIUM_LLAMA_CLI); + const model = option("--model", process.env.TERRARIUM_REFLECTION_MODEL); + const timeoutMs = Number(option("--timeout-ms", "30000")); + const infer = binary && model ? createLlamaCliInfer({ + binary: resolve(binary), + model: resolve(model), + threads: Number(option("--threads", "4")), + }) : null; + const reflection = new ReflectionOrgan(repository, { + infer, + engine: option("--engine", infer ? "qwen3-0.6b-q8_0" : "1gb-policy"), + contextTokens: Number(option("--context", "2048")), + maxOutputTokens: Number(option("--max-output", "96")), + timeoutMs, + }); + const result = await reflection.reflect(); + const sleep = await sleepCommit(repository); + const verification = await verifyRepository(root); + const memory = await memoryCgroup(); + console.log(JSON.stringify({ + ok: verification.stateHash === repository.stateHash(), + root, + beforeStateHash: before, + stateHash: verification.stateHash, + lastSeq: verification.lastSeq, + headRecordHash: verification.headRecordHash, + decision: result.decision, + reason: result.reason, + promptChars: result.promptChars, + contextTokens: result.contextTokens, + maxOutputTokens: result.maxOutputTokens, + timeoutMs: result.timeoutMs, + metrics: result.metrics, + commit: sleep.commit, + sleepStatus: sleep.status, + rss: process.memoryUsage().rss, + memory, + listener: "none", + }, null, 2)); +} + const command = process.argv[2]; if (command === "demo") { await demo(); +} else if (command === "reflect") { + await reflect(); } else if (command === "verify") { console.log(JSON.stringify(await verifyRepository(resolve(option("--root", "./terrarium-state"))), null, 2)); } else { - console.error("usage: cli.mjs demo|verify [--root PATH] [--ticks N] [--require-memory-max BYTES]"); + console.error("usage: cli.mjs demo|reflect|verify [--root PATH] [--require-memory-max BYTES]"); process.exitCode = 2; } diff --git a/experiments/intelligent-terrarium/src/core.mjs b/experiments/intelligent-terrarium/src/core.mjs index bf299be043..90719510f2 100644 --- a/experiments/intelligent-terrarium/src/core.mjs +++ b/experiments/intelligent-terrarium/src/core.mjs @@ -1,5 +1,6 @@ import { canonical, clone, hash } from "./canonical.mjs"; import { Prng } from "./prng.mjs"; +import { decideReflection } from "./reflection-policy.mjs"; export const LIMITS = Object.freeze({ entities: 12, @@ -118,6 +119,9 @@ export class Terrarium { case "organ-prod": outputs = this.#organProd(payload); break; + case "reflection-decision": + this.#reflectionDecision(payload); + break; case "visitor-leave": this.#visitorLeave(payload); break; @@ -218,6 +222,40 @@ export class Terrarium { return []; } + #reflectionDecision(payload) { + if (payload.schema !== 1) throw new TypeError("reflection event schema must be 1"); + const expectedFields = ["decision", "engine", "outputDigest", "reason", "requestId", "schema", payload.proposal ? "proposal" : "failure"].sort(); + if (Object.keys(payload).sort().join(",") !== expectedFields.join(",")) { + throw new TypeError("reflection event fields do not match schema"); + } + if (!/^[a-f0-9]{24}$/.test(String(payload.requestId || ""))) throw new TypeError("invalid reflection request id"); + if (!/^[a-f0-9]{64}$/.test(String(payload.outputDigest || ""))) throw new TypeError("invalid reflection output digest"); + if (!/^[a-zA-Z0-9._:/-]{1,160}$/.test(String(payload.engine || ""))) throw new TypeError("invalid reflection engine"); + const evaluation = decideReflection({ proposal: payload.proposal, failure: payload.failure }); + if (payload.decision !== evaluation.decision || payload.reason !== evaluation.reason) { + throw new Error("reflection decision does not match deterministic policy"); + } + pushEpisode(this.state, { + tick: this.state.tick, + kind: "reflected", + requestId: payload.requestId, + outcome: evaluation.decision, + reason: evaluation.reason, + organ: evaluation.proposal?.target || null, + }); + if (evaluation.decision !== "accepted") return; + const { target, intensity } = evaluation.proposal; + if (target === "sensory" || target === "drive") { + this.state.mind.drives.curiosity = round(clamp(this.state.mind.drives.curiosity + intensity, 0, 1)); + } else if (target === "memory") { + this.state.mind.drives.rest = round(clamp(this.state.mind.drives.rest - intensity, 0, 1)); + } else if (target === "voice") { + this.state.mind.drives.social = round(clamp(this.state.mind.drives.social + intensity, 0, 1)); + } else { + this.state.mind.weights.approach = round(clamp(this.state.mind.weights.approach + intensity, 0, 1)); + } + } + #advance(value) { const ticks = Number(value); if (!Number.isInteger(ticks) || ticks < 1 || ticks > LIMITS.advanceTicks) { diff --git a/experiments/intelligent-terrarium/src/llama-cli.mjs b/experiments/intelligent-terrarium/src/llama-cli.mjs new file mode 100644 index 0000000000..2aee16cbdd --- /dev/null +++ b/experiments/intelligent-terrarium/src/llama-cli.mjs @@ -0,0 +1,124 @@ +import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; + +function metricsFrom(output, latencyMs, smapsPeakRssKb) { + const evalMatch = /eval time\s*=\s*[\d.]+ ms\s*\/\s*(\d+) tokens\s*\(\s*([\d.]+) tokens per second\s*\)/i.exec(output); + const promptMatch = /prompt eval time\s*=\s*[\d.]+ ms\s*\/\s*(\d+) tokens\s*\(\s*([\d.]+) tokens per second\s*\)/i.exec(output); + const summaryMatch = /\[\s*Prompt:\s*([\d.]+) t\/s\s*\|\s*Generation:\s*([\d.]+) t\/s\s*\]/i.exec(output); + return { + latencyMs: Math.round(latencyMs * 100) / 100, + outputTokens: evalMatch ? Number(evalMatch[1]) : null, + outputTokensPerSecond: evalMatch ? Number(evalMatch[2]) : (summaryMatch ? Number(summaryMatch[2]) : null), + promptTokens: promptMatch ? Number(promptMatch[1]) : null, + promptTokensPerSecond: promptMatch ? Number(promptMatch[2]) : (summaryMatch ? Number(summaryMatch[1]) : null), + smapsPeakRssKb, + }; +} + +async function rssKb(pid) { + try { + const text = await readFile(`/proc/${pid}/smaps_rollup`, "utf8"); + return Number(/^Rss:\s+(\d+) kB$/m.exec(text)?.[1] || 0); + } catch { + return 0; + } +} + +export function createLlamaCliInfer({ binary, model, threads = 4, nice = "/usr/bin/nice" } = {}) { + if (!binary || !model) throw new TypeError("llama-cli binary and model are required"); + return ({ prompt, contextTokens, maxOutputTokens, timeoutMs, signal }) => new Promise((resolve, reject) => { + const subcommand = /(?:^|\/)llama$/.test(binary) ? ["cli"] : []; + const args = [ + "-n", "15", binary, + ...subcommand, + "--model", model, + "--prompt", prompt, + "--ctx-size", String(contextTokens), + "--n-predict", String(maxOutputTokens), + "--threads", String(threads), + "--batch-size", "256", + "--ubatch-size", "256", + "--n-gpu-layers", "0", + "--device", "none", + "--fit", "off", + "--cache-type-k", "q8_0", + "--cache-type-v", "q8_0", + "--seed", "23", + "--temp", "0.2", + "--top-k", "20", + "--top-p", "0.8", + "--perf", + "--no-warmup", + "--no-context-shift", + "--conversation", + "--single-turn", + "--reasoning", "off", + "--reasoning-budget", "0", + "--no-display-prompt", + "--simple-io", + ]; + const started = performance.now(); + const child = spawn(nice, args, { + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, CUDA_VISIBLE_DEVICES: "" }, + }); + let stdout = ""; + let stderr = ""; + let smapsPeakRssKb = 0; + let timedOut = false; + const sample = setInterval(() => { + void rssKb(child.pid).then((value) => { smapsPeakRssKb = Math.max(smapsPeakRssKb, value); }); + }, 25); + sample.unref(); + const timer = setTimeout(() => { + timedOut = true; + child.kill("SIGKILL"); + }, timeoutMs); + timer.unref(); + const abort = () => { + timedOut = true; + child.kill("SIGKILL"); + }; + signal?.addEventListener("abort", abort, { once: true }); + child.stdout.on("data", (chunk) => { + stdout += chunk; + if (stdout.length > 65_536) child.kill("SIGKILL"); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk; + if (stderr.length > 262_144) child.kill("SIGKILL"); + }); + child.once("error", reject); + child.once("close", async (code, killedBy) => { + clearTimeout(timer); + clearInterval(sample); + signal?.removeEventListener("abort", abort); + smapsPeakRssKb = Math.max(smapsPeakRssKb, await rssKb(child.pid)); + if (timedOut) { + const error = new Error("llama-cli timed out"); + error.code = "ETIMEDOUT"; + reject(error); + } else if (code !== 0) { + const error = new Error(`llama-cli exited ${code ?? killedBy ?? "unknown"}`); + error.code = "ELLAMA"; + reject(error); + } else { + const combined = `${stderr}\n${stdout}`; + const metrics = metricsFrom(combined, performance.now() - started, smapsPeakRssKb); + if (/failed to initialize samplers|\berror:/i.test(combined) + || metrics.outputTokensPerSecond === 0) { + const error = new Error("llama-cli produced no usable generation"); + error.code = "ELLAMA"; + reject(error); + return; + } + const promptAt = stdout.lastIndexOf(prompt); + const generated = promptAt >= 0 ? stdout.slice(promptAt + prompt.length) : stdout; + resolve({ + text: generated.trim(), + metrics, + }); + } + }); + }); +} diff --git a/experiments/intelligent-terrarium/src/reflection-policy.mjs b/experiments/intelligent-terrarium/src/reflection-policy.mjs new file mode 100644 index 0000000000..a5df87e0ff --- /dev/null +++ b/experiments/intelligent-terrarium/src/reflection-policy.mjs @@ -0,0 +1,32 @@ +const ORGAN_NAMES = ["sensory", "spatial", "drive", "memory", "action", "voice"]; +const ACTIONS = new Set(["attune", "broadcast"]); +const FAILURES = new Set(["disabled", "unavailable", "timeout", "malformed"]); + +export function validateReflectionProposal(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("proposal must be an object"); + const keys = Object.keys(value).sort(); + if (keys.join(",") !== "action,intensity,schema,target") throw new TypeError("proposal fields do not match schema"); + if (value.schema !== 1) throw new TypeError("proposal schema must be 1"); + if (!ACTIONS.has(value.action)) throw new TypeError("proposal action is invalid"); + if (!ORGAN_NAMES.includes(value.target)) throw new TypeError("proposal target is invalid"); + if (!Number.isFinite(value.intensity) || value.intensity < -1 || value.intensity > 1) { + throw new TypeError("proposal intensity must be within -1..1"); + } + return { + schema: 1, + action: value.action, + target: value.target, + intensity: Math.round(value.intensity * 1000) / 1000, + }; +} + +export function decideReflection({ proposal, failure } = {}) { + if (failure !== undefined) { + if (!FAILURES.has(failure) || proposal !== undefined) throw new TypeError("invalid reflection fallback"); + return { decision: "fallback", reason: failure, proposal: null }; + } + const clean = validateReflectionProposal(proposal); + if (clean.action !== "attune") return { decision: "rejected", reason: "action-not-authorized", proposal: clean }; + if (Math.abs(clean.intensity) > 0.25) return { decision: "rejected", reason: "intensity-outside-policy", proposal: clean }; + return { decision: "accepted", reason: "bounded-attunement", proposal: clean }; +} diff --git a/experiments/intelligent-terrarium/src/reflection.mjs b/experiments/intelligent-terrarium/src/reflection.mjs new file mode 100644 index 0000000000..c238aaf088 --- /dev/null +++ b/experiments/intelligent-terrarium/src/reflection.mjs @@ -0,0 +1,157 @@ +import { canonical, hash } from "./canonical.mjs"; +import { decideReflection, validateReflectionProposal } from "./reflection-policy.mjs"; + +const MAX_CONTEXT_TOKENS = 2048; +const MAX_OUTPUT_TOKENS = 128; +const MAX_PROMPT_CHARS = 6000; +const MAX_OUTPUT_CHARS = 8192; + +function episodeCounts(episodes) { + const counts = {}; + for (const episode of episodes.slice(-64)) { + const key = String(episode.kind || "unknown").slice(0, 32); + counts[key] = (counts[key] || 0) + 1; + } + return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b))); +} + +export function reflectionDigest(repository) { + const state = repository.terrarium.state; + const energy = state.entities.reduce((sum, entity) => sum + entity.energy, 0) / state.entities.length; + return { + schema: 1, + stateHash: repository.stateHash(), + tick: state.tick, + entityCount: state.entities.length, + meanEnergy: Math.round(energy * 1000) / 1000, + drives: state.mind.drives, + weights: state.mind.weights, + recentKinds: episodeCounts(state.mind.episodes), + }; +} + +export function reflectionPrompt(repository) { + const prompt = [ + "You are the bounded reflection organ of a deterministic terrarium.", + "Return exactly one JSON object and nothing else. Do not think aloud.", + "The object must contain exactly four fields: schema, action, target, and intensity.", + "Set schema to integer 1 and action to string attune.", + "The target may be sensory, spatial, drive, memory, action, or voice.", + "Use action attune. Keep intensity numeric and between -0.25 and 0.25. /no_think", + `State digest: ${canonical(reflectionDigest(repository))}`, + ].join("\n"); + if (prompt.length > MAX_PROMPT_CHARS) throw new Error("reflection prompt exceeded bound"); + return prompt; +} + +export function parseReflectionOutput(text) { + const bounded = String(text || ""); + if (!bounded || bounded.length > MAX_OUTPUT_CHARS) throw new TypeError("reflection output is empty or oversized"); + const withoutThought = bounded.replace(/[\s\S]*?<\/think>/gi, "").replace(/```(?:json)?|```/gi, "").trim(); + const candidates = withoutThought.match(/\{[^{}]*\}/g) || []; + for (let index = candidates.length - 1; index >= 0; index -= 1) { + try { + return validateReflectionProposal(JSON.parse(candidates[index])); + } catch { /* Try the preceding flat object; llama-cli may echo the prompt. */ } + } + throw new TypeError("reflection output has no valid proposal object"); +} + +function failureKind(error) { + if (error?.code === "ETIMEDOUT" || error?.name === "TimeoutError" || error?.name === "AbortError") return "timeout"; + if (error instanceof SyntaxError || error instanceof TypeError) return "malformed"; + return "unavailable"; +} + +async function boundedInference(infer, request, timeoutMs) { + const controller = new AbortController(); + let timer; + const timeout = new Promise((resolve, reject) => { + timer = setTimeout(() => { + controller.abort(); + const error = new Error("reflection inference timed out"); + error.name = "TimeoutError"; + error.code = "ETIMEDOUT"; + reject(error); + }, timeoutMs); + }); + try { + return await Promise.race([infer({ ...request, signal: controller.signal }), timeout]); + } finally { + clearTimeout(timer); + } +} + +export class ReflectionOrgan { + constructor(repository, { + infer = null, + engine = "1gb-policy", + contextTokens = MAX_CONTEXT_TOKENS, + maxOutputTokens = 96, + timeoutMs = 30_000, + } = {}) { + this.repository = repository; + this.infer = infer; + this.engine = String(engine).replace(/[^a-zA-Z0-9._:/-]/g, "-").slice(0, 160) || "unknown"; + this.contextTokens = Math.max(256, Math.min(MAX_CONTEXT_TOKENS, Math.floor(contextTokens))); + this.maxOutputTokens = Math.max(1, Math.min(MAX_OUTPUT_TOKENS, Math.floor(maxOutputTokens))); + this.timeoutMs = Math.max(100, Math.min(30_000, Math.floor(timeoutMs))); + this.tail = Promise.resolve(); + } + + reflect() { + const result = this.tail.then(() => this.#reflect()); + this.tail = result.catch(() => {}); + return result; + } + + async #reflect() { + const prompt = reflectionPrompt(this.repository); + let proposal; + let failure; + let inference = null; + let outputDigest = null; + if (!this.infer) { + failure = "disabled"; + outputDigest = hash("reflection-disabled"); + } else { + try { + inference = await boundedInference(this.infer, { + prompt, + contextTokens: this.contextTokens, + maxOutputTokens: this.maxOutputTokens, + timeoutMs: this.timeoutMs, + }, this.timeoutMs); + const text = typeof inference === "string" ? inference : inference?.text; + outputDigest = hash(String(text || "")); + proposal = parseReflectionOutput(text); + } catch (error) { + failure = failureKind(error); + outputDigest ||= hash(`${failure}:${error?.name || "Error"}`); + } + } + const evaluation = decideReflection({ proposal, failure }); + const requestId = hash({ head: this.repository.headRecordHash, outputDigest }).slice(0, 24); + const payload = { + schema: 1, + requestId, + engine: this.engine, + outputDigest, + decision: evaluation.decision, + reason: evaluation.reason, + }; + if (evaluation.proposal) payload.proposal = evaluation.proposal; + else payload.failure = failure; + const transaction = await this.repository.transact("reflection-decision", payload); + return { + ...transaction, + decision: evaluation.decision, + reason: evaluation.reason, + promptChars: prompt.length, + contextTokens: this.contextTokens, + maxOutputTokens: this.maxOutputTokens, + timeoutMs: this.timeoutMs, + metrics: typeof inference === "object" ? inference?.metrics || null : null, + }; + } +} diff --git a/experiments/intelligent-terrarium/src/repository.mjs b/experiments/intelligent-terrarium/src/repository.mjs index 7b9a5e7ce9..c49549b720 100644 --- a/experiments/intelligent-terrarium/src/repository.mjs +++ b/experiments/intelligent-terrarium/src/repository.mjs @@ -92,20 +92,26 @@ export class StateRepository { const terrarium = new Terrarium(seedDocument.seed); const records = []; let previousHash = ZERO_HASH; + const journalRecords = []; for (const file of journalFiles) { const contents = await readFile(file, "utf8"); for (const line of contents.split("\n")) { if (!line.trim()) continue; const record = JSON.parse(line); - if (record.schema !== 1) throw new Error(`unsupported journal schema ${record.schema}`); - if (record.prevHash !== previousHash) throw new Error(`journal hash chain broken at event ${record.seq}`); - if (record.recordHash !== recordHash(record)) throw new Error(`journal record hash mismatch at event ${record.seq}`); - terrarium.apply(record); - if (terrarium.stateHash() !== record.stateHash) throw new Error(`state hash mismatch at event ${record.seq}`); - previousHash = record.recordHash; - records.push(record); + if (!Number.isSafeInteger(record.seq) || record.seq < 1) throw new Error(`invalid journal sequence in ${file}`); + journalRecords.push(record); } } + journalRecords.sort((left, right) => left.seq - right.seq); + for (const record of journalRecords) { + if (record.schema !== 1) throw new Error(`unsupported journal schema ${record.schema}`); + if (record.prevHash !== previousHash) throw new Error(`journal hash chain broken at event ${record.seq}`); + if (record.recordHash !== recordHash(record)) throw new Error(`journal record hash mismatch at event ${record.seq}`); + terrarium.apply(record); + if (terrarium.stateHash() !== record.stateHash) throw new Error(`state hash mismatch at event ${record.seq}`); + previousHash = record.recordHash; + records.push(record); + } const segmentPath = join(journalRoot, "segments", `${segmentId}.ndjson`); await mkdir(join(journalRoot, "segments"), { recursive: true }); diff --git a/experiments/intelligent-terrarium/test/identity.test.mjs b/experiments/intelligent-terrarium/test/identity.test.mjs index 915dad88ef..1e5545c472 100644 --- a/experiments/intelligent-terrarium/test/identity.test.mjs +++ b/experiments/intelligent-terrarium/test/identity.test.mjs @@ -128,7 +128,8 @@ test("verified prods obey a deterministic per-handle rate bound", async () => { }); test("terrarium-dev is a hidden authorize-only loopback client seam", async () => { - const source = await readFile(resolve("../../system/public/aesthetic.computer/disks/terrarium-dev.mjs"), "utf8"); + const piecePath = process.env.TERRARIUM_PIECE_PATH || resolve("../../system/public/aesthetic.computer/disks/terrarium-dev.mjs"); + const source = await readFile(piecePath, "utf8"); const serverSource = await readFile(resolve("src/server.mjs"), "utf8"); assert.match(source, /await authorize\(\)/); assert.match(source, /Authorization: `Bearer \$\{token\}`/); diff --git a/experiments/intelligent-terrarium/test/mediorgan.test.mjs b/experiments/intelligent-terrarium/test/mediorgan.test.mjs index ff2562e414..1efc63f7ed 100644 --- a/experiments/intelligent-terrarium/test/mediorgan.test.mjs +++ b/experiments/intelligent-terrarium/test/mediorgan.test.mjs @@ -6,6 +6,28 @@ import test from "node:test"; import { verifyRepository } from "../src/repository.mjs"; import { createTerrariumServer } from "../src/server.mjs"; +function ndjsonReader(response) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let pending = ""; + return { + async next() { + for (;;) { + const newline = pending.indexOf("\n"); + if (newline >= 0) { + const line = pending.slice(0, newline); + pending = pending.slice(newline + 1); + if (line) return JSON.parse(line); + } + const { done, value } = await reader.read(); + if (done) throw new Error("terrarium stream ended before the next message"); + pending += decoder.decode(value, { stream: true }); + } + }, + cancel: () => reader.cancel(), + }; +} + test("server refuses every non-loopback binding", async () => { const root = await mkdtemp(join(tmpdir(), "terrarium-bind-")); await assert.rejects(createTerrariumServer({ root, host: "0.0.0.0" }), /refuses non-loopback/); @@ -51,6 +73,47 @@ test("loopback visitor page exposes a WebGL terrarium and mediorgan controls", a await app.stop(); }); +test("two visitors receive the same authoritative snapshot and sonic event", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-two-visitors-")); + const capabilities = { "cap-alex": "@alex", "cap-beth": "@beth" }; + const app = await createTerrariumServer({ root, capabilities, tickMs: 0 }); + const base = `http://${app.address.address}:${app.address.port}`; + const connect = async (capability) => { + const response = await fetch(`${base}/api/stream`, { + headers: { Authorization: `Bearer ${capability}` }, + }); + assert.equal(response.status, 200); + return ndjsonReader(response); + }; + const [alex, beth] = await Promise.all([connect("cap-alex"), connect("cap-beth")]); + const [alexWelcome, bethWelcome] = await Promise.all([alex.next(), beth.next()]); + assert.equal(alexWelcome.type, "welcome"); + assert.equal(bethWelcome.type, "welcome"); + assert.equal(alexWelcome.handle, "@alex"); + assert.equal(bethWelcome.handle, "@beth"); + + try { + const prod = await fetch(`${base}/api/prod`, { + method: "POST", + headers: { Authorization: "Bearer cap-alex", "Content-Type": "application/json" }, + body: JSON.stringify({ target: "voice", modality: "text", stimulus: "sing together", position: { x: -2, y: 1, z: 1 } }), + }); + assert.equal(prod.status, 202); + const [alexSnapshot, bethSnapshot] = await Promise.all([alex.next(), beth.next()]); + assert.equal(alexSnapshot.type, "snapshot"); + assert.equal(bethSnapshot.type, "snapshot"); + assert.equal(alexSnapshot.state.stateHash, bethSnapshot.state.stateHash); + assert.equal(alexSnapshot.state.lastSeq, bethSnapshot.state.lastSeq); + const [alexSonic, bethSonic] = await Promise.all([alex.next(), beth.next()]); + assert.equal(alexSonic.type, "sonic"); + assert.equal(bethSonic.type, "sonic"); + assert.deepEqual(alexSonic.event, bethSonic.event); + } finally { + await Promise.all([alex.cancel(), beth.cancel()]); + await app.stop(); + } +}); + test("concurrent ticks and outside prods remain one contiguous replayable journal", async () => { const root = await mkdtemp(join(tmpdir(), "terrarium-concurrent-")); const capability = "local-concurrency-capability"; diff --git a/experiments/intelligent-terrarium/test/reflection.test.mjs b/experiments/intelligent-terrarium/test/reflection.test.mjs new file mode 100644 index 0000000000..3411127623 --- /dev/null +++ b/experiments/intelligent-terrarium/test/reflection.test.mjs @@ -0,0 +1,137 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { Terrarium } from "../src/core.mjs"; +import { parseReflectionOutput, reflectionPrompt, ReflectionOrgan } from "../src/reflection.mjs"; +import { StateRepository, verifyRepository } from "../src/repository.mjs"; + +async function repository(prefix) { + const root = await mkdtemp(join(tmpdir(), prefix)); + const state = await StateRepository.create(root, { seed: "reflection-test", profile: "1gb" }); + state.segmentPath = join(root, "journal", "segments", "reflection.ndjson"); + return { root, state }; +} + +test("bounded reflection accepts a schema proposal without exposing selected-memory secrets", async () => { + const { root, state } = await repository("terrarium-reflect-accept-"); + const secret = "private-token-and-email@example.test"; + await state.transact("visitor-enter", { handle: "@alex", position: {} }); + await state.transact("visitor-signal", { handle: "@alex", signal: secret }); + let captured; + const reflection = new ReflectionOrgan(state, { + contextTokens: 99_999, + maxOutputTokens: 999, + timeoutMs: 99_999, + engine: "qwen3-test", + infer: async (request) => { + captured = request; + return { + text: 'not persisted\n{"schema":1,"action":"attune","target":"sensory","intensity":0.125}', + metrics: { latencyMs: 12, outputTokensPerSecond: 34 }, + }; + }, + }); + const result = await reflection.reflect(); + assert.equal(result.decision, "accepted"); + assert.equal(result.contextTokens, 2048); + assert.equal(result.maxOutputTokens, 128); + assert.equal(result.timeoutMs, 30_000); + assert.ok(result.promptChars < 6000); + assert.equal(captured.contextTokens, 2048); + assert.doesNotMatch(captured.prompt, /@alex|private-token|example\.test/); + const journal = await readFile(state.segmentPath, "utf8"); + assert.match(journal, /"kind":"reflection-decision"/); + assert.match(journal, /"decision":"accepted"/); + const reflectionRecord = journal.trim().split("\n").at(-1); + assert.doesNotMatch(reflectionRecord, /private-token|example\.test|not persisted/); + const replay = await verifyRepository(root); + assert.equal(replay.stateHash, state.stateHash()); + assert.equal(replay.headRecordHash, state.headRecordHash); +}); + +test("authority deterministically rejects an unsafe but schema-valid proposal", async () => { + const { root, state } = await repository("terrarium-reflect-reject-"); + const result = await new ReflectionOrgan(state, { + infer: async () => '{"schema":1,"action":"broadcast","target":"voice","intensity":0.1}', + }).reflect(); + assert.equal(result.decision, "rejected"); + assert.equal(result.reason, "action-not-authorized"); + assert.equal((await verifyRepository(root)).stateHash, state.stateHash()); + + const candidate = Terrarium.fromSnapshot(state.terrarium.snapshot()); + assert.throws(() => candidate.apply({ + seq: candidate.state.lastSeq + 1, + kind: "reflection-decision", + payload: { + schema: 1, + requestId: "a".repeat(24), + engine: "test", + outputDigest: "b".repeat(64), + proposal: { schema: 1, action: "broadcast", target: "voice", intensity: 0.1 }, + decision: "accepted", + reason: "bounded-attunement", + }, + }), /does not match deterministic policy/); +}); + +test("parser selects the final proposal when llama-cli echoes prompt JSON", async () => { + const { state } = await repository("terrarium-reflect-echo-"); + assert.throws(() => parseReflectionOutput(reflectionPrompt(state)), /no valid proposal/); + const result = await new ReflectionOrgan(state, { + infer: async () => [ + '> Schema: {"schema":1,"action":"attune","target":"sensory","intensity":0.2}', + '{"schema":1,"action":"attune","target":"voice","intensity":0.05}', + "[ Prompt: 205.2 t/s | Generation: 24.9 t/s ]", + ].join("\n"), + }).reflect(); + assert.equal(result.decision, "accepted"); + assert.equal(result.record.payload.proposal.target, "voice"); +}); + +test("malformed, timeout, and disabled inference unconditionally fall back", async () => { + const malformed = await repository("terrarium-reflect-malformed-"); + const malformedResult = await new ReflectionOrgan(malformed.state, { + infer: async () => '{"schema":1,"action":"attune","target":"memory","intensity":0.1,"secret":"no"}', + }).reflect(); + assert.deepEqual([malformedResult.decision, malformedResult.reason], ["fallback", "malformed"]); + + const timeout = await repository("terrarium-reflect-timeout-"); + const timeoutResult = await new ReflectionOrgan(timeout.state, { + timeoutMs: 100, + infer: ({ signal }) => new Promise((resolve, reject) => { + signal.addEventListener("abort", () => { + const error = new Error("aborted test inference"); + error.name = "AbortError"; + reject(error); + }, { once: true }); + }), + }).reflect(); + assert.deepEqual([timeoutResult.decision, timeoutResult.reason], ["fallback", "timeout"]); + + const disabled = await repository("terrarium-reflect-disabled-"); + const disabledResult = await new ReflectionOrgan(disabled.state).reflect(); + assert.deepEqual([disabledResult.decision, disabledResult.reason], ["fallback", "disabled"]); + await disabled.state.transact("advance", { ticks: 1 }); + assert.equal((await verifyRepository(disabled.root)).stateHash, disabled.state.stateHash()); +}); + +test("reflection inference admits only one request at a time", async () => { + const { state } = await repository("terrarium-reflect-queue-"); + let active = 0; + let peak = 0; + const reflection = new ReflectionOrgan(state, { + infer: async () => { + active += 1; + peak = Math.max(peak, active); + await new Promise((resolve) => setTimeout(resolve, 10)); + active -= 1; + return '{"schema":1,"action":"attune","target":"drive","intensity":0.01}'; + }, + }); + const results = await Promise.all([reflection.reflect(), reflection.reflect(), reflection.reflect()]); + assert.equal(peak, 1); + assert.deepEqual(results.map(({ decision }) => decision), ["accepted", "accepted", "accepted"]); + assert.deepEqual(state.records.map(({ seq }) => seq), [1, 2, 3]); +}); diff --git a/experiments/intelligent-terrarium/test/repository.test.mjs b/experiments/intelligent-terrarium/test/repository.test.mjs index d1fc014b19..aaf1a70e33 100644 --- a/experiments/intelligent-terrarium/test/repository.test.mjs +++ b/experiments/intelligent-terrarium/test/repository.test.mjs @@ -32,6 +32,19 @@ test("tampered journal content fails hash verification", async () => { await assert.rejects(StateRepository.open(root), /record hash mismatch|state hash mismatch/); }); +test("journal replay follows sequence rather than segment filename order", async () => { + const root = await mkdtemp(join(tmpdir(), "terrarium-segment-order-")); + const repository = await StateRepository.create(root, { seed: "segment-order" }); + repository.segmentPath = join(root, "journal", "segments", "zzz-first.ndjson"); + await repository.transact("advance", { ticks: 3 }); + repository.segmentPath = join(root, "journal", "segments", "aaa-second.ndjson"); + await repository.transact("advance", { ticks: 4 }); + const replay = await verifyRepository(root); + assert.equal(replay.lastSeq, 2); + assert.equal(replay.stateHash, repository.stateHash()); + assert.equal(replay.headRecordHash, repository.headRecordHash); +}); + test("sleep commits only allowlisted state and no-op sleep makes no commit", async () => { const root = await mkdtemp(join(tmpdir(), "terrarium-sleep-")); const repository = await StateRepository.create(root, { seed: "sleep-cycle" }); diff --git a/experiments/intelligent-terrarium/test/score.test.mjs b/experiments/intelligent-terrarium/test/score.test.mjs new file mode 100644 index 0000000000..167c2c8305 --- /dev/null +++ b/experiments/intelligent-terrarium/test/score.test.mjs @@ -0,0 +1,16 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +test("fullscreen score presents organs, honest gated QR, and no network dependency", async () => { + const html = await readFile(new URL("../score.html", import.meta.url), "utf8"); + assert.match(html, / Date: Thu, 23 Jul 2026 14:27:46 -0700 Subject: [PATCH 07/14] Record automatic miner guard recovery --- experiments/intelligent-terrarium/KEEPER.md | 22 ++++++++++++++----- experiments/intelligent-terrarium/score.html | 2 +- .../intelligent-terrarium/test/score.test.mjs | 2 ++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md index ba46d52995..2fa726991d 100644 --- a/experiments/intelligent-terrarium/KEEPER.md +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -388,8 +388,9 @@ state repository. Nothing was pushed or deployed. `MemoryHigh=900 MiB`, zero swap, and low priority. It journaled `disabled` fallback deterministically; cgroup peak was 31,932,416 bytes and Node RSS was 78,917,632 bytes. The 1 GB floor still never loads the model. -- The final 2 GB coexistence run completed in 6.71 seconds while - `matador-miner` remained active at 100% GPU use. Qwen generated at 13.3 +- The final 2 GB coexistence run completed in 6.71 seconds after the miner's + automatic recovery, while `matador-miner` remained active at 100% GPU use. + Qwen generated at 13.3 tokens/second after an 88.9 tokens/second prompt pass. `/proc` smaps peak RSS was 825,940 KiB (about 807 MiB), below both policy thresholds; cgroup peak was 227,233,792 bytes because model file pages were already warm and charged @@ -404,10 +405,19 @@ state repository. Nothing was pushed or deployed. Two earlier retries also completed and slept cleanly as sequences 910–911. - A scan of journal, checkpoints, autobiography, manifest, and visitor facts found no bearer/token, email-like identifier, model/cache path, or GGUF name. - The listening-socket hash was identical before and after inference. The miner - had independently exited during the earlier build window and later returned; - no keeper command signalled, stopped, restarted, or reconfigured it. The - final run proves live coexistence after its return. + The listening-socket hash was identical before and after inference. +- Stage 4 did affect mining indirectly: `btx-miner-guard` detected the nice-15 + compiler/build workload, stopped PID 2928 at 12:34:02, and automatically + restarted the service as PID 136022 at 12:49:41 after the build processes + exited. This was a 15 minute 39 second guard pause, so Stage 4 does **not** + claim zero miner effect. No keeper command signalled, stopped, restarted, or + reconfigured the miner. Immediately before the pause it reported about 4,115 + nonce/s and used 3,298 MiB process VRAM; after automatic recovery it reported + 4,141 then 4,169 nonce/s and returned to the same 3,298 MiB process VRAM. + The later audit showed `mining_state:"mining"`, 100% GPU utilization, no + rejected/stale shares, and a 1-hour average of 3,754.7 nonce/s. The final + CPU-only reflection run proves live coexistence after recovery, not + uninterrupted coexistence across the build. - `score.html` is a self-contained 1920×1080 spatial graphic measure: the six organs are colored staves, outside prods ripple through the Mediorgan membrane, reflection breathes inside the body, and quiet proof marks show the diff --git a/experiments/intelligent-terrarium/score.html b/experiments/intelligent-terrarium/score.html index 7387868c34..40b480fe3b 100644 --- a/experiments/intelligent-terrarium/score.html +++ b/experiments/intelligent-terrarium/score.html @@ -55,7 +55,7 @@
hard ceiling2 GiB · SWAP 0
inferenceACCEPT · SENSORY +.25
-
GPUMINER UNTOUCHED
+
GPUGUARD AUTO-RESUMED
QR code for the future authenticated terrarium web client route diff --git a/experiments/intelligent-terrarium/test/score.test.mjs b/experiments/intelligent-terrarium/test/score.test.mjs index 167c2c8305..3cde867e6a 100644 --- a/experiments/intelligent-terrarium/test/score.test.mjs +++ b/experiments/intelligent-terrarium/test/score.test.mjs @@ -11,6 +11,8 @@ test("fullscreen score presents organs, honest gated QR, and no network dependen } assert.match(html, /CLIENT GATE · PREVIEW/); assert.match(html, /remain closed pending Stage 5 approval/); + assert.match(html, /GUARD AUTO-RESUMED/); + assert.doesNotMatch(html, /MINER UNTOUCHED/); assert.match(html, /score-assets\/join-preview\.svg/); assert.doesNotMatch(html, / Date: Thu, 23 Jul 2026 14:39:41 -0700 Subject: [PATCH 08/14] Make the display a volumetric imagination --- experiments/intelligent-terrarium/KEEPER.md | 23 +- experiments/intelligent-terrarium/score.html | 283 ++++++++++-------- .../intelligent-terrarium/test/score.test.mjs | 20 +- 3 files changed, 179 insertions(+), 147 deletions(-) diff --git a/experiments/intelligent-terrarium/KEEPER.md b/experiments/intelligent-terrarium/KEEPER.md index 2fa726991d..2f9d138133 100644 --- a/experiments/intelligent-terrarium/KEEPER.md +++ b/experiments/intelligent-terrarium/KEEPER.md @@ -248,8 +248,9 @@ The first thin slice is accepted only when all of these pass: ## Next keeper action -Hold after Stage 4. The local fullscreen score may remain as a display-only -rehearsal, but its QR is deliberately marked as a non-live Stage 5 preview. +Hold after Stage 4. The local fullscreen imagination may remain as a display-only +rehearsal. Client-route details and the earlier QR concept are deliberately +absent from the screen; the Stage 5 gateway remains closed. A real logged-in-token exercise, WebAudio-device check, physical Xbox-controller check, and any reachable authenticated gateway still require keeper decisions. No push, deployment, persistent authority service, or public listener is @@ -418,11 +419,13 @@ state repository. Nothing was pushed or deployed. rejected/stale shares, and a 1-hour average of 3,754.7 nonce/s. The final CPU-only reflection run proves live coexistence after recovery, not uninterrupted coexistence across the build. -- `score.html` is a self-contained 1920×1080 spatial graphic measure: the six - organs are colored staves, outside prods ripple through the Mediorgan - membrane, reflection breathes inside the body, and quiet proof marks show the - journal, replay, resident envelope, and miner boundary. Its QR encodes the - intended `https://aesthetic.computer/terrarium-dev` route but visibly says the - client gate is only a preview pending Stage 5. It opens from `file://` in a - fullscreen Chromium kiosk on the existing `jastow` GNOME session; it opens no - port and depends on no remote script. +- `score.html` is now only the terrarium's **imagination**: one large living + volumetric pixel lava lamp, per the keeper's display direction. It has no + visible title, panel, metric, legend, or QR. + A fixed 48 × 36 × 48 WebGL2 voxel lattice renders the overlap of six bounded + organ fields, culture growth, outside-prod waves, and reflection pulses as + density, color, and motion. Provenance, identity, memory, miner, and Stage 5 + gate facts remain screen-reader metadata and keeper evidence rather than + visual chrome. It opens from `file://` in a fullscreen Chromium kiosk on the + existing `jastow` GNOME session; it opens no port and depends on no remote + script. diff --git a/experiments/intelligent-terrarium/score.html b/experiments/intelligent-terrarium/score.html index 40b480fe3b..2110a1e9cc 100644 --- a/experiments/intelligent-terrarium/score.html +++ b/experiments/intelligent-terrarium/score.html @@ -3,145 +3,176 @@ - Intelligent Terrarium — Mediorgan Score + Intelligent Terrarium — Imagination - -
-
JAS-NZXT · LOCAL REHEARSAL · 23 JULY 2026
-

Intelligent Terrarium
Mediorgan score

-
outside prods cross the membrane as stimulus · organs listen and answer · one authority writes the body
-
- -
-
- SENSORYSPATIALDRIVE - MEMORYACTIONVOICE -
-
QWEN3 0.6B Q8_0 · LLAMA.CPP CPU · CTX 2048 · OUTPUT ≤ 128
-
+ +
+ Intelligent Terrarium measure 912. Replay verified. One authoritative writer. + Sensory, spatial, drive, memory, action, and voice organs. Reflection accepted: + sensory plus 0.25. Two GiB hard ceiling, zero swap. Miner guard auto-resumed. + Authenticated client gate remains closed pending Stage 5 approval. +
+ + diff --git a/experiments/intelligent-terrarium/web/board.mjs b/experiments/intelligent-terrarium/web/board.mjs new file mode 100644 index 0000000000..c1cc3db5e4 --- /dev/null +++ b/experiments/intelligent-terrarium/web/board.mjs @@ -0,0 +1,136 @@ +const get = (id) => document.getElementById(id); +const history = []; +const raster = get("board-raster"); +const rasterContext = raster.getContext("2d", { alpha: false }); +const rasterWidth = 80; +const rasterHeight = 45; +let rasterState = { iteration: 0, coverage: 0, capacity: 96, accepted: 0, active: {} }; +let lastRasterFrame = 0; +let checkpointDeadline = 0; +let checkpointIteration = 0; +let checkpointRemaining = 0; + +function text(id, value) { get(id).textContent = value ?? "—"; } + +function progress(id, value) { + get(id).style.width = `${Math.max(0, Math.min(1, Number(value) || 0)) * 100}%`; +} + +function rasterSignal(x, y, state, epoch) { + const active = state.active || {}; + const id = String(active.id || "piecefarm"); + const seed = [...id].reduce((sum, char) => (sum * 33 + char.charCodeAt(0)) >>> 0, 5381); + return (seed + x * 41 + y * 73 + epoch * 17 + state.iteration * 3) >>> 0; +} + +function drawRaster(time) { + requestAnimationFrame(drawRaster); + if (time - lastRasterFrame < 125) return; + lastRasterFrame = time; + if (raster.width !== rasterWidth || raster.height !== rasterHeight) { + raster.width = rasterWidth; + raster.height = rasterHeight; + rasterContext.imageSmoothingEnabled = false; + } + const epoch = Math.floor(time / 750); + const coverage = rasterState.capacity ? rasterState.coverage / rasterState.capacity : 0; + const novelty = Number(rasterState.active?.novelty || 0); + for (let y = 0; y < rasterHeight; y += 1) { + for (let x = 0; x < rasterWidth; x += 1) { + const signal = rasterSignal(x, y, rasterState, epoch); + const band = Math.floor(x / 8) + Math.floor(y / 5); + const hue = (signal % 360 + coverage * 140 + novelty * 90 + band * 19) % 360; + const pulse = (signal + epoch + x + y) % 7 === 0; + rasterContext.fillStyle = `hsl(${hue} ${pulse ? 90 : 72}% ${pulse ? 42 : 19 + signal % 15}%)`; + rasterContext.fillRect(x, y, 1, 1); + } + } +} + +function renderCountdown() { + const remainingMs = Math.max(0, checkpointDeadline - Date.now()); + const seconds = Math.ceil(remainingMs / 1000); + const hours = String(Math.floor(seconds / 3600)).padStart(2, "0"); + const minutes = String(Math.floor(seconds % 3600 / 60)).padStart(2, "0"); + const remainder = String(seconds % 60).padStart(2, "0"); + text("countdown-clock", `${hours}:${minutes}:${remainder}`); + text("countdown-detail", `${checkpointRemaining.toLocaleString()} ITERATIONS TO EDITION ${checkpointIteration.toLocaleString()}`); +} + +function drawChart() { + const canvas = get("chart"); + const ratio = devicePixelRatio || 1; + canvas.width = Math.max(1, Math.floor(canvas.clientWidth * ratio)); + canvas.height = Math.max(1, Math.floor(canvas.clientHeight * ratio)); + const ctx = canvas.getContext("2d"); + ctx.scale(ratio, ratio); + const w = canvas.clientWidth, h = canvas.clientHeight; + ctx.clearRect(0, 0, w, h); + ctx.strokeStyle = "#253a35"; + ctx.lineWidth = 1; + for (let i = 1; i < 4; i += 1) { + const y = (h / 4) * i; + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); + } + if (history.length < 2) return; + ctx.strokeStyle = "#ffda68"; + ctx.lineWidth = 3; + ctx.beginPath(); + history.forEach((point, index) => { + const x = index / (history.length - 1) * w; + const y = h - point * h; + if (index) ctx.lineTo(x, y); else ctx.moveTo(x, y); + }); + ctx.stroke(); +} + +function update(state) { + const active = state.active || {}; + rasterState = state; + text("mission", (state.score || "search program-output space without surrendering verification").toUpperCase()); + text("ticker", `ITER ${state.iteration} · VERIFIED ${state.accepted} · COVERAGE ${state.coverage}/${state.capacity} · NOW FARMING ${active.source || "(empty soup)"}`); + checkpointIteration = Number(state.checkpoint?.nextIteration || 0); + checkpointRemaining = Number(state.checkpoint?.iterationsRemaining || 0); + checkpointDeadline = Date.now() + Number(state.checkpoint?.estimatedMs || 0); + renderCountdown(); + const coverageRatio = state.capacity ? state.coverage / state.capacity : 0; + const yieldRatio = state.iteration ? state.accepted / state.iteration : 0; + text("phase-value", "2 / 5"); + progress("phase-fill", 2 / 5); + text("coverage-value", `${state.coverage} / ${state.capacity}`); + progress("coverage-fill", coverageRatio); + text("yield-value", `${(yieldRatio * 100).toFixed(1)}%`); + progress("yield-fill", yieldRatio); + text("novelty-value", Number(active.novelty || 0).toFixed(3)); + progress("novelty-fill", active.novelty); + text("iteration", state.iteration); + text("novelty", Number(active.novelty || 0).toFixed(3)); + text("coverage", `${state.coverage}/${state.capacity}`); + text("accepted", state.accepted); + text("rejected", state.rejected); + text("operations", Number(active.metrics?.operations || 0).toLocaleString()); + history.push(coverageRatio); + if (history.length > 240) history.shift(); + drawChart(); +} + +async function stream() { + const response = await fetch("/api/stream"); + if (!response.ok) throw new Error(`observatory stream refused (${response.status})`); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let pending = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + pending += decoder.decode(value, { stream: true }); + const lines = pending.split("\n"); + pending = lines.pop(); + for (const line of lines) if (line) update(JSON.parse(line).state); + } +} + +window.addEventListener("resize", drawChart); +setInterval(renderCountdown, 250); +requestAnimationFrame(drawRaster); +stream().catch((error) => { text("ticker", `STREAM ERROR · ${error.message}`); }); diff --git a/experiments/intelligent-terrarium/web/soup.css b/experiments/intelligent-terrarium/web/soup.css new file mode 100644 index 0000000000..c781308d5b --- /dev/null +++ b/experiments/intelligent-terrarium/web/soup.css @@ -0,0 +1,265 @@ +:root { + color-scheme: dark; + font-family: "FiraCode Nerd Font Mono", "Courier New", monospace; + font-synthesis: none; + -webkit-font-smoothing: none; + text-rendering: optimizeSpeed; + background: #05080d; + color: #f1f4e8; +} + +* { box-sizing: border-box; } +html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #05080d; } +body { cursor: none; } +canvas { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; } + +.soup-overlay { + position: fixed; + inset: 0 0 auto 0; + z-index: 3; + min-height: 0; + padding: clamp(5px, .55vw, 10px) clamp(8px, 1vw, 18px); + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: clamp(10px, 1.4vw, 28px); + pointer-events: none; + color: #f7ffe9; + background: #020508e8; + border-bottom: 1px solid #53655d; + text-shadow: 2px 2px 0 #05080d; +} + +.soup-overlay header, +.soup-overlay footer { + display: flex; + justify-content: space-between; + gap: clamp(8px, 1vw, 18px); + font-size: clamp(13px, 1.15vw, 22px); + line-height: 1; +} + +.soup-overlay header b { color: #ffda68; font-size: 1.1em; } +.soup-overlay header span { color: #9ff1cd; } + +.soup-overlay strong { + display: block; + max-width: 100%; + margin: 0; + overflow: hidden; + color: #ffffff; + font-size: clamp(14px, 1.4vw, 27px); + line-height: 1; + white-space: nowrap; + text-overflow: clip; + text-align: center; +} + +.soup-overlay footer { color: #ef82dc; font-size: clamp(11px, .95vw, 18px); } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.board { + position: relative; + isolation: isolate; + min-height: 100vh; + padding: clamp(18px, 2.4vw, 46px); + display: grid; + grid-template-rows: auto auto auto auto auto auto 1fr auto; + gap: clamp(14px, 2vh, 28px); + background: + radial-gradient(circle at 82% 10%, #432044 0, transparent 36%), + radial-gradient(circle at 8% 90%, #123c3a 0, transparent 40%), + #080a10; +} + +.board-raster { + position: absolute; + inset: 0; + z-index: 0; + width: 100%; + height: 100%; + opacity: .54; + image-rendering: pixelated; + filter: saturate(1.35) contrast(1.08); +} + +.board::before { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + background: + linear-gradient(90deg, #03070bdf 0 54%, #140817d4 100%), + repeating-linear-gradient(0deg, transparent 0 31px, #ffffff08 31px 32px); +} + +.board > :not(.board-raster) { position: relative; z-index: 2; } + +.mast { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 24px; + border-bottom: 2px solid #e8efc7; + padding-bottom: 12px; +} + +.mast h1 { margin: 0; font-size: clamp(40px, 4.4vw, 84px); line-height: .9; letter-spacing: -.045em; } +.mast p { margin: 0; font-size: clamp(24px, 1.9vw, 36px); color: #9acbb7; } + +.source { + min-height: 1.9em; + max-width: 22ch; + font-size: clamp(48px, 5.8vw, 112px); + font-weight: 800; + line-height: .92; + letter-spacing: -.07em; + color: #ffda68; + overflow-wrap: anywhere; +} + +.program-ticker { + min-width: 0; + display: flex; + align-items: center; + gap: clamp(16px, 2vw, 36px); + border-block: 3px solid #ffda68; + padding: clamp(8px, 1vh, 14px) 0; + overflow: hidden; + white-space: nowrap; + font-size: clamp(32px, 2.7vw, 52px); + color: #f1f4e8; + background: #05080dcc; +} + +.program-ticker b { + flex: none; + padding: .12em .35em; + color: #05080d; + background: #ffda68; + letter-spacing: .08em; +} + +.program-ticker span { overflow: hidden; text-overflow: clip; } + +.countdown { + display: grid; + grid-template-columns: auto auto 1fr; + align-items: center; + gap: clamp(20px, 2.2vw, 44px); + padding: clamp(9px, 1vh, 16px) clamp(14px, 1.4vw, 26px); + border: 3px solid #ffda68; + background: #120d18e8; +} + +.countdown b { color: #9acbb7; font-size: clamp(30px, 2.2vw, 42px); } +.countdown span { color: #ffda68; font-size: clamp(54px, 4.7vw, 90px); font-weight: 800; line-height: .85; letter-spacing: -.06em; } +.countdown small { color: #f1f4e8; font-size: clamp(28px, 1.9vw, 36px); text-align: right; } + +.progress-rack { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: clamp(8px, 1vw, 18px); +} + +.progress-card { + min-width: 0; + padding: clamp(10px, 1vw, 18px); + border: 1px solid #61726a; + background: #091017e6; +} + +.progress-card header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + font-size: clamp(26px, 1.9vw, 37px); +} + +.progress-card header b { color: #9acbb7; letter-spacing: .06em; } +.progress-card header span { color: #f1f4e8; font-size: 1.25em; } + +.progress-track { + height: clamp(18px, 2.1vh, 30px); + margin: clamp(8px, .8vh, 12px) 0; + border: 2px solid #465951; + background: #020508; +} + +.progress-track i { + display: block; + width: 0; + height: 100%; + background: linear-gradient(90deg, #19a6a0, #ffda68); + box-shadow: 0 0 18px #ffda6866; + transition: width 600ms ease; +} + +.phase-progress .progress-track i { background: linear-gradient(90deg, #c72cf2, #ffda68); } +.progress-card small { color: #9db8ab; font-size: clamp(19px, 1.25vw, 24px); letter-spacing: .035em; } + +.next-strip { + display: flex; + align-items: center; + gap: clamp(18px, 2vw, 38px); + min-width: 0; + padding: clamp(8px, .8vh, 13px) clamp(12px, 1.2vw, 22px); + border-left: clamp(10px, 1vw, 18px) solid #c72cf2; + background: linear-gradient(90deg, #31133f, #101019 65%); + font-size: clamp(34px, 3vw, 58px); + line-height: 1; + white-space: nowrap; +} + +.next-strip b { flex: none; color: #ffda68; } +.next-strip span { overflow: hidden; color: #f1f4e8; font-weight: 700; } + +.board-body { + min-height: 0; + display: grid; + grid-template-columns: 1fr; + gap: clamp(18px, 3vw, 54px); +} + +.metrics { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 10px; + align-content: start; +} + +.metric { + min-height: clamp(95px, 13vh, 190px); + border: 1px solid #8b9671; + padding: clamp(10px, 1.3vw, 22px); + background: #0b1016c7; +} + +.metric b { display: block; font-size: clamp(22px, 1.55vw, 30px); color: #9acbb7; letter-spacing: .055em; } +.metric span { display: block; margin-top: .18em; font-size: clamp(48px, 3.9vw, 74px); line-height: 1; } + +.lineage { border-left: 2px solid #e8efc7; padding-left: clamp(16px, 2vw, 34px); } +.lineage h2 { margin: 0 0 12px; font-size: clamp(15px, 1.4vw, 25px); color: #9acbb7; } +.lineage dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 9px 16px; font-size: clamp(13px, 1.35vw, 24px); } +.lineage dt { color: #71847b; } +.lineage dd { margin: 0; overflow-wrap: anywhere; } + +.chart { width: 100%; height: clamp(72px, 12vh, 150px); border-top: 1px solid #4b5b55; } + +@media (max-aspect-ratio: 1/1) { + .board-body { grid-template-columns: 1fr; } + .lineage { border-left: 0; border-top: 2px solid #e8efc7; padding: 16px 0 0; } +} diff --git a/experiments/intelligent-terrarium/web/soup.html b/experiments/intelligent-terrarium/web/soup.html new file mode 100644 index 0000000000..bdcff96e46 --- /dev/null +++ b/experiments/intelligent-terrarium/web/soup.html @@ -0,0 +1,19 @@ + + + + + + Piecefarm — Living Lisp Soup + + + + + +

Connecting to Piecefarm.

+ + + diff --git a/experiments/intelligent-terrarium/web/soup.mjs b/experiments/intelligent-terrarium/web/soup.mjs new file mode 100644 index 0000000000..2331adad4c --- /dev/null +++ b/experiments/intelligent-terrarium/web/soup.mjs @@ -0,0 +1,424 @@ +const canvas = document.getElementById("soup"); +const description = document.getElementById("description"); +const soupIteration = document.getElementById("soup-iteration"); +const soupProgram = document.getElementById("soup-program"); +const soupCoverage = document.getElementById("soup-coverage"); +const soupVerification = document.getElementById("soup-verification"); +const ctx = canvas.getContext("2d", { alpha: false }); +let state = { programs: [], selected: null, iteration: 0, coverage: 0, capacity: 96 }; +let selectedIndex = 0; +let columns = 4; +let lastMove = 0; +const rasterCache = new Map(); +const grooveCache = new Map(); +const groovePending = new Set(); +const GROOVE_TRACKS = Object.freeze([ + ["header", "HEADER", "#ff5a67"], + ["sequence", "SEQUENCE", "#ff984f"], + ["functions", "FUNCTIONS", "#ffd84a"], + ["bodies", "BODIES", "#93e85f"], + ["projection", "PROJECTION", "#40dfaa"], + ["lifecycle", "LIFECYCLE", "#38d9e6"], + ["state", "STATE", "#4f9cff"], + ["sprites", "SPRITES", "#8f7cff"], + ["proposals", "PROPOSALS", "#d96cff"], + ["source", "SOURCE", "#ff62bc"], + ["fringe", "FRINGE", "#f7f0d0"], +]); + +function grooveCoordinates() { + const coordinates = []; + for (let ring = 0; ring < 16; ring += 1) { + const low = ring, high = 159 - ring; + for (let x = low; x <= high; x += 1) coordinates.push([x, low]); + for (let y = low + 1; y <= high; y += 1) coordinates.push([high, y]); + for (let x = high - 1; x >= low; x -= 1) coordinates.push([x, high]); + for (let y = high - 1; y > low; y -= 1) coordinates.push([low, y]); + } + return coordinates; +} + +const groovePixels = grooveCoordinates(); + +function renderSubstrate(program, groove) { + const canvas = document.createElement("canvas"); + canvas.width = 160; canvas.height = 160; + const pixels = new Uint8ClampedArray(160 * 160 * 4); + const margin = groove.groove; + for (let pixel = 0; pixel < groovePixels.length; pixel += 1) { + const [x, y] = groovePixels[pixel], target = (y * 160 + x) * 4, source = pixel * 6; + pixels[target] = parseInt(margin.slice(source, source + 2), 16); + pixels[target + 1] = parseInt(margin.slice(source + 2, source + 4), 16); + pixels[target + 2] = parseInt(margin.slice(source + 4, source + 6), 16); + pixels[target + 3] = 255; + } + const sample = program.sample, side = Number(sample?.width || 0); + if (sample?.rgb && side > 0 && side === Number(sample.height)) { + for (let y = 0; y < 128; y += 1) for (let x = 0; x < 128; x += 1) { + const sx = Math.min(side - 1, Math.floor(x * side / 128)); + const sy = Math.min(side - 1, Math.floor(y * side / 128)); + const source = (sy * side + sx) * 6, target = ((y + 16) * 160 + x + 16) * 4; + pixels[target] = parseInt(sample.rgb.slice(source, source + 2), 16); + pixels[target + 1] = parseInt(sample.rgb.slice(source + 2, source + 4), 16); + pixels[target + 2] = parseInt(sample.rgb.slice(source + 4, source + 6), 16); + pixels[target + 3] = 255; + } + } + canvas.getContext("2d").putImageData(new ImageData(pixels, 160, 160), 0, 0); + return canvas; +} + +function ensureGroove(program) { + if (!program?.id || groovePending.has(program.id)) return; + const cached = grooveCache.get(program.id); + if (cached && Date.now() - cached.at < 12_000) return; + groovePending.add(program.id); + fetch(`/api/groove/${encodeURIComponent(program.id)}`) + .then((response) => response.ok ? response.json() : Promise.reject(new Error(`groove ${response.status}`))) + .then((groove) => grooveCache.set(program.id, { + at: Date.now(), canvas: renderSubstrate(program, groove), needlePixel: groove.record?.needlePixel || 0, + record: groove.record || null, + })) + .catch(() => {}) + .finally(() => groovePending.delete(program.id)); +} + +const palette = { + resident: "#94f0bd", + dissolving: "#a57970", + rejected: "#f05f78", + classic: "#ffda68", + grammar: "#75cfde", + llm: "#ef82dc", + prox: "#ef82dc", +}; + +function fit() { + const ratio = devicePixelRatio || 1; + canvas.width = Math.max(1, Math.floor(canvas.clientWidth * ratio)); + canvas.height = Math.max(1, Math.floor(canvas.clientHeight * ratio)); + ctx.setTransform(ratio, 0, 0, ratio, 0, 0); + ctx.imageSmoothingEnabled = false; +} + +function programValues(program, phase) { + const values = [...(program.sample?.input || [])]; + const trace = program.sample?.trace || []; + if (!trace.length) return { values, active: null }; + const end = Math.floor(phase * trace.length) % (trace.length + 28); + let active = null; + for (let i = 0; i < Math.min(end, trace.length); i += 1) { + const event = trace[i]; + if (event[0] === "s") [values[event[1]], values[event[2]]] = [values[event[2]], values[event[1]]]; + if (event[0] === "w") values[event[1]] = event[3]; + active = event; + } + return { values, active }; +} + +function tileLayout(count) { + const w = canvas.clientWidth, h = canvas.clientHeight; + columns = 4; + return { columns: 4, rows: 3, width: w / 4, height: h / 3 }; +} + +function boardPrograms() { + const addressed = state.displayPrograms?.filter((program) => program?.domain === "raster") || []; + const programs = addressed.length ? addressed : (state.programs || []).filter((program) => program.domain === "raster").slice(-12); + return programs.slice(0, 12); +} + +function fillField(programs) { + const layout = tileLayout(programs.length); + const slots = 12; + const field = Array.from({ length: slots }, (_, index) => ({ + ...programs[index % programs.length], + visualEcho: index >= programs.length, + visualSlot: index, + })); + return { field, layout }; +} + +function drawMembranes(programs, layout) { + const originals = programs.filter((program) => !program.visualEcho); + const positions = new Map(originals.map((program) => [program.id, { + x: (program.visualSlot % layout.columns + .5) * layout.width, + y: (Math.floor(program.visualSlot / layout.columns) + .5) * layout.height, + }])); + ctx.save(); + ctx.globalCompositeOperation = "screen"; + ctx.lineWidth = 1.25; + for (const program of originals) { + const a = positions.get(program.id), b = positions.get(program.parent); + if (!a || !b) continue; + ctx.strokeStyle = `${palette[program.origin] || "#677"}3d`; + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + const mid = (a.x + b.x) / 2; + ctx.bezierCurveTo(mid, a.y, mid, b.y, b.x, b.y); + ctx.stroke(); + ctx.fillStyle = `${palette[program.origin] || "#677"}66`; + ctx.fillRect(a.x - 1.5, a.y - 1.5, 3, 3); + } + ctx.restore(); +} + +function drawGrooveBands(cached, x, y, width, height, left, top, side) { + const rightGutter = x + width - (left + side); + const outside = rightGutter >= 38; + const railWidth = Math.max(4, Math.min(10, side * .025)); + const railX = outside ? left + side + 4 : left + side - railWidth - 3; + const rowHeight = side / GROOVE_TRACKS.length; + const density = cached?.record?.density?.tracks || {}; + ctx.save(); + ctx.font = `${Math.max(6, Math.min(8, rowHeight * .3))}px "FiraCode Nerd Font Mono", "Courier New", monospace`; + ctx.textBaseline = "middle"; + for (let index = 0; index < GROOVE_TRACKS.length; index += 1) { + const [key, label, color] = GROOVE_TRACKS[index]; + const fill = Math.max(0, Math.min(1, Number(density[key]?.pixelFill) || 0)); + const rowY = top + index * rowHeight; + ctx.globalAlpha = outside ? .34 : .68; + ctx.fillStyle = color; + ctx.fillRect(railX, rowY, railWidth, Math.max(1, rowHeight - 1)); + ctx.globalAlpha = 1; + ctx.fillRect(railX, rowY, railWidth * fill, Math.max(1, rowHeight - 1)); + if (outside) { + ctx.fillStyle = color; + ctx.globalAlpha = .82; + ctx.fillText(label, railX + railWidth + 4, rowY + rowHeight / 2); + } + } + ctx.restore(); +} + +function drawRasterBed(program, x, y, width, height, time) { + ensureGroove(program); + const cached = grooveCache.get(program.id), substrate = cached?.canvas; + if (substrate) { + const side = Math.min(width, height), left = x + (width - side) / 2, top = y + (height - side) / 2; + ctx.save(); ctx.globalAlpha = program.visualEcho ? .28 : .68; + ctx.imageSmoothingEnabled = false; + ctx.drawImage(substrate, left, top, side, side); + ctx.restore(); + drawGrooveBands(cached, x, y, width, height, left, top, side); + const live = state.runtime?.vm?.telemetry?.residents?.find((row) => row.id === program.id); + const needlePixel = Math.max(0, Math.min(groovePixels.length - 1, Number(live?.needlePixel ?? cached.needlePixel) || 0)); + const [needleX, needleY] = groovePixels[needlePixel]; + const scale = side / 160, pulse = .45 + .55 * Math.abs(Math.sin(time * Math.PI / 260)); + ctx.save(); + ctx.globalAlpha = pulse; + ctx.fillStyle = "#fff7c5"; + ctx.shadowColor = "#ff2ca8"; ctx.shadowBlur = Math.max(5, scale * 4); + ctx.fillRect(left + needleX * scale, top + needleY * scale, Math.max(2, scale), Math.max(2, scale)); + ctx.restore(); + return; + } + const sample = program.sample; + if (sample?.rgb && sample.width > 0 && sample.height > 0) { + const key = `${program.id}:${sample.width}:${sample.height}:${sample.rgb.length}`; + let raster = rasterCache.get(key); + if (!raster) { + raster = document.createElement("canvas"); + raster.width = sample.width; raster.height = sample.height; + const pixels = new Uint8ClampedArray(sample.width * sample.height * 4); + for (let source = 0, target = 0; source < sample.rgb.length; source += 6, target += 4) { + pixels[target] = parseInt(sample.rgb.slice(source, source + 2), 16); + pixels[target + 1] = parseInt(sample.rgb.slice(source + 2, source + 4), 16); + pixels[target + 2] = parseInt(sample.rgb.slice(source + 4, source + 6), 16); + pixels[target + 3] = 255; + } + raster.getContext("2d").putImageData(new ImageData(pixels, sample.width, sample.height), 0, 0); + rasterCache.set(key, raster); + } + ctx.save(); ctx.globalAlpha = program.visualEcho ? .25 : .48; + ctx.imageSmoothingEnabled = false; + ctx.drawImage(raster, x, y, width, height); + ctx.restore(); + return; + } + const seed = [...String(program.id || "0")].reduce((sum, char) => (sum * 33 + char.charCodeAt(0)) >>> 0, 5381); + const descriptor = program.descriptor || []; + const rasterColumns = 16; + const rasterRows = 8; + const cellWidth = width / rasterColumns; + const cellHeight = height / rasterRows; + ctx.save(); + ctx.globalAlpha = program.visualEcho ? .19 : .28; + for (let row = 0; row < rasterRows; row += 1) { + for (let column = 0; column < rasterColumns; column += 1) { + const signal = (seed + column * 37 + row * 71 + Math.floor((descriptor[(column + row) % Math.max(1, descriptor.length)] || 0) * 997)) % 360; + ctx.fillStyle = `hsl(${signal} 82% ${18 + (signal % 29)}%)`; + ctx.fillRect(x + column * cellWidth, y + row * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight)); + } + } + ctx.restore(); +} + +function drawTile(program, index, layout, time) { + const col = index % layout.columns, row = Math.floor(index / layout.columns); + const x = col * layout.width, y = row * layout.height; + const pad = Math.max(3, Math.min(8, layout.width * .025)); + const selected = program.id === state.selected && !program.visualEcho; + const statusColor = palette[program.status] || "#64716b"; + const originColor = palette[program.origin] || "#b5c0b6"; + ctx.fillStyle = selected ? "#172329" : index % 2 ? "#091117" : "#071016"; + ctx.fillRect(x + 1, y + 1, layout.width - 2, layout.height - 2); + drawRasterBed(program, x + 2, y + 2, layout.width - 4, layout.height - 4, time); + ctx.strokeStyle = selected ? "#fff7c5" : `${statusColor}72`; + ctx.lineWidth = selected ? 3 : 1; + ctx.strokeRect(x + 1.5, y + 1.5, layout.width - 3, layout.height - 3); + + const font = Math.max(12, Math.min(22, layout.height * .11)); + ctx.font = `${selected ? "bold " : ""}${font}px "FiraCode Nerd Font Mono", "Courier New", monospace`; + ctx.textBaseline = "top"; + ctx.fillStyle = originColor; + ctx.fillText(program.visualEcho ? "↻" : program.status === "rejected" ? "×" : program.retained ? "●" : "·", x + pad, y + pad); + ctx.fillStyle = "#dce7dc"; + ctx.fillText(program.id || "unread", x + pad + font * 1.2, y + pad); + const machine = program.hardware || { label: "1X", resolution: 128 }; + const badge = `${machine.label || "1X"} ${machine.resolution || 128}²`; + ctx.font = `bold ${Math.max(10, font * .72)}px "FiraCode Nerd Font Mono", "Courier New", monospace`; + const badgeWidth = ctx.measureText(badge).width + pad * 1.5; + ctx.fillStyle = "#05080ddd"; ctx.fillRect(x + layout.width - badgeWidth - pad, y + pad, badgeWidth, font); + ctx.fillStyle = "#ffda68"; ctx.fillText(badge, x + layout.width - badgeWidth - pad / 2, y + pad + 1); + ctx.font = `${selected ? "bold " : ""}${font}px "FiraCode Nerd Font Mono", "Courier New", monospace`; + ctx.fillStyle = "#b7cfc4"; + const source = program.source || program.error || "(unreadable)"; + ctx.fillText(source.slice(0, Math.max(8, Math.floor((layout.width - pad * 2) / (font * .62)))), x + pad, y + pad + font * 1.35); + + if (program.status === "rejected" || !program.sample) { + ctx.fillStyle = "#cf6679"; + ctx.fillText((program.error || "rejected").slice(0, 38), x + pad, y + layout.height - pad - font); + return; + } + + const phase = (time * .00016 + index * .071) % 1; + const { values, active } = programValues(program, phase); + const graphTop = y + pad + font * 3; + const graphHeight = Math.max(8, layout.height - (graphTop - y) - pad - font * 1.2); + const cellWidth = Math.max(1, (layout.width - pad * 2) / Math.max(1, values.length)); + const max = Math.max(1, ...values); + values.forEach((value, valueIndex) => { + const intensity = value / max; + const hue = 120 + intensity * 240 + (program.descriptor?.[2] || 0) * 90 + index * 7; + ctx.fillStyle = `hsl(${hue % 360} 92% ${26 + intensity * 58}%)`; + const barHeight = Math.max(2, intensity * graphHeight); + ctx.fillRect(x + pad + valueIndex * cellWidth, graphTop + graphHeight - barHeight, Math.max(1, cellWidth - 1), barHeight); + }); + if (active) { + ctx.strokeStyle = active[0] === "c" ? "#ffda68" : active[0] === "s" ? "#ef82dc" : "#75cfde"; + ctx.lineWidth = 2; + for (const valueIndex of [active[1], active[2]]) { + if (valueIndex < 0) continue; + ctx.strokeRect(x + pad + valueIndex * cellWidth, graphTop, Math.max(2, cellWidth), graphHeight); + } + } + ctx.font = `${Math.max(10, font * .78)}px "FiraCode Nerd Font Mono", "Courier New", monospace`; + const health = state.runtime?.vm?.telemetry?.residents?.find((row) => row.id === program.id); + const analysis = state.runtime?.vm?.telemetry?.analysis?.find((row) => row.id === program.id); + ctx.fillStyle = statusColor; + const footer = `${program.visualEcho ? "ECHO " : ""}N ${Number(program.novelty || 0).toFixed(2)} Q ${Number(program.quality || 0).toFixed(2)} G${program.generation || 0}`; + ctx.fillText(footer, x + pad, y + layout.height - pad - font * .72); + if (health) { + const barX = x + pad, barY = y + layout.height - Math.max(4, pad * .55); + const barWidth = layout.width - pad * 2, barHeight = Math.max(3, pad * .32); + ctx.fillStyle = "#000000"; ctx.fillRect(barX, barY, barWidth, barHeight); + const healthGradient = ctx.createLinearGradient(barX, 0, barX + barWidth, 0); + healthGradient.addColorStop(0, "#e32636"); + healthGradient.addColorStop(.34, "#ff7a1a"); + healthGradient.addColorStop(.67, "#ffd84a"); + healthGradient.addColorStop(1, "#72e889"); + ctx.fillStyle = healthGradient; + ctx.fillRect(barX, barY, barWidth * Math.max(0, Math.min(100, health.hp)) / 100, barHeight); + if (analysis) { + ctx.fillStyle = "#ffffff"; + ctx.fillRect(barX + barWidth * Math.max(0, Math.min(100, analysis.cutoff)) / 100 - 1, barY - 2, 2, barHeight + 4); + } + } +} + +function draw(time = 0) { + const programs = boardPrograms(); + ctx.fillStyle = "#04080d"; + ctx.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight); + if (!programs.length) { + ctx.fillStyle = "#9acbb7"; + ctx.font = "24px monospace"; + ctx.fillText("awakening Lisp Soup…", 24, 24); + requestAnimationFrame(draw); + return; + } + const { field, layout } = fillField(programs); + field.forEach((program, index) => drawTile(program, index, layout, time)); + drawMembranes(field, layout); + requestAnimationFrame(draw); +} + +async function select(index) { + const programs = boardPrograms(); + if (!programs.length) return; + selectedIndex = (index + programs.length) % programs.length; + const id = programs[selectedIndex].id; + state.selected = id; + await fetch("/api/select", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ id }), + }); +} + +function navigate(dx, dy) { + select(selectedIndex + dx + dy * columns).catch(() => {}); +} + +window.addEventListener("keydown", (event) => { + if (event.key === "ArrowLeft" || event.key.toLowerCase() === "a") navigate(-1, 0); + if (event.key === "ArrowRight" || event.key.toLowerCase() === "d") navigate(1, 0); + if (event.key === "ArrowUp" || event.key.toLowerCase() === "w") navigate(0, -1); + if (event.key === "ArrowDown" || event.key.toLowerCase() === "s") navigate(0, 1); +}); + +function pollGamepad(time) { + const pad = navigator.getGamepads?.()[0]; + if (pad && time - lastMove > 220) { + const x = pad.axes[0] || 0, y = pad.axes[1] || 0; + if (Math.abs(x) > .6 || Math.abs(y) > .6) { + navigate(Math.abs(x) > .6 ? Math.sign(x) : 0, Math.abs(y) > .6 ? Math.sign(y) : 0); + lastMove = time; + } + } + requestAnimationFrame(pollGamepad); +} + +async function stream() { + const response = await fetch("/api/stream"); + if (!response.ok) throw new Error(`soup stream refused (${response.status})`); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let pending = ""; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + pending += decoder.decode(value, { stream: true }); + const lines = pending.split("\n"); + pending = lines.pop(); + for (const line of lines) { + if (!line) continue; + state = JSON.parse(line).state; + const programs = boardPrograms(); + selectedIndex = Math.max(0, programs.findIndex((program) => program.id === state.selected)); + const active = programs[selectedIndex]; + soupIteration.textContent = `ITER ${state.iteration.toLocaleString()}`; + soupProgram.textContent = active?.source || "(EMPTY SOUP)"; + soupCoverage.textContent = `${state.coverage} / ${state.capacity} NICHES`; + soupVerification.textContent = `${state.accepted.toLocaleString()} VERIFIED`; + description.textContent = `${programs.length} organisms in a fixed 4×3 field; ${state.coverage} archive cells; selected ${active?.source || "none"}.`; + } + } +} + +window.addEventListener("resize", fit); +fit(); +requestAnimationFrame(draw); +requestAnimationFrame(pollGamepad); +stream().catch((error) => { description.textContent = error.message; }); -- 2.51.2 From 6785b64b0168d3615dd519afa9f72a0375fe8706 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Fri, 24 Jul 2026 18:09:37 -0700 Subject: [PATCH 14/14] Record stopped Piecefarm handoff for Blueberry --- .../intelligent-terrarium/PIECEFARM-OPS.md | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/experiments/intelligent-terrarium/PIECEFARM-OPS.md b/experiments/intelligent-terrarium/PIECEFARM-OPS.md index 9c01d0fe2f..c73e886d3d 100644 --- a/experiments/intelligent-terrarium/PIECEFARM-OPS.md +++ b/experiments/intelligent-terrarium/PIECEFARM-OPS.md @@ -1,6 +1,6 @@ # Piecefarm live operations -Last verified: 2026-07-24 17:46 PDT +Last verified: 2026-07-24 18:03 PDT ## Current season @@ -9,11 +9,13 @@ Last verified: 2026-07-24 17:46 PDT - State root: `/home/me/.local/share/piecefarm/state` - Experiment: `/home/me/piecefarm/experiments/intelligent-terrarium` - API: loopback-only at `127.0.0.1:8788` -- Displays: two native SDL3/OpenGL panels at 2560×1440, approximately 142–143 FPS +- Displays: stopped; the last live run held two native SDL3/OpenGL panels at + 2560×1440 and approximately 139–144 FPS - Visible embodiment: twelve fixed-address RGB residents, sampled pixel-perfectly - Search population: heterogeneous 32×32, 64×64, 128×128, and 256×256 fields -- Audio: active stereo Piecefarm stream on the tower's ALC1220 analog Line Out -- Search cadence: 100 ms launch clock, eight CPU workers, 64-proposal look-ahead +- Audio: stopped; the last route was the tower's ALC1220 analog Line Out at 38% +- Search cadence when resumed: 100 ms launch clock, eight CPU workers, + 64-proposal look-ahead The launch clock and worker count are user-manager environment overrides (`PIECEFARM_CYCLE_MS=100`, `PIECEFARM_WORKERS=8`, @@ -21,19 +23,18 @@ The launch clock and worker count are user-manager environment overrides user-manager lifetime, but not necessarily a logout or reboot. The source defaults remain deliberately more conservative. -The service is running but deliberately **disabled** for login autostart. Do -not enable it or make the current launch/worker overrides permanent without a -keeper decision. +The service was explicitly stopped before handoff and is `inactive`. It remains +deliberately **disabled** for login autostart. Do not start it, enable it, or +make the current launch/worker overrides permanent without a keeper decision. The separate fleet status cockpit is closed; its login autostart entry is unchanged. ## Loopboy caretaker -- Canonical prox: `neo:tal#A2F2270F` (host machine `neo.local`) -- Agent: Codex on `neo.local`, advertised as `neo` by the fresh fleet ledger -- CWD: this experiment directory -- Private route: the existing Piecefarm `alex` Loopboy contact -- Delivery: isolated prox inbox; no Terminal, clipboard, pointer, or keyboard injection +There is no active Piecefarm caretaker at handoff. The final Neo replacement +exited before Neo's battery shutdown. A successor on Blueberry should use this +experiment directory, bind the existing private `alex` contact, and use the +isolated prox inbox—never Terminal, clipboard, pointer, or keyboard injection. The caretaker verifies service health, iteration movement, display assignment, display rate, and the actual PipeWire sink/port. It should re-enter @@ -48,20 +49,33 @@ The earlier `neo:tupas#758ACE5E` rock disappeared from Neo's fresh ledger after the client switched to a mobile network. Its first replacement inherited an unrelated stale session and was allowed to exit. At 17:57 PDT the genuinely fresh guarded `neo:tal#A2F2270F` session was launched in the same worktree and -bound to the same isolated `alex` Loopboy route; the autonomous jas-nzxt -service did not stop during either control-plane change. +bound to the same isolated `alex` Loopboy route. It later exited, and at 18:03 +PDT the jas-nzxt service was explicitly stopped and reset to clean `inactive` +state. + +## Blueberry pickup + +The complete source handoff is branch `agent/intelligent-terrarium`, commit +`e904bf8f5` or later, on the canonical Tangled `origin`. On Blueberry: + +```sh +git fetch origin agent/intelligent-terrarium +git switch --track origin/agent/intelligent-terrarium +cd experiments/intelligent-terrarium +npm test +``` + +Read this file and `SCORE.md` before touching the remote runtime. The saved farm +state and its autonomous Git history remain on jas-nzxt under the paths above; +the source branch intentionally does not contain that mutable state. ## Capacity reading -The desktop is near its shared compute capacity, but not its RAM capacity. -With the 64-proposal look-ahead live, Piecefarm uses roughly 5–7 logical cores -in Node plus 1.1–1.4 cores in the native display/VM. The independent miner uses -about two more CPU cores, so the observed system load is roughly 11 on 12 -logical CPUs. Piecefarm is about 1.12 GB current / 1.21 GB peak of its 2 GB -cgroup maximum and the host still has about 12 GiB available RAM. Sustained -two-display presentation is roughly 136–142 FPS. Under the co-resident miner, -mixed-resolution search completes roughly 38–45 evaluations/second, up from -20–25/second with the old eight-proposal barrier. +The final live snapshot used about 1.73 GB current / 1.89 GB peak of its 2 GB +cgroup maximum. Sustained two-display presentation was roughly 139–144 FPS. +The final mixed-resolution search snapshot reported 12.8 evaluations/second; +earlier windows reached 38–45/second, so a successor should treat throughput as +load-sensitive rather than quoting a single capacity number. The RTX 3070 is effectively saturated: the latest process sample attributed about 84% SM to `matador-miner` and 14% graphics work to Piecefarm. Piecefarm's OpenGL presentation remains smooth, -- 2.51.2