membrane
A text-based hypervector entity-field hippocampus for opencode that learns as you go, shares a Couchbase-backed team memory, and orchestrates LLM calls between local and cloud inference.
A Python lab. It runs as a local HTTP server on
127.0.0.1 in front of Ollama (or Anthropic) — technically a proxy, but that
is the mechanism, not the point. The point is the field.
Why that name? The project began as one question — how do you measure "variety" for a memory that decides absorb-a-cached-answer vs escalate-to-the-model? — and the answer is what makes the memory above possible and is why it is called membrane: a selective, active boundary between lived memory (the inside) and the incoming world (the outside) that absorbs what it has the variety for and escalates the rest. The full story lives in The thermodynamic principle and The math findings.
This README is the developer workflow — install, launch, connect a team field, and the daily loop. How the entity field works under the hood, how sharing works, and the project's history are in the sub-documents (see Documentation map).
Quick start
Membrane runs two ways. Both need an Ollama server with one chat model and one
embedding model (ollama pull qwen3:8b && ollama pull mxbai-embed-large, or any cloud model Ollama
serves), and 1–3 GB of RAM for the seat.
Standalone: its own web UI
pip install membrane-seat # or: uv tool install membrane-seat
mkdir ~/bob && cd ~/bob
membrane standalone # opens http://localhost:4096/ in your browser
You get membrane home: the field in 3D, the chat, and voice ("Hey Bob" — a new membrane is Bob
until you agree on a name together; it will ask). Other devices on your network reach it at
http://<this machine>:4096/. No login on the local network; a public or proxied caller is
refused. Voice needs a secure origin, so the microphone works on localhost (the first voice use
downloads ~65 MB of speech models, checked against their checksums).
Inside Home Assistant (HACS)
- HACS → ⋮ → Custom repositories →
https://github.com/chedim/membrane, category Integration. - Install Membrane, restart Home Assistant.
- Settings → Devices & services → Add integration → Membrane → Inside Home Assistant. If Home Assistant already has the Ollama integration, it is used; otherwise give Ollama's address. Pick the models.
Home Assistant then runs the seat as its own child process (started and stopped with HA,
restarted if it dies; its data in <config>/membrane/), opens on membrane home for everyone (HA's
sidebar folds away; ☰ leads to HA's own pages — switch it off in the integration's options), adds
the seat as an Assist conversation agent, and its field's readings as sensors.
What it can do in your house: by default the seat may use Home Assistant through a hidden, non-admin, local-only user ("Membrane") — read any state, call any service, and mirror the entities it chooses (they update live). It acts on what people ask, but its commands are written by a model: turn Let the seat use Home Assistant off in the integration's options if you'd rather it only talks. It can't change Home Assistant's configuration or users.
Connect to a seat running elsewhere is for a seat on another machine (the integration is then only its UI and agent).
Bugs and ideas
github.com/chedim/membrane/issues — your membrane knows this too: when it finds a bug in itself it drafts the issue and hands you the link to file.
Table of contents
- Quick start
- Installation
- Sharing a team field (Couchbase)
- Checking on the field
- Documentation map
- Development
- Status
Installation
Install the CLI (one line, no sudo)
curl -fsSL https://raw.githubusercontent.com/chedim/membrane/main/install.sh | bash
Installs the membrane CLI globally, user-local (~/.local/bin):
uv tool install if you have uv, otherwise a
managed venv at ~/.membrane/venv + a symlink on your PATH. Re-running it is
safe (upgrade in place). To install from a local checkout instead (e.g. while
hacking on membrane):
MEMBRANE_REPO=/path/to/membrane bash install.sh
Requires Python ≥ 3.10 (numpy is the only import-time dep); embeddings/chat come from a running
Ollama. Without installing anything, one-off via uvx:
uvx --from "membrane @ git+ssh://git@github.com/chedim/membrane" membrane install.
Dependencies, honestly:
- Python ≥ 3.10 + numpy — the only import-time dep.
couchbase(the shared field) are installed by default (paho-mqtt was removed 2026-10-03 — the mqtt event bus is retired; cross-seat transport is couch's peer_chat lane) (pip install -r requirements.txt/ the pyproject) but imported lazily; either missing degrades that feature, never the boot. - Ollama (
brew install ollama/ https://ollama.com) — NOT auto-installed; embeddings and local chat need it running on:11434. - Couchbase — where the field lives for real seats: one bucket per team,
the
membranescope, nine collections (events,sentences,terms,entity_states,entity_deltas,relations,claims,annotations,origin_stats), created and indexed on first boot (ensure_infra). With no cluster configured or reachable the seat degrades to a RAM field — it runs, and nothing persists across restarts. - opencode — NOT auto-installed either;
membrane opencodelaunches it if found on PATH (sh <(curl ... opencode.ai/install.sh)), otherwise the proxy runs bare and you point any OpenAI-compatible client athttp://127.0.0.1:11500/v1(--no-launchskips the spawn entirely). The other surfaces (viewer, review, ingest, codegraph) need nothing beyond Python. - The viewer ships inside the package (three.js + both pixel fonts are vendored,
served same-origin at
/viewer, opened in your browser on start;--no-viewer --no-opento disable) — no CDN, no node, no browser install.
First run in a project
cd ~/projects/my-app
membrane opencode
That's the whole workflow. On first run in a directory it bootstraps that project:
- auto-installs the eight agent skills (query / learn / forget / state /
global-field / codegraph / ingest / restart)
into
.opencode/skills/there (project-scoped on purpose — an opencode launched elsewhere never sees them; skip withMEMBRANE_NO_AUTOINSTALL=1), - opens an entity field for the project, namespaced by its origin (a sha1 tag of the project path) — events, entity states, relations, claims — all in the shared scope when one is configured,
- backfills what it can without cost: the codegraph (code events and
callsrelations) and, withmembrane backfill, the project's history, - if no memory sharing is configured, offers (once, interactive shell only) to connect a Couchbase cluster for this project — decline and it stays a private local field (see Sharing a team field).
It then starts the OpenAI-compatible proxy on :11500 (backend Ollama at
http://localhost:11434, override --backend/--model) and opens the
opencode TUI attached to membrane's own serve surface (stage 3.5:
membrane IS the server — sessions, turns, tool parts and their history live
in membrane's stores beside the field; the TUI is a thin client). The
surface's REST/SSE triple is printed at boot and the basic-auth pair lives
in .membrane/serve-auth.json. Every turn is captured into the field
(discourse and observations as events), entities move as the conversation
names them, and the model is grounded by what those entities hold —
docs/entity-field.md is the design doc.
Daily workflow
Inside opencode, the field answers five canonical frames itself — intercepts: no model call, never learned:
membrane: query the field for <topic> read recall
membrane: learn that <fact> write a memory (a NOTE event)
membrane: forget #N tombstone event #N and
recompute what it touched
field state memory health/stats
membrane: suggest review scope suggestions for
the cross-project commons
The agent may also emit these frames in its own prose; answers arrive next
turn as clearly-marked [membrane-intercept] blocks. Writes from model prose
execute by default (--no-agent-writes reverts to report-only): a learn runs
the same guards as any turn and is audited origin: agent (tagged
agent-note).
Everything else is ambient. Each turn the model sees what the field holds about the entities it names (its evidence sentences, the latest observations, the code, relations, conflicts) and — on later turns — only the diff: which entity states moved since the last message it processed, from conversation, code, other seats or entity scripts. Two safety lanes run on every turn and stay advisory:
- the gate finds the earlier ask nearest this one and its answer; when it
is not stale (no newer observation or conflict on the ask's entities), the
pair is logged as an A/B — the replay next to what the model actually
answered, judged for agreement.
--replay-ab Pserves the replay on a fraction P of eligible turns (default 0 = shadow: you are never bypassed); - the [membrane] monologue in the thinking channel shows the gate verdict,
field vitals, and the per-turn memory flow.
/monologuetoggles it.
Idle background loops run on a LOCAL cortex by default (--idle-cortex-model)
so a stuck background seat can never burn cloud tokens. Token economics and
routing: docs/token-economics.md.
Other surfaces
| Command | What it does |
|---|---|
membrane opencode |
the daily driver: seat proxy on :11500 + membrane's own serve surface (loopback) with the opencode TUI attached as a thin client — sessions, turns and tool parts live in membrane; opencode is display only (MEMBRANE_TUI_DIRECT=1 restores the old direct-provider lane) |
membrane serve |
the same seat, headless: membrane's OWN opencode-compatible REST/SSE server on :4096 (opencode replaced, no child process) |
membrane backfill |
rebuild the field from history (code first, then the audit log; idempotent, resumable) |
membrane review |
print what the field learned (annotations, replay A/B with --ab) |
membrane connect |
(re)run shared-field onboarding for THIS project |
membrane reset |
kill the seat on :11500 (or --listen), relaunch membrane opencode fresh |
membrane install |
install .opencode/skills/* explicitly (+ .gitignore entry); prints the AGENTS.md blurb |
membrane auth |
store a cloud API key (anthropic, openrouter, gemini, groq, github, mistral) in ~/.membrane/auth.json (600) — its models become routable rungs + picker entries |
membrane move |
migrate the field between stores (--efield: copy the entity-field collections between clusters) |
membrane migrate |
interactive migration front door: detects where the field lives, asks for the target, runs move |
membrane reembed |
move the field to another embedding model (--efield --embed-model M: re-embed every live event, re-apply) |
membrane update |
check the install source, show what changed, apply (git pull --ff-only) and re-exec |
membrane ingest |
cold start: absorb docs/README into the field (DOC events; a second ingest is a no-op) |
membrane codegraph |
stdlib-AST call graph (stats / impact / callers / callees) |
membrane doctor |
what's installed/running (CLI, skills, Ollama) + the field's vitals |
membrane tui / proxy / anthropic / demo |
RETIRED — print a pointer to membrane opencode |
./run.sh still works in-repo and delegates to the same CLI (bare = opencode;
./run.sh restart = membrane reset).
MEMBRANE_EFIELD selects the field's mode: on (the default: capture,
embed, apply), shadow (capture events only — a lab ablation), off.
Standalone and Home Assistant
membrane standalone is the seat with its own web UI (membrane home, the chat,
voice) — see docs/standalone.md. Inside Home Assistant the
Membrane integration (HACS) runs the seat as HA's child process and makes
membrane home HA's landing page — see docs/home-assistant.md.
Serving headless (membrane serve)
membrane serve runs the seat with no TUI and no opencode at all —
membrane hosts its OWN opencode-compatible REST/SSE server
(membrane/server.py): sessions, messages, turns, tool parts, aborts, the
SSE event stream. Any opencode client connects unchanged: the mobile
companion app, IDE plugins, a script. The turn
loop calls the seat's OWN /v1 on loopback — gate, injection, capture,
banner and the routing ladder run through the exact code path every other
client's turn uses, so the serve surface adds no separate memory, and the
model's membrane tool invocations (the MCP lane, built-ins all off) appear
to the client as ordinary tool parts riding message.part.updated
(pending → completed), so you can watch what it's doing from the room.
The connection triple is printed at boot:
serve: http://<lan-ip>:4096 (native serve, basic auth user opencode)
- Auth is basic auth (opencode's env names,
OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD): no/incorrect credentials get401, only the pair in.membrane/serve-auth.json(mode 600, generated once,--serve-pass/--serve-userto override) gets through. Credentials survive restarts, so a device's saved connection keeps working. - Sessions and messages persist beside the field
(
serve-sessions.json/serve-msgs.json, atomic write-through), so a device's history survives a seat restart.--serve+--no-launchis legal now — the surface is a thread, independent of the launch decision. - The TUI is a client too. Every launch boot starts the serve surface
(loopback-only unless
--serve) and opens the opencode TUI attached to it (opencode attach, same basic-auth pair) — so a desktop session and a phone session are the same store: membrane's. The TUI's own turn loop, tool loop and local session storage are bypassed entirely; the attached client only displays what the server publishes. Old lanes:MEMBRANE_TUI_DIRECT=1(TUI fronting/v1directly) andMEMBRANE_LEGACY_SERVE=1(theopencode servechild). - Exposure is WiFi-only for now (
--serve-hostdefaults to0.0.0.0, the LAN): reach the server only from a network you own. The planned route makes the seat's phone (the boat's agDroid HA server) the host — the security model (VPN or LAN-only, no public exposure) gets designed there. --serve-port(default 4096, walks up if busy) and--listen(the proxy, still never leaves 127.0.0.1) are independent.- The minds are visible.
/agents(orGET /agent) lists the ambient minds as agents: the three thinking-loop workers (loop-1..3, each one's description carrying its live claim — the entity it is processing — and the event queue's depth) plusthinkeranddreamerwith their real counters (beats/spoken/ticks, waves/committed/gc). The list is served fresh every call from the running process — nothing cached, nothing faked. - The loop queue is RAM-only and dies with the seat. Watch events
waiting for a thinking loop live in memory (no persistence): a seat
restart drops the pending queue on purpose — stale reactive work
outliving its justification is a failure mode, and a clean restart is
the correct response. If you need an event to survive a restart, it
belongs in the field (
membrane: learn that ...), not in the queue. - The old lane survives as an ablation:
MEMBRANE_LEGACY_SERVE=1restores the previous design (launch anopencode servechild). A failed serve bind prints loudly and the seat keeps running without the room-facing surface — never a dead seat over a surface.
Claude Code (Anthropic cloud) — RETIRED
The old ./run.sh anthropic / --anthropic observer mode (a proxy on
:11435 in front of Claude Code) was cut with the proxy — membrane anthropic now only prints the retirement pointer. The opencode seat is the
surface; to route Anthropic models through it, membrane auth stores the
key (~/.membrane/auth.json, mode 600) and its models become rungs on the
router's ladder.
Sharing a team field (Couchbase)
By default a seat's field is private (RAM, or a local cluster). To make a team
share one substrate — same project, different machines, one memory — point
every seat at the same Couchbase bucket: one bucket per team, the
membrane scope, the nine entity-field collections; projects distinguish
themselves by the origin axis (a seat writes ONLY rows keyed by its own
origin — do NOT add per-project buckets, that would defeat cross-seat
agreement).
membrane connect (recommended)
Run in the project directory; answer four prompts:
membrane connect
# conn string [couchbase://127.0.0.1]
# user [Administrator]
# password (hidden)
# bucket [membrane]
The same offer appears automatically on the first membrane opencode /
membrane reset in a project that has no store configured (interactive shell
only; declining is remembered — it becomes a private field, and membrane connect re-opens the question).
Secrets never touch the repo. Credentials are stored per-project OUTSIDE
it, at ~/.membrane/projects/<slug>.env (mode 600, slug = project path +
hash), and are exported into the environment at every launch from that
directory. A .env inside the repo would leak on the first git add . —
that failure mode is designed out.
Kill switch: MEMBRANE_NO_ASK=1 silences the launch-time prompt (an explicit
membrane connect still works, as does plain env config).
Manual setup via env
The classic path still works (cortex-experiment's env convention):
export MEMBRANE_STORE=couchbase
export COUCHBASE_CONN_STRING=couchbase://<team-host>
export COUCHBASE_USER=... COUCHBASE_PASSWORD=... COUCHBASE_BUCKET=membrane
A local cluster answering at the default endpoint is auto-detected at boot — no config needed on a dev box that already runs one. Unreachable cluster → transparent fallback to the RAM field (degradation, never failure).
What sharing guarantees
Mechanics that make sharing collision-free (details in docs/multi-dev-field.md and docs/entity-field.md):
- every event, state, relation and claim is namespaced by origin (the seat-id where it was born); writes only ever touch own keys; foreign rows are read-only inputs to merged reads — a foreign entity's effective state is the seed plus the mass-weighted mean of every seat's displacement;
- two seats that moved the same entity in opposing directions produce a divergence — surfaced as a cross-seat conflict in both blocks, never silently overwritten (nothing is destroyed);
- a seat's working set includes foreign states near what the session is
talking about (ANN) plus the commons: claims the same way from two or
more seats (promoted at night;
disputedwhen a seat contradicted it); - teammates online get live peer asks (bounded fan-out, LAN/VPN);
peers: N liveat boot.
The full team process — day-one setup checklist, the async work loop across time zones, the watch layer, operator rules — is docs/shared-field-workflow.md.
Checking on the field
field state(in opencode) /membrane state/membrane doctor— the field's vitals: mode and backend, live events by stream, entities by tier (nursery/live/dormant), geometry (effective rank, free energy, the conflict fraction), replay A/B ledger, the last night pass.membrane review(--abfor the replay pairs) — what the field learned.- The MCP
statetool exposes the same payload to the agent.
What the numbers mean and how the field's "thermodynamics" work: docs/principle.md (plain words) and docs/math-findings.md (the measured math), with docs/entity-field.md connecting both to the code.
Lifecycle: updates, restarts, compatibility
- Update check at boot. On
membrane opencode/membrane reset, membrane checks its install source (the git checkout it runs from, or the pip/uv direct URL it was installed from). If an update exists it prints the CHANGELOG (the commits between you and the remote tip) and asksUpdate membrane now? [y/N]in the terminal — answering yes pulls (--ff-only, a dirty tree is refused) and re-execs with the same argv. Non-interactive sessions are never interrupted;membrane updateruns the same path manually. Opt out withMEMBRANE_NO_UPDATE_CHECK=1. A PyPI install (membrane-seat) compares itself with PyPI's latest and asks the same way (thenpip install --upgradeand a restart). Inside Home Assistant, updates come through HACS (the integration pins itsmembrane-seat). - Restart only from the shell.
./run.sh restart(=membrane reset: kills the seat listening on :11500 — or--listen— and relaunchesmembrane opencode) is the safe procedure — see the membrane-restart skill. Membrane never restarts itself while running (no file watcher, no self-re-exec); a restart loses nothing: the field re-hydrates from its backend (membrane reembed --efieldis the one way a restart CAN change results — moving to another embedding model). - Embedding-model moves. The field's vectors are pinned to a model
(
termsseeds are cached per model);membrane reembed --efield --embed-model Mre-embeds every live event and re-applies the log in order — the rebuilt field pins equal to a fresh one.
Documentation map
| Doc | What's in it |
|---|---|
| docs/standalone.md | Standalone mode (membrane standalone): the web UI, the first run and the name, who can reach it, voice, settings.json, updates |
| docs/home-assistant.md | Membrane in Home Assistant (HACS): setup, the UI takeover, the Assist agent and sensors, what the seat may do in the house, options, where its data lives, its life |
| docs/entity-field.md | THE FIELD. Design: events → entities → readers; the update rule and statistics; relations and claims; injection (full block / state diff); the gate + replay A/B; storage, sharing, the background mind; the e17–e25 evidence. The division of hands (the operator/listener/plan tool lanes + the remounted intent lane) |
| docs/shared-field-workflow.md | Team field: the process. Day-one setup, the async loop, watches, operator rules, go-live checklist |
| docs/multi-dev-field.md | Team field: the substrate. The origin axis, disjoint writers / merged reads, divergence conflicts, the commons — what makes multi-seat sharing collision-free |
| docs/global-field.md | The global commons: facts true beyond one repo; operator promotion, consensus, residency |
| docs/background-mind.md | The background mind: thinker + dreamer on events, the NightClock, births/junk/activity on entity stats, entity scripts and tracks, the intent steering wheel, the pause (the operator's stable point: what stops, what keeps), kill switches |
| docs/vsa-token-lane.md | Entity keys + relations (the VSA lane). eids.py canonical ids, entity_key/role_key binding, the relations composites, structure-awareness numbers, consumers, kill switches |
| docs/token-economics.md | Where the savings come from (intercepts, the replay lane, the state diff) + the router: the ladder, the ceiling, the outage fallback, the free-tier default budget |
| docs/monologue.md | The [membrane] thinking-channel banner: every line explained, the memory flow (gave/learned), /monologue toggle |
| docs/symbol-birth.md | The symbol crystallization: how the hierarchy is born — symbols come into being through language; the structural membrane mapping, the consequence to keep |
| docs/principle.md | "Only variety absorbs variety" — Ashby's law with the telos flipped; effective rank and free energy measured on entity states; the conflict tax |
| docs/math-findings.md | The measured math: the substrate-phase experiments (cosine primary, novelty specialist) and e18–e25 (spans, streams, coherence, relations, claims) |
| docs/ab-critic-scorecard.md | The A/B scorecard vs a plain cosine cache — six criticisms faced honestly |
| docs/fork-port-notes.md | Fork notes (the fork now runs its own memory design; what it took to port membrane's ideas there) |
the cycled / endless environment (sibling cortex-experiment repo) |
The theory notes behind the principle (the loop, and the trap unbounded variety sets) |
Retired with the exemplar field (kept in git history, removed from docs/):
close-the-gap.md (TencentDB-gap work, superseded by the code-evidence
lane), post-restart-plan.md (the pre-rewrite roadmap), exploration-game.md
and dev-organism.md (the toy world and the cell organism — cut in
070af25).
Development
Source layout: membrane/ (the field: streams → efield/efstore →
entity_block/efgate/night, the seat proxy, the native serve server
(server.py, opencode-compatible), the opencode host (the legacy ablation),
the CLI), experiments/ (the e-series rigs behind math-findings), tests/
(unittest + pytest, plain-python runnable).
Run the tests
python -m unittest discover -s tests # or: .venv/bin/python -m unittest discover -s tests
python -m pytest tests/test_global_field.py tests/test_lab_isolation.py # the field-backed ones
No server needed: everything runs against in-process fakes
(tests/_ef_fakes.py — a sha256-seeded embedder and a lazy fake cluster that
runs exactly the SQL++ the field emits).
The variety toy
The original variety experiment (what makes a store confident vs unsure — the gate's calibration in one command):
pip install -r requirements.txt # numpy only
python experiments/variety_toy.py
Expected: the low-variety store has low effective-rank and low spread; a query off its manifold reads as highly novel (gate escalates), while the same query against the high-variety store reads as in-manifold (gate acts).
Status
Working lab, in daily use on its own development (dogfooded: this repo's own
field learned the codebase while it was being written — the backfilled field
answers membrane state with ~14k live entities, and its background mind
digests, dreams, and speaks about its own work). Version 0.2.0b1 — the first beta-testing release; API and flag surfaces may still move. Private project — no
license yet; all rights reserved by the author.
Metadata
Release files for membrane-seat 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| membrane_seat-0.3.1.tar.gz | 3.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| membrane_seat-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 7.5 MB
Release files / membrane_seat-0.3.1.tar.gz
| Download URL | membrane_seat-0.3.1.tar.gz |
|---|---|
| Size | 3.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ace123007c7327d1788c021bb8409617d432319ba12097e64c3301ea29598c7c
|
|
BLAKE2b-256 checksum How to use checksums |
4ace46d0c5e83348215147b1596b745ed4a2f2bae919855b8658d263ee6a6eaa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / membrane_seat-0.3.1-py3-none-any.whl
| Download URL | membrane_seat-0.3.1-py3-none-any.whl |
|---|---|
| Size | 3.6 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2f9b36af877d202b3f3a174906a5e5cf09b6c41442ba21191a54daaacf495cef
|
|
BLAKE2b-256 checksum How to use checksums |
0d8a78595da08bb7b299311baa2bcb2c9940134695e86e76846aa6b7d1b45fcd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|