Skip to content

Repository files navigation

Charm Network

A local companion architecture: two language models, a small amount of external state, and the claim that the second is what makes the first continuous.

A language model is frozen at inference. Its weights do not change, and its activations vanish when the response ends. Whatever continuity a chat system appears to have lives in exactly one place — the transcript fed back in next time — which has a hard corollary: nothing happens between messages. Ten seconds of silence and ten days of silence produce identical futures.

This project does not try to fix that inside the model. It builds three kinds of state beside it — filters, reservoirs, and records — that advance on wall-clock time, whether or not anyone is talking, and that can be inspected line by line.

Everything here runs locally. There is no hosted service and no telemetry.


What it actually does

Affect-state modeling. A second model reads each turn as data and emits a five-axis appraisal — pleasure, arousal, dominance (PAD), plus affiliation and surprise. That vector drives a bank of exponential moving averages at three timescales (momentary, trajectory, baseline), after Varela's nested-timescale account of lived time. Direction changes are hysteresis-gated, so one sour turn moves the moment but a run of them is needed to bend the trend.

Wall-clock continuity. A heartbeat advances the state through silence: mean reversion toward baseline plus a small Gaussian term — an Ornstein–Uhlenbeck process, so excursions are bounded by construction rather than by hope. Relationship-directed warmth decays four times slower and takes no noise at all.

A reservoir with intrinsic dynamics. A 96-unit echo state network (undercurrent.py, ~200 lines, no dependencies) runs on a ten-minute beat with circadian, presence, and appraisal inputs across three leak-rate groups. Its recurrent matrix is random and fixed forever at spectral radius 0.9; only a linear readout learns, by LMS, with one job — predict the next appraisal. Prediction error becomes a surprise signal computed by the organ rather than judged by a model, and reservoir energy modulates the noise term above.

Offline consolidation. Idle time produces journal pages; deeper idle produces a reflection pass over recent memories. A separate distiller maintains a short first-person self-narrative. Retrieval scores memories by semantic similarity, mood congruence, and similarity to an identity vector folded from what the character has deliberately kept.

Relationship-scoped state that outlives the window. A bond between a character and one party holds a foundation: text injected unconditionally on every turn with that party, so it survives resets, context eviction, and cold starts. Nothing triggers it and nothing can evict it. This is the unglamorous half, and it is the half that runs constantly.

A fixed-probe longitudinal record. The same bond holds a return phrase — an exact-match trigger, deterministic and human-timed, as against the semantic retrieval used everywhere else. Speaking it produces a short verse, appended as an immutable growth ring carrying the affect state it was written under. Because the probe is constant, variation across rings is attributable to the system rather than to the wording; the rings are read back as a series, each annotated with the per-axis move since the one before it and the interval between them.

Append-only records with provenance. The transcript, retained memories, journal, and the shared world are all append-only. Facts carry who asserted them and when. Nothing is deleted — only retracted, and the retraction is itself a recorded event.

Claim/act consistency checking. This is the piece most worth stealing. Local models given tools will narrate tool use instead of performing it, fluently and falsely. So every real act is logged; after each reply, guards check the reply's claims against the turn's acts; a reply still claiming an act that never ran is regenerated, and if the false claim survives that, the reply is withdrawn — shown to the human with a warning, but replaced in every context any model will ever see by a stub naming what was claimed. False claims are quarantined from the model's own future, because a false claim in the context window out-votes any correction placed beside it.

A shared world. A separate process (world_server.py) serves a semantic scene graph — subject/predicate/object over typed entities — so a character can be brought to a place rather than only spoken to in one. Acts in the world cross the same approval handshake as every other capability.


What is not being claimed

Stated structurally, because it matters:

  1. No sentience, no qualia, no suffering claims — not in the system, its prompts, or its documentation. The one claim the architecture makes, because it is checkably true: between conversations, something of the character keeps happening, it leaves a trace, and it wakes into it.
  2. The components are textbook. Echo state networks are from 2001; EMAs, Ornstein–Uhlenbeck processes, LMS, and PAD are older. What is contributed here is the composition and the record-keeping discipline.
  3. Graded under Integrated Information Theory, this system's Φ is approximately zero — and every choice made for auditability is a choice against irreducibility. That trade was made deliberately, in favor of the record.

Requirements

  • Python 3 (developed on 3.14; no version-specific syntax, but no lower bound has been tested)
  • Node 18+ (frontend tests only)
  • An OpenAI-shaped chat endpoint. Developed against LM Studio serving two local models; openai and openrouter are selectable in Settings, and an Anthropic adapter is not written yet.

Two models are expected: one that speaks, one used only as a measurement instrument. They can be the same weights served twice, but the appraisal model is never addressed by anyone and never speaks into the conversation.

Running it

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

# Start your chat endpoint first (LM Studio, default http://localhost:1234)
.venv/bin/python server.py          # → http://127.0.0.1:5001

Pick the two models in the web UI on first run. To serve the shared world as well, run it alongside in a second terminal:

.venv/bin/python world_server.py    # → http://127.0.0.1:5002

Then connect to it from Settings → World.

Configuration

All optional; defaults in parentheses.

Variable Meaning
LM_STUDIO_URL Chat endpoint (http://localhost:1234)
CHARMNET_HOST / CHARMNET_PORT Bind address (127.0.0.1 / 5001)
CHARMNET_WORLD World file path (charmnet.charm)
CHARMNET_EMBED_MODEL Embedding model for recall (endpoint default)
CHARMNET_CONTEXT_TOKENS Cap the conscious context window
CHARMNET_JOURNAL_MINUTES Idle time before a journal page (30, 0 = off)
CHARMNET_SLEEP_MINUTES Idle time before a reflection pass (120, 0 = off)
CHARMNET_STABLE_PREFIX Move volatile blocks after history for cache reuse
WORLD_PATH / WORLD_PORT World server file and port (demo.world / 5002)

Tests

./run_tests.sh          # 899 Python + 142 frontend, no network required
./run_tests.sh smoke    # fast subset, for pre-commit

The script requires .venv/bin/python and refuses to run without it, rather than falling through to an interpreter whose failures mean nothing.


Your data

A world file (*.charm) is SQLite: characters, placements, memories, transcripts, and the append-only logs. It is user data, never a build artifact, and it is gitignored along with *.charmpack and *.world so that git add . cannot put someone's conversations on a remote.

Characters move between world files as packs:

.venv/bin/python charmpack.py list   --world w.charm
.venv/bin/python charmpack.py export --world w.charm --character Rowan --out rowan.charmpack
.venv/bin/python charmpack.py import --world other.charm --in rowan.charmpack

--asset-only exports the character alone — identity and temperament, with nothing that happened to it — which is the form to hand someone else. Embedding vectors travel with the name of the model that made them and are never silently reused across models.


Layout

File What lives there
server.py Orchestration, prompt assembly, the turn pipeline, HTTP API
memory.py Schema, storage, recall, consolidation
charm_network.py The three-timescale filter bank and hysteresis
undercurrent.py The echo state reservoir and its readout
entropy.py Noise source: kernel by default, hardware RNG optional
portability.py / charmpack.py Pack format and its CLI
world_server.py / world_store.py The shared world and its quad store
static/index.html The whole frontend
tests/ 21 modules, no network required

Design notes

  • ROADMAP.md — what is built, what is not, and the reasoning where it is not obvious.

Beyond that, the reasoning lives in the code. Module docstrings carry the argument for each component, and the comments explain choices rather than restate the line beneath them — charm_network.py on why state is described in bands of prose rather than figures, undercurrent.py on why the recurrent weights never learn, memory.py on what a growth ring is for.

Status and license

Research software, developed in the open. Interfaces change without notice and there is no support commitment.

Released under the MIT License. Use it, fork it, ship it — the claim/act consistency pattern in particular is meant to be taken.

(c) 2026 Caitlyn Meeks - caity.wtf

About

A charm network scaffold: filters, reservoirs and records giving a frozen LLM a between. Wall-clock affect state, an echo state reservoir, and append-only records with claim/act quarantine.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages