Memory Layers
Written by ddxfish on .
- Docs
- Plugin Author Guide
- Memory Layers
- All docs ↓
You are reading the dev branch of the docs, which may describe unreleased changes. Read the main docs.
Register your own layer in the Mind Palace memory system. Your content becomes
part of the AI's mind: searchable with search_memory, woven to the people and
places she knows, walkable by the connection spider, browsable on its own tab
in the web UI — without you writing a search engine, an embedding pipeline, or
a single line of frontend.
Use cases: mirror an Obsidian vault, a wiki, a documentation tree, a lore book, an external note system — anything with items of text worth remembering.
Requires: the mindpalace plugin (the Mind Palace memory system). When it
isn't active, your layer simply doesn't light up; declare your imports lazily
(shown below) and your plugin degrades cleanly.
How it works — the mirror model
You don't serve queries. You mirror your content into the palace as normal memory chunks tagged with your layer key, and the palace does everything else: full-text + semantic search, entity-mention weaving (names in your text link to entities in her graph automatically), embeddings (generated in the background), export/import, and a generated web tab.
Your source of truth stays yours. The palace holds a copy keyed by your
source ids, and you reconcile on change: delete by source, re-save.
1. Declare the layer
{
"name": "my-vault",
"capabilities": {
"memory_layers": [
{
"key": "vault",
"label": "Vault",
"icon": "🗄️",
"description": "Notes mirrored from my Obsidian vault.",
"librarian": false,
"writable": false
}
]
}
}
| Field | Required | Meaning |
|---|---|---|
key |
yes | Lowercase identifier — letters, digits, underscores only (must be a valid Python identifier, so no hyphens). Unique across plugins; the first registrant wins. The reserved core layers (self, events, entities, knowledge, goals) are refused. |
label |
no | Display name (tab, chips). Defaults to the key, title-cased. |
icon |
no | Emoji for the tab and flyout entry. |
description |
no | One line shown in the AI's search tool description and on your tab. |
librarian |
no (default false) |
Opt in to librarian passes: her memory curator will review, merge, and retire chunks in your layer alongside her own memories. Leave false if your sync owns the content. |
writable |
no (default false) |
Allow the AI to save_memory directly into your layer. Leave false for sync-owned mirrors — rows she saved would be orphaned or clobbered by your next reconciliation. |
mode |
no (default 'mirror') |
Reserved. Only 'mirror' exists today. |
Registration happens at plugin load and unwinds at unload/disable — the AI's tool schemas update live in both directions.
2. Write content through layer_api
Import lazily, inside functions — never at module top-level — so plugin load order never matters:
def sync_vault(notes, scope='default'):
try:
from plugins.mindpalace.tools import layer_api
except ImportError:
return # Mind Palace not active — mirror later
# Batch sync: ONE transaction, ONE ledger line for the whole run
items = [{'content': n.text[:512], 'source': n.path,
'label': n.folder, 'fields': {'tags': n.tags}} for n in notes]
ids, msg = layer_api.bulk_save(items, layer='vault', scope=scope)
def note_changed(note, scope='default'):
from plugins.mindpalace.tools import layer_api
# Reconciliation: source is your external id
layer_api.delete_by_source('vault', note.path, scope=scope)
layer_api.save(note.text[:512], layer='vault', scope=scope,
source=note.path, fields={'tags': note.tags})
The API (core/plugins/mindpalace/tools/layer_api.py, imported as plugins.mindpalace.tools.layer_api):
save(content, layer, scope='default', label=None, fields=None, source=None)→(chunk_id, message). Full pipeline: inline embedding, FTS, entity-mention weaving. One ledger line per call — use for single items, not loops.bulk_save(items, layer, scope='default')→(ids, message). One transaction, embeddings deferred to the background sweep, one ledger summary line. Use this for syncs.delete_by_source(layer, source, scope='default')→(count, message). Removes every chunk you saved with thatsource, cleans its graph edges, one ledger line.get_sources(layer, scope='default')→{source: {'count', 'fields'}}for your mirrored chunks. The cheap diff: stamp a content hash intofieldsat save time, compare here, skip unchanged sources on the next sync.sync_note(layer, summary, scope='default', detail=None)— one ledger line about an out-of-band event. Sparingly: one per sync run, never one per item.
The fence: layer_api accepts only your registered plugin layers — the
palace's own layers are never writable through it. Content is capped at 512
chars per chunk (like every palace memory); chunk long notes yourself.
Ledger etiquette matters. The ledger is the AI's "what changed in my mind"
stream, read every time she reads her self-sheet. A 500-note sync must be one
line ("my-vault synced 500 items into vault"), not 500. bulk_save and
delete_by_source do this for you; don't loop save() over a big import.
3. What you get for free
- Search:
search_memory(layer='vault')and all-layer searches include your content; your layer is listed in the tool's description automatically. - Weaving: names in your content matching her entities create graph edges at write time; the spider walks them like any memory.
- Web tab: your layer appears in the Mind Palace tab strip and the Mind flyout, with browse/search/favorite/export — zero frontend code.
- Scope semantics: pass the chat's memory scope (read
scope_memoryfromcore.chat.function_manager— returnNoneon error and disable, never fall back to'default'). Scope deletion deletes your mirrored rows with everything else. - Settings: need a vault path? Declare
capabilities.settings(guide) — your tool executor receives them automatically.
Disabled = dark
When your plugin is disabled, your layer goes dark: mirrored rows stay in the database but vanish from every read path — search, browse, spider, tabs. Re-enable the plugin and they light back up. Nothing is deleted. (Deleting a whole scope deletes its plugin-layer rows too — scope deletion is total.)
Minimal working example
The whole contract in two small files. The manifest declares the layer (plus however you trigger your sync — a tool, a schedule task, a daemon):
{
"name": "lore-book",
"version": "0.1.0",
"description": "Mirrors lore.txt into the Mind Palace.",
"capabilities": {
"memory_layers": [
{"key": "lore", "label": "Lore", "icon": "📜",
"description": "World lore mirrored from lore.txt."}
],
"tools": ["tools/lore_tools.py"]
}
}
And the sync itself — reconcile by source, then re-mirror in one batch:
def sync_lore(scope='default'):
try:
from plugins.mindpalace.tools import layer_api
except ImportError:
return "Mind Palace not active - nothing mirrored.", False
from pathlib import Path
src = Path(__file__).parent.parent / 'lore.txt'
paras = [p.strip() for p in src.read_text(encoding='utf-8').split('\n\n')]
layer_api.delete_by_source('lore', str(src), scope=scope)
items = [{'content': p[:512], 'source': str(src)} for p in paras if p]
ids, msg = layer_api.bulk_save(items, layer='lore', scope=scope)
return msg, True
Call it from a plugin tool's execute() (see Tools), a
scheduled task, or a daemon loop. Search, the web tab,
weaving, and embeddings all arrive free once the rows land.
For a bigger sync, do the cheap diff first: stamp a content hash into
fields at save time, read get_sources() on the next run, and skip
sources whose hash didn't change — a no-change sync should write zero
ledger lines.
Gotchas
- Lazy imports only.
from plugins.mindpalace.tools import layer_apiat module top-level breaks under load-order changes and when Mind Palace is disabled. Inside the function, wrapped in try/except: always safe. - Don't write the palace DB directly.
layer_apiis the contract; raw SQL againstmind.dbwill break across palace versions and skips the ledger, weaving, and FTS. writable: falsestill lets users edit. The web tab lets the user delete/favorite your mirrored chunks — your next sync should tolerate missing rows (idempotent re-save bysource).- Sign after every edit (
python tools/sign_plugin.py <your-plugin>) like any plugin.
Reference for AI
MEMORY LAYERS (plugin-declared Mind Palace layers):
- Manifest:
capabilities.memory_layers= [{key, label?, icon?, description?, librarian? (default false), writable? (default false), mode? ('mirror' only)}]. Key: lowercase valid identifier (no hyphens), unique across plugins, first registrant wins; reserved keys refused: self, events, entities, knowledge, goals. Registered at plugin load, unregistered at unload/disable; the AI's tool schemas update live both ways. - Requires the
mindpalaceplugin. Import lazily INSIDE functions:from plugins.mindpalace.tools import layer_apiwrapped in try/except ImportError — never at module top level. - API (
core/plugins/mindpalace/tools/layer_api.py; import name staysplugins.mindpalace...):save(content, layer, scope='default', label=None, fields=None, source=None) -> (chunk_id|None, message)— full pipeline (inline embedding, FTS, entity weaving), one ledger line per call; single items only, never loops.bulk_save(items, layer, scope='default') -> (ids, message)— items = [{'content', 'label'?, 'fields'?, 'source'?}]; one transaction, embeddings deferred to the background sweep, ONE ledger summary line; empty/over-length items skipped and counted; very large batches are cut off by a runaway backstop.delete_by_source(layer, source, scope='default') -> (count, message)— reconciliation primitive; cleans graph edges; one ledger line.get_sources(layer, scope='default') -> {source: {'count', 'fields'}}— the cheap diff (stamp a content hash into fields at save time, compare here).sync_note(layer, summary, scope='default', detail=None) -> bool— one ledger line for an out-of-band event; one per sync run maximum.
- Invariants: layer_api writes ONLY registered plugin layers (the palace's own layers are fenced); content capped at 512 chars per chunk; ledger etiquette = one line per sync run, never per item; plugin disabled => layer goes dark (rows kept, hidden from every read path) and re-enable relights; deleting a scope deletes its plugin-layer rows.
- Scope: read the chat's memory scope via
core.chat.function_manager.scope_memory.get(); on error return None and disable — never fall back to 'default'. - Never write mind.db directly; re-sign after any edit (
python tools/sign_plugin.py <plugin>).
Edit this page on GitHub · docs/plugin-author/memory-layers.md @ a5fa790 · synced
