membrane
A living entity-field memory for a resident AI seat — hypervectors with a readable geometry, a background mind that works while you're away, every write guarded and audited, shared across seats by construction. Local-first; it runs in front of Ollama (or inside Home Assistant).
A Python lab. It runs as a local HTTP server on 127.0.0.1 — 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 measured underpinnings are in The math findings.
What separates membrane from other agentic memory on GitHub
Most memory projects give your agent a store to query. Membrane is built on different ground rules; each one is checkable in the repo.
1. The memory has a shape, not just a bag of chunks
The field is not cosine-over-pasted-summaries. It is an entity field over
hypervectors with a geometry you can read and measure: entities with positions
and states, relations between them, tiers of life (nursery → live → dormant),
effective rank, free energy, a conflict fraction. membrane state prints the
geometry of a running field; experiments/variety_toy.py
shows in one command why a low-variety store reads its outliers as novel and a
high-variety store doesn't (the absorb-vs-escalate decision is measured, not
vibes); docs/math-findings.md carries the experiments.
2. It is a seat, not a decorator
You don't bolt it onto an app — the memory is the resident process: its own
REST/SSE serve surface, its own chat UI, voice, the Home Assistant integration.
The turn loop runs inside the seat, so every client's turn goes through the
same field path; conversation transcript and memory are the same store, not two
systems reconciling each other (see membrane serve, below). While nobody is
talking, a background mind keeps working the field — a thinker digesting event
streams, a dreamer consolidating at night, watchers, entity scripts — and its
minds are visible as live agents at GET /agent (counters served fresh from
the running process, nothing cached, nothing faked). The repo dogfoods it: its
own field holds ~14k live entities and learned the codebase while it was
written.
3. Structure crystallizes; you curate nothing
No schema to define, no tags to assign, no knowledge graph to hand-build. Entities are born when experience names them, their states move as the world speaks, relations and claims accumulate, symbols crystallize through language (docs/symbol-birth.md). When two seats assert opposite things about the same entity, the divergence is surfaced, never silently overwritten — nothing in a teammate's field is destroyed by yours.
4. Every write stands trial
A memory that lets the model whisper to itself unsupervised drifts into
self-flattery. Membrane's writes run the same guards a turn runs: dedup,
variety gate, length limits, fenced-code refusal, junk cut — every write
audited with its origin (agent / mcp / night / operator) in
audit.jsonl, and every claim traces back to the events that earned it. A
dedicated lane replays earlier gate-verdicts against what the model actually
answered and judges agreement — shadow by default (--replay-ab P=0:
recorded, never bypassing you), measured at 0.77 mean similarity on a live
seat (docs/ab-critic-scorecard.md faces the six
criticisms honestly).
5. Multi-seat sharing is a property of the data model, not a feature
Every event, state, relation and claim is namespaced by the seat (origin) that
wrote it — writes are disjoint, reads are merged, so one team bucket is
collision-free by construction: a foreign entity's effective state is the seed
plus the mass-weighted mean of every seat's displacement, cross-seat agreement
is promoted into the commons at night, and disputed claims get marked
disputed, not overwritten. Live peer asks (LAN/VPN, bounded fan-out) let a
seat consult a sibling's field mid-turn. The mechanics:
docs/multi-dev-field.md; the team process:
docs/shared-field-workflow.md.
6. Local-first, and it degrades honestly
No SaaS, no account, no telemetry, no cloud round-trip required: chat and
embeddings run on your Ollama (cloud models are routable rungs you add with
membrane auth); the substrate is your Couchbase bucket — and with no cluster
reachable the seat drops to a RAM field and still boots (degradation,
never failure). The whole field is a substrate you own and can move
(membrane move / reembed). The memory is for your data: the private-use
license covers personal use and keeps it out of paid products
(LICENSE).
How it maps to the patterns you've already seen
| The common pattern | What membrane does instead |
|---|---|
| Vector store + retriever: paste docs, query chunks | Events with provenance; recall returns nearest past situations-and-actions and the named entities' held evidence, not loose chunks |
| Transcript summarizers / rolling history | The full transcript is never replayed: each turn carries a since-your-last-message diff of what actually moved |
| Hand-curated knowledge graphs | No hand work: entities, relations, claims crystallize from experience; structure emerges, and every element is auditable back to events |
| In-framework memory (your agent runtime keeps it) | The seat is the runtime — sessions, turns and tool parts live in membrane's store; memory and history can't fork |
| Hosted memory APIs (account, endpoint, egress) | Your bucket or your disk; offline mode keeps the seat alive with no cloud at all |
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).
The daily work: typed tools, guarded writes
A seat hands its model the membrane MCP surface — query, state, learn,
forget, the entity-hand tools (entity_add/edit/remove/link/inherit,
entity_query, entity_tracks, entities_related …), intend, peer_post,
graph. The model reads the field with query/state, files an earned
lesson with learn (same guards as any turn), cleans its own mistakes with
forget — writes are audited with their origin. Meanwhile the ambient layer
needs no command at all: every turn the model sees what the field holds about
the entities it names, and on later turns only the diff — which entity
states moved since the last message it processed, whether the change came
from conversation, code, other seats or entity scripts. Two safety lanes run
on every turn and stay advisory: the gate replays the nearest past ask
against the live answer (A/B, shadow by default), and the [membrane]
monologue in the thinking channel shows the gate verdict, field vitals and
the per-turn memory flow (/monologue toggles it).
Idle background loops run on a LOCAL, cheaper model by default
(--idle-cortex-model) so a stuck background seat can never burn cloud
tokens. Token economics and routing:
docs/token-economics.md.
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.
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
Dependencies, honestly:
- Python ≥ 3.10 + numpy — the only import-time dep (
couchbaseinstalled by default, imported lazily; a missing cluster 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. - opencode — optional;
membrane opencodelaunches it if found on PATH, otherwise point any OpenAI-compatible client athttp://127.0.0.1:11500/v1. - The viewer ships inside the package (three.js + both pixel fonts are vendored, served same-origin) — no CDN, no node, no browser install.
First run in a project
cd ~/projects/my-app
membrane opencode
On first run in a directory it bootstraps that project: auto-installs the
agent skills into .opencode/skills/ there (project-scoped on purpose), opens
an entity field namespaced by the project's origin, and backfills what it can
without cost (the codegraph; with membrane backfill, the project's history).
If no memory sharing is configured it offers (once, interactive shell only) to
connect a Couchbase cluster — decline and it stays a private local field.
It 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: sessions, turns,
tool parts and their history live in membrane's stores beside the field — the
TUI is a thin client. 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.
Surfaces
| Command | What it does |
|---|---|
membrane opencode |
the daily driver: proxy on :11500 + membrane's own serve surface (loopback) with the opencode TUI attached as a thin client (MEMBRANE_TUI_DIRECT=1 restores the old direct-provider lane) |
membrane serve |
the same seat, headless: membrane's OWN opencode-compatible REST/SSE server (opencode replaced, no child process) |
membrane standalone |
the seat with its own web UI — membrane home (the field in 3D), chat, voice; see docs/standalone.md |
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) |
membrane auth |
store a cloud API key in ~/.membrane/auth.json (600) — its models become routable rungs |
membrane move / membrane migrate |
migrate the field between stores |
membrane reembed |
move the field to another embedding model (re-embeds every live event, re-applies the log in order) |
membrane update |
check the install source, show what changed, apply 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 + the field's vitals |
MEMBRANE_EFIELD selects the field's mode: on (the default: capture,
embed, apply), shadow (capture events only — a lab ablation), off.
Headless serving (membrane serve)
membrane serve runs the seat with no TUI and no opencode at all —
sessions, messages, turns, tool parts, aborts, the SSE event stream, served by
membrane itself on :4096 (basic auth, the pair in .membrane/serve-auth.json,
credentials survive restarts). 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 and
routing run through the exact code path every other client's turn uses, so
the serve surface adds no separate memory, and the model's tool invocations
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.
--serve-port (default 4096, walks up if busy) and --listen (the proxy,
never leaves 127.0.0.1) are independent. Exposure is LAN-only by design:
reach the server only from a network you own.
- The minds are visible.
/agentslists the ambient minds as agents — the three thinking-loop workers (each with its live claim and the event queue's depth) plusthinkeranddreamerwith their real counters. - The loop queue is RAM-only and dies with the seat — a seat restart
drops pending reactive work on purpose; stale reactions must not outlive
their justification. What must survive a restart belongs in the field
(
learn), not in the queue.
Sharing a team field (Couchbase)
Point every seat at the same Couchbase bucket: one bucket per team, the
membrane scope, the nine entity-field collections (events, sentences,
terms, entity_states, entity_deltas, relations, claims,
annotations, origin_stats), created and indexed on first boot. 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 # conn string, user, password (hidden), bucket
The same offer appears automatically on the first run in a project with no
store configured (interactive shell only; declining is remembered). Secrets
never touch the repo — credentials are stored per-project OUTSIDE it, at
~/.membrane/projects/<slug>.env (mode 600), and a .env inside the repo is
designed-out of the failure mode. Plain env config still works:
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; an unreachable cluster transparently falls back to the RAM field. What sharing guarantees — disjoint writes / merged reads, divergence surfaced, the commons, live peer asks — is in docs/multi-dev-field.md, and the full team process in docs/shared-field-workflow.md.
Measured, not promised
- Token economics, live seat (2026-10-12): 5,736 live entities over 4,949 streamed events; the standing per-turn header ~3 KB (entity tracks, one line each) instead of state dumps; per-turn injection is the state diff, not the transcript; a recall of a few hundred tokens replaces re-reading a 4,000-line log. docs/token-economics.md
- The gate is calibrated by experiment: cosine-primary similarity with a novelty specialist as the escalation judge; the variety toy reproduces the decision in one command. docs/math-findings.md, docs/ab-critic-scorecard.md (replay A/B agreement 0.77 on a live seat, six criticisms faced)
- The field's own state is readable:
membrane state/membrane doctorprint live events by stream, entities by tier, geometry (effective rank, free energy, conflict fraction), the replay ledger, the last night pass.
Lifecycle
- Update check at boot (opt out
MEMBRANE_NO_UPDATE_CHECK=1): the CLI compares against its install source (git or PyPI), prints the CHANGELOG delta, and asks before pulling (--ff-only; a dirty tree is refused). Inside Home Assistant, updates come through HACS. - Restart only from the shell (
membrane reset/./run.sh restart). Membrane never restarts itself while running, and a restart loses nothing: the field re-hydrates from its backend. The one way a restart CAN change results ismembrane reembed(moving to another embedding model). - Field moves are first-class:
membrane movebetween stores,membrane migrateas the interactive front door,membrane reembed --efield --embed-model Mpins the rebuilt field equal to a fresh one.
Documentation map
| Doc | What's in it |
|---|---|
| docs/standalone.md | Standalone mode: 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 |
| docs/entity-field.md | THE FIELD. Events → entities → readers; the update rule; relations and claims; injection (full block / state diff); the gate + replay A/B; storage, sharing, the background mind; the operator/listener/plan tool lanes + the intent lane |
| docs/seat-first-2-tier.md | The seat-first two-tier field: local authoritative store, a published global tier on your terms |
| 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 |
| 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, the NightClock, entity scripts and tracks, the intent steering wheel, the pause, kill switches |
| docs/vsa-token-lane.md | Entity keys + relations (the VSA lane). Canonical ids, key binding, the relations composites, structure-awareness numbers |
| docs/token-economics.md | Where the savings come from + the router: the ladder, the ceiling, the outage fallback |
| docs/monologue.md | The [membrane] thinking-channel banner: every line explained, /monologue toggle |
| docs/symbol-birth.md | The symbol crystallization: how the hierarchy is born through language |
| docs/math-findings.md | The measured math: the variety/gate experiments and the e-series evidence |
| 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 (what it took to port membrane's ideas into another project) |
Development
Source layout: membrane/ (the field: streams → efield/efstore →
entity_block/efgate/night; the seat's serve server (server.py,
opencode-compatible); the MCP surface (mcp_server.py); the CLI),
experiments/ (the e-series rigs behind math-findings), tests/ (unittest +
pytest, plain-python runnable).
python -m unittest discover -s tests # everything runs against in-process fakes, no server needed
python experiments/variety_toy.py # the variety toy: the gate's calibration in one command
Status
Working lab, in daily use on its own development (dogfooded: this repo's own field holds ~14k live entities and its background mind digests, dreams, and speaks about its own work). Version 0.3.4; API and flag surfaces may still move. Licensing: private-use license (see LICENSE) — free for home and personal use; commercial use, redistribution and paid offerings need the author's written permission.
Metadata
Release files for membrane-seat 0.3.4
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.4.tar.gz | 3.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| membrane_seat-0.3.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 7.5 MB
Release files / membrane_seat-0.3.4.tar.gz
| Download URL | membrane_seat-0.3.4.tar.gz |
|---|---|
| Size | 3.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bc8b85579d120afad2807abe84215acabfa917a910641b4509e5ff3211599b10
|
|
BLAKE2b-256 checksum How to use checksums |
4f2d9a39798c1472d4f64bbf8580fb8f47e46e412c7bc808e8e681b829161fe8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / membrane_seat-0.3.4-py3-none-any.whl
| Download URL | membrane_seat-0.3.4-py3-none-any.whl |
|---|---|
| Size | 3.6 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
26abbe11f0ddc9d901564f83aa8db5b59d44928aedfcec939b3b7f96551ec00c
|
|
BLAKE2b-256 checksum How to use checksums |
af28c37e50940422ce0de6f64da9393fd33f9d92e53fec9d259620652dd3eaab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|