Configuration¶
MemCastle runs with no configuration at all. Everything below is for changing where it keeps things, or how it behaves.
Where files live¶
MemCastle follows the Unix XDG Base Directory convention on Linux and macOS alike.
macOS does not use ~/Library/Application Support: both platforms use the same paths,
so a dotfiles repository or a shell profile written once works on both.
| What | Default | Relocated by |
|---|---|---|
| Config file | ~/.config/memcastle/config.toml |
XDG_CONFIG_HOME, or --config / MEMCASTLE_CONFIG |
| Palace (persistent data) | ~/.local/share/memcastle/default |
XDG_DATA_HOME, or --palace / MEMCASTLE_PALACE_PATH / palace.path |
| Daemon registry (runtime state) | ~/.local/state/memcastle/run/ |
XDG_STATE_HOME |
| OAuth credential files (only when no keyring is used) | ~/.local/share/memcastle/credentials/ |
XDG_DATA_HOME, or credentials.dir |
| Package assets (not user data) | <prefix>/share/memcastle, or none |
--assets-dir / MEMCASTLE_ASSETS_DIR / assets.dir |
The last row is not a place you keep anything. It is where an OS package installs read-only files, the bundled sources, the agent integrations and the skills, and it is never under the XDG directories. See Runtime assets.
The XDG variables are honoured as the specification describes:
an unset, empty or relative value is ignored, and the default under your home directory is used instead.
For example, with XDG_DATA_HOME=/srv/data the palace is /srv/data/memcastle/default.
A project has its own, separate file, .config/memcastle.toml in the project's directory.
It is not this configuration: it holds no daemon setting, and it says which wing and room a project's memory belongs to
(see Project configuration).
The config file is optional.
If the default file does not exist, built-in defaults are used.
A file named explicitly with --config must exist, because a typo there would otherwise be silently ignored.
Palace and data path selection¶
A palace is one directory.
With the embedded store, the SurrealDB files live in its db/ subdirectory.
The path is chosen by, highest precedence first: --palace, MEMCASTLE_PALACE_PATH, palace.path in the config file,
then $XDG_DATA_HOME/memcastle/default.
It must be an absolute path; a relative one is rejected at startup, because a daemon and a client started
from different directories would disagree about which palace they mean.
Every command resolves the palace the same way, so serve, status, migrate, daemon stop and the rest agree on it.
A client finds a running daemon through the registry file, which is keyed by the palace path.
daemon start and daemon restart pass the palace they resolved on to the new daemon.
The registry is runtime metadata, not palace data, so it lives in the state directory rather than beside the palace.
Backing up or copying a palace never carries a stale daemon record with it.
Each palace has its own file, $XDG_STATE_HOME/memcastle/run/<palace-hash>/daemon.json,
where <palace-hash> is derived from the palace's canonical path.
If no home directory can be determined, the registry falls back to memcastle/run under the system temporary directory.
Storage and data describes the file.
Precedence¶
From lowest to highest, a later layer overrides an earlier one:
- Built-in defaults.
- The config file.
MEMCASTLE_*environment variables.- Command-line flags.
A malformed environment variable is an error naming the variable; it is never silently ignored.
The resolved configuration is validated once all layers are applied.
Unknown keys in the TOML file are currently ignored by normal configuration loading.
Run memcastle doctor to see warnings for unknown keys, including misspelled nested settings.
Logging has one more input.
Its precedence, highest first, is MEMCASTLE_LOG, RUST_LOG, the -v / -vv flags, then logging.level.
MEMCASTLE_LOG, RUST_LOG and logging.level accept a level (error, warn, info, debug, trace)
or a filter directive such as memcastle=debug,warn.
Config file¶
TOML. Every key is optional, and a file may set only some of them.
[palace]
path = "/home/alice/.local/share/memcastle/default"
# Global fallback for all evidence, including sources not yet installed.
[preferences]
default = "normal" # "low", "normal", or "high"
[preferences.sources.pi]
level = "high"
[[preferences.sources.pi.criteria]]
path = "working_directory" # exact JSON metadata key, or dot-separated nested keys
equals = "/home/alice/work" # string, number or boolean; exact match
level = "high"
# Overrides for the palace selected above; unspecified settings inherit.
[palace.preferences.sources.opencode]
level = "high"
[server]
bind = "127.0.0.1"
port = 8420
[logging]
level = "info"
# "text" for a terminal, "json" (one object per line) for journald and log shippers.
format = "text"
# Optional bearer-token authentication. Prefer MEMCASTLE_AUTH_TOKEN to `token` here,
# so the secret stays out of the file.
[auth]
enabled = false
# Defaults for `memcastle db start`. Nothing here opens the endpoint; see "The database admin endpoint".
[db]
bind = "127.0.0.1"
port = 8000
allow_remote = false
allowed_origins = []
# The listener that `webhook` triggers are delivered to; see "Triggers". Off by default: nothing listens until it is
# enabled here *and* a webhook trigger is enabled.
[webhook]
enable = false
bind = "127.0.0.1"
port = 8787
allow_remote = false
max_body_bytes = 1048576
max_concurrent = 16
# Where embeddings come from. Without a provider search stays lexical; see "Embeddings".
[embeddings]
provider = "none" # "none", "command" or "http"
# command = ["/usr/local/bin/embed"]
# url = "http://localhost:11434/v1"
# model = "nomic-embed-text"
timeout_secs = 30
batch_size = 16
# Where entities and relationships are extracted from mined text. Off by default; see "Extraction".
[extraction]
provider = "none" # "none", "heuristic", "command" or "http"
# command = ["/usr/local/bin/extract"]
# url = "http://localhost:11434/v1"
# model = "llama3.1"
timeout_secs = 120
batch_size = 8
max_entities = 32 # kept from one drawer
max_relations = 64
min_confidence = 0.3 # relationships below this are dropped
# How eagerly duplicate memories and entity spellings are recognised; see "Deduplication".
[dedup]
enabled = true # false stores every drawer and links nothing
near_threshold = 0.9 # similarity, 0.5 to 1, at which a drawer is linked as a likely copy
entity_fuzzy = true # a unique one-character typo converges on the entity it resembles
# How mining cuts and bounds what it reads; see "Mining sources".
[mining]
chunk_chars = 6000 # characters per drawer; a longer document becomes several drawers
max_file_bytes = 2097152 # directory mining skips files larger than this
max_documents = 0 # 0 means unlimited; a positive value caps one job, whose next run resumes the cursor
# Installed WebAssembly sources (see "Writing a mining source"): where they live, and what each call may use.
# sources_dir = "/home/alice/.local/share/memcastle/sources"
source_memory_mib = 256 # the most memory one call into an installed source may use
source_timeout_secs = 60 # the longest one call may run; a source's own limits can only lower these
# Where `memcastle source search|install <name>|update` look: an https URL, a file:// URL or an absolute path to a
# memcastle-index.json (or a directory holding one). The official registry by default; a list here replaces it, and
# `[]` means none. The default is https://memcastle.github.io/registry.json.
# It is read only when you run one of those commands.
# registries = ["https://example.org/memcastle/index.json"]
# The GitHub API a registry entry that names a repository is resolved through (see "Publishing and installing sources").
# github_api_url = "https://api.github.com"
# "optional" installs an unsigned package and refuses a bad signature from a key you trust; "required" installs only
# what a key in `trusted_keys` signed. Keys are the public keys `memcastle source keygen` prints.
trust = "optional"
# trusted_keys = ["Zm9vYmFy..."]
[plugins]
# Plugin catalogues are independent of the legacy source registry. They are read on explicit search requests only.
# registries = ["https://memcastle.github.io/plugins.json"]
# trust = "optional" # set "required" to accept only releases signed by a trusted key
# trusted_keys = [] # ed25519 public keys (same format as mining.trusted_keys)
# Immutable plugin versions live here by default: $XDG_DATA_HOME/memcastle/plugins.
# dir = "/home/alice/.local/share/memcastle/plugins"
# Where the tokens of a source signed in with OAuth are kept; see "Credentials".
[credentials]
backend = "auto" # "auto" (the platform keyring, else a file), "keyring" or "file"
# dir = "/home/alice/.local/share/memcastle/credentials" # for the file store
# What the daemon mines, by name; see "Miners". Written by `memcastle miner set`, or by hand.
[[miners]]
name = "docs"
source = "directory"
locator = "/home/alice/src/docs"
# Only to serve assets from somewhere other than the installed or embedded ones: a package unpacked elsewhere, or a
# checkout of the repository (which holds sources/, integrations/, skills/ and web/dist).
[assets]
dir = "/home/alice/src/memcastle"
# The web UI, served under /ui. Off by default; see "Web UI".
[web]
enable = false
[jobs]
max_concurrency = 4
background_concurrency = 2 # mine/embed/extract jobs; leaves a slot for other jobs
drain_timeout_secs = 10
lease_ttl_secs = 30
# Embedded SurrealKV under palace.path (the default), or a remote SurrealDB.
[store]
mode = "embedded"
# When an embedded palace forces its writes to disk: "every" (each commit, the default),
# "never" (leave it to the operating system) or an interval over 100ms such as "500ms", "5s" or "1m".
sync = "every"
jobs.background_concurrency limits mine, embed and extract jobs independently of jobs.max_concurrency.
When the total limit is greater than one, the effective background limit is capped at one less than the total;
with a total limit of one, one background job may run.
Lower the background limit if API reads slow down during large mines or embedding sweeps;
raise it only when the datastore and CPU have spare capacity.
Source preferences¶
[preferences] supplies the global fallback; each [preferences.sources.<connector>] optionally sets a level and
[[preferences.sources.<connector>.criteria]] can select a level for a matching source-document metadata value.
Connector names are mining adapter names (pi, opencode, directory, or an installed source), not broad drawer kinds.
There is no shared criterion vocabulary: use JSON keys actually exposed by that connector, including nested paths
separated by dots; keys must consist of letters, digits, underscores or hyphens.
Rules compare JSON strings, numbers and booleans exactly, without case folding or coercion.
The longest matching path wins; equal-depth matches use their first declaration.
An unknown connector or a drawer without a mining origin uses the default;
an absent key or unmatched value uses its connector level, or the default when no level was set.
An unknown connector in configuration is allowed so a source can be configured before it is installed.
The global configuration can also contain [palace.preferences] with the same keys and nested connector tables.
Optionally put a preferences-only file at <palace.path>/preferences.toml:
[preferences]
default = "normal"
[preferences.sources.pi]
level = "normal" # explicitly replaces a global `high`
criteria = [] # explicitly clears inherited connector criteria
Precedence is built-in normal, global [preferences], global [palace.preferences], then the palace-local file.
Only supplied fields replace earlier fields; a connector level does not erase inherited criteria.
The selected palace.path (including environment and --palace overrides) determines which local file is read.
This file is distinct from a project's .config/memcastle.toml.
Changes take effect on daemon restart; previous source-document metadata is retained on the originating drawer.
Levels shape ranks within relevant candidates and the read-side hint on unresolved conflicting facts.
They do not exclude a source, guarantee correctness, raise an extractor's confidence, or reopen superseded memory.
Explicit corrections and validity still win; relevance, original confidence and freshness remain decisive.
Search-hit signals.preference reports the effective level, connector and matched criterion for inspection.
See Source preferences for the ranking and inheritance decision.
A remote store also needs a URL and credentials:
[store]
mode = "remote"
url = "ws://localhost:8000"
namespace = "memcastle"
database = "main"
username = "root"
password = "..."
Remote store settings are file-only; there is no environment variable for them.
Keep secrets out of version control: put this file outside any repository, and restrict it with chmod 600.
Environment variables¶
| Setting (TOML key) | Environment variable | Default |
|---|---|---|
palace.path |
MEMCASTLE_PALACE_PATH |
~/.local/share/memcastle/default |
server.bind (an IP address) |
MEMCASTLE_BIND |
127.0.0.1 |
server.port (0 to 65535) |
MEMCASTLE_PORT |
8420 |
assets.dir (an absolute path) |
MEMCASTLE_ASSETS_DIR |
none: installed, then embedded assets |
web.enable (true or false) |
MEMCASTLE_WEB_ENABLE |
false |
logging.level |
MEMCASTLE_LOG |
info |
logging.format |
MEMCASTLE_LOG_FORMAT |
text (text or json) |
jobs.max_concurrency (at least 1) |
MEMCASTLE_JOBS_MAX_CONCURRENCY |
4 |
jobs.background_concurrency (at least 1) |
MEMCASTLE_JOBS_BACKGROUND_CONCURRENCY |
2 |
jobs.drain_timeout_secs (1 to 86400) |
MEMCASTLE_JOBS_DRAIN_TIMEOUT_SECS |
10 |
jobs.lease_ttl_secs (3 to 86400) |
MEMCASTLE_JOBS_LEASE_TTL_SECS |
30 |
auth.enabled (true or false) |
MEMCASTLE_AUTH_ENABLED |
false |
auth.token (at least 16 characters) |
MEMCASTLE_AUTH_TOKEN |
none |
db.bind (an IP address) |
MEMCASTLE_DB_BIND |
127.0.0.1 |
db.port (0 to 65535) |
MEMCASTLE_DB_PORT |
8000 |
db.allow_remote (true or false) |
MEMCASTLE_DB_ALLOW_REMOTE |
false |
db.allowed_origins (a list) |
MEMCASTLE_DB_ALLOWED_ORIGINS (comma-separated) |
none |
webhook.enable (true or false) |
MEMCASTLE_WEBHOOK_ENABLE |
false |
webhook.bind (an IP address) |
MEMCASTLE_WEBHOOK_BIND |
127.0.0.1 |
webhook.port (0 to 65535) |
MEMCASTLE_WEBHOOK_PORT |
8787 |
webhook.allow_remote (true or false) |
MEMCASTLE_WEBHOOK_ALLOW_REMOTE |
false |
embeddings.provider (none, command, http, openai or external) |
MEMCASTLE_EMBEDDINGS_PROVIDER |
none |
embeddings.url (an http:// or https:// URL) |
MEMCASTLE_EMBEDDINGS_URL |
none |
embeddings.model |
MEMCASTLE_EMBEDDINGS_MODEL |
none |
embeddings.api_key |
MEMCASTLE_EMBEDDINGS_API_KEY |
none |
embeddings.timeout_secs (1 to 3600) |
MEMCASTLE_EMBEDDINGS_TIMEOUT_SECS |
30 |
embeddings.batch_size (1 to 1024) |
none | 16 |
embeddings.command (a list) |
none | none |
embeddings.dimensions (positive integer, legacy single-provider form) |
none | 768 |
embeddings.default (named profile) |
none | legacy default when a single provider is configured |
embeddings.providers.<name>.type (command, openai, http or external) |
none | none |
embeddings.providers.<name>.url (base_url is an alias) |
none | none |
embeddings.profiles.<name>.provider, .model, .dimensions |
none | none |
extraction.provider (none, heuristic, command or http) |
MEMCASTLE_EXTRACTION_PROVIDER |
none |
extraction.url (an http:// or https:// URL) |
MEMCASTLE_EXTRACTION_URL |
none |
extraction.model |
MEMCASTLE_EXTRACTION_MODEL |
none |
extraction.api_key |
MEMCASTLE_EXTRACTION_API_KEY |
none |
extraction.timeout_secs (1 to 3600) |
MEMCASTLE_EXTRACTION_TIMEOUT_SECS |
120 |
extraction.batch_size (1 to 256) |
MEMCASTLE_EXTRACTION_BATCH_SIZE |
8 |
extraction.min_confidence (0 to 1) |
MEMCASTLE_EXTRACTION_MIN_CONFIDENCE |
0.3 |
extraction.max_entities (1 to 1000) |
none | 32 |
extraction.max_relations (1 to 10000) |
none | 64 |
extraction.command (a list) |
none | none |
dedup.enabled (true or false) |
MEMCASTLE_DEDUP_ENABLED |
true |
dedup.near_threshold (0.5 to 1) |
MEMCASTLE_DEDUP_NEAR_THRESHOLD |
0.9 |
dedup.entity_fuzzy (true or false) |
MEMCASTLE_DEDUP_ENTITY_FUZZY |
true |
mining.chunk_chars (200 to 100000) |
MEMCASTLE_MINING_CHUNK_CHARS |
6000 |
mining.max_file_bytes (1 to 67108864) |
MEMCASTLE_MINING_MAX_FILE_BYTES |
2097152 |
mining.max_documents (0 or 1 to 1000000) |
MEMCASTLE_MINING_MAX_DOCUMENTS |
0 (unlimited) |
mining.sources_dir (an absolute path) |
MEMCASTLE_MINING_SOURCES_DIR |
$XDG_DATA_HOME/memcastle/sources |
mining.source_memory_mib (16 to 4096) |
MEMCASTLE_MINING_SOURCE_MEMORY_MIB |
256 |
mining.source_timeout_secs (1 to 3600) |
MEMCASTLE_MINING_SOURCE_TIMEOUT_SECS |
60 |
mining.registries (a list of URLs or absolute paths) |
MEMCASTLE_MINING_REGISTRIES (comma-separated; empty for none) |
the official registry |
mining.github_api_url (an https:// URL) |
MEMCASTLE_MINING_GITHUB_API_URL |
https://api.github.com |
| none (never in the file) | GH_TOKEN, else GITHUB_TOKEN |
no token: GitHub's unauthenticated rate limit applies |
mining.trust (optional or required) |
MEMCASTLE_MINING_TRUST |
optional |
mining.trusted_keys (a list of base64 public keys) |
MEMCASTLE_MINING_TRUSTED_KEYS (comma-separated) |
none |
mining.bundled_dir (an absolute path) |
MEMCASTLE_MINING_BUNDLED_DIR |
share/memcastle/sources of the installation |
plugins.dir (an absolute path) |
none | $XDG_DATA_HOME/memcastle/plugins |
plugins.registries (a list of URLs or absolute paths) |
none | https://memcastle.github.io/plugins.json |
plugins.trust (optional or required) |
none | optional |
plugins.trusted_keys (a list of base64 public keys) |
none | none |
credentials.backend (auto, keyring or file) |
MEMCASTLE_CREDENTIALS_BACKEND |
auto |
credentials.dir (an absolute path) |
MEMCASTLE_CREDENTIALS_DIR |
$XDG_DATA_HOME/memcastle/credentials |
store.sync (every, never or an interval over 100ms) |
MEMCASTLE_STORE_SYNC |
every |
store.mode and remote settings |
none | embedded |
auth.token is a secret, and is handled as one: MemCastle never logs it, prints it, serialises it, or writes it anywhere,
and the client commands read it from the environment or the config file, never from a flag.
Whitespace around MEMCASTLE_AUTH_TOKEN is ignored,
but a token in the config file with leading or trailing whitespace is refused at start,
because a client's token is trimmed too and the two could never match.
See Authentication.
embeddings.api_key and extraction.api_key are secrets too: they are never logged, printed or serialised,
and an invalid value is never echoed.
Some variables are read by the command line rather than the config file:
| Variable | Equivalent flag |
|---|---|
MEMCASTLE_CONFIG |
--config |
MEMCASTLE_MODE |
--mode |
XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME |
none |
RUST_LOG |
none |
Command-line flags¶
| Flag | Applies to | Overrides |
|---|---|---|
--config <FILE> |
every command | the default config file location |
--palace <PATH> |
every command | palace.path, MEMCASTLE_PALACE_PATH |
--bind <IP> |
serve, daemon start, daemon restart |
server.bind, MEMCASTLE_BIND |
--port <PORT> |
serve, daemon start, daemon restart |
server.port, MEMCASTLE_PORT |
--assets-dir <DIR> |
serve, daemon start, daemon restart, integration list, install and update |
assets.dir, MEMCASTLE_ASSETS_DIR |
--mode <MODE> |
every command (acted on by client commands) | the memory mode of the session |
-v, -vv |
every command | the log level of memcastle itself |
The listener: address and port¶
The daemon serves the REST API and MCP on one TCP listener, 127.0.0.1 port 8420 unless configured otherwise:
The address and the port are separate settings, so either can be changed alone.
Each is chosen by, highest precedence first: the flag, the environment variable, the config file, the default.
serve, daemon start, daemon restart and a supervisor such as systemd all start the daemon through the same path,
so the same three sources work everywhere.
Client commands (status, search, job and the rest) read the config file and the environment,
but not the flags, so a daemon started on a non-default port with --port is found through its registry file.
daemon start and daemon restart pass their --bind, --port and --assets-dir on to the new daemon.
memcastle status shows which of the two it used (endpoint_source: registry or config).
It exits 0 for a healthy daemon, 1 for a degraded one and 3 when none is running,
and prints JSON when piped (or with --json) for scripts:
memcastle status # human-readable report in a terminal, JSON in a pipe
memcastle status --json | jq .daemon.datastore
memcastle status || echo "exit $?" # 3 means not running
server.bind is an IP address, IPv4 or IPv6, and not a host name.
A host:port value, as bind accepted before the port became its own setting, is refused with a message naming the port
setting to use instead.
Port 0 asks the OS for a free port, which is useful for tests and scripts;
the port actually chosen is in the daemon's registry file and its log.
The default never listens on all network interfaces.
Authentication is off unless you enable it, so use a non-loopback address such as 0.0.0.0 only on a network you trust,
or with authentication enabled.
The daemon logs a warning when it listens beyond loopback without it.
The MCP endpoint additionally refuses a non-loopback Host header, see Authentication.
The listener is bound before anything that changes state: the runtime assets are only located, not yet used.
If the address is taken, needs privileges, or does not exist on this machine,
the start fails with memcastle::server::bind_failed, naming the address and what to change,
and has not created the palace, migrated it or touched its jobs.
Embeddings¶
Semantic and hybrid search rank by vectors, and a vector has to come from a model.
MemCastle links no model and does not want your API keys, so the [embeddings] section only says how to reach one.
Without it the palace works exactly as before and search is lexical.
A vector is derived data: it is stored beside the drawer, never replaces its content, and can be recomputed at any time.
The legacy single-provider configuration defaults to 768 dimensions; set dimensions for another model.
The daemon requests each profile's configured dimensions and rejects a different-sized response with
memcastle::embed::dimension_mismatch.
Vectors for different profiles have separate records keyed by a generation-pipeline fingerprint, including the
connection identity, model, dimensions and truncation version.
Changing any of those settings creates a new space, rather than silently reusing incompatible vectors.
The first space to claim a schema-declared 768-, 1536- or 3072-dimensional HNSW tier owns that tier permanently;
other spaces, including models with the same dimensions, use slower exact profile-filtered ranking.
Other positive dimensions also use exact ranking, so a model change never needs runtime schema DDL.
The indexed tier cannot be reassigned by disabling a profile; choose the profile to index before the first embedding
at that dimension (usually by setting it as the default), or plan for exact searches that grow with its corpus size.
Use a versioned, immutable model identifier when the provider offers one.
If an endpoint silently changes the model behind the same alias, MemCastle cannot detect that from configuration:
pin the revision at the provider, or change the configured model/profile deliberately and re-embed.
Named connections and profiles¶
[embeddings]
default = "local"
[embeddings.providers.ollama]
type = "openai"
base_url = "http://localhost:11434/v1"
[embeddings.providers.router]
type = "openai"
base_url = "https://openrouter.ai/api/v1"
[embeddings.profiles.local]
provider = "ollama"
model = "qwen3-embedding"
dimensions = 768
[embeddings.profiles.bulk]
provider = "router"
model = "openai/text-embedding-3-small"
dimensions = 1536
The openai type implements the OpenAI embeddings protocol, including compatible endpoints.
http remains available for the legacy single-provider form.
For caller-computed vectors without a daemon provider, use type = "external" for a named connection and select its
profile explicitly on upload and query; the declared model and dimensions identify the space.
Do not use one external profile for vectors from different models.
The existing single-provider keys under [embeddings] map to a profile named default when that provider is enabled.
If only named profiles are configured, set embeddings.default to enable automatic sweeps and unqualified semantic
queries; otherwise select a profile explicitly for each job or search.
Old vectors in drawer.embedding have no verifiable model identity: they are retained but excluded from profile search.
Run memcastle embed --profile NAME to populate a new space.
The command provider¶
[embeddings]
provider = "command"
command = ["/usr/local/bin/embed", "--quiet"]
model = "my-model" # passed to the program, optional
MemCastle runs the program, without a shell, once per batch, writes one JSON object to its standard input and reads one from its standard output:
{"model": "my-model", "dimension": 768, "texts": ["first text", "second text"]}
{"embeddings": [[0.01, 0.02, "… 768 numbers"], [0.03, 0.04, "…"]]}
The program returns one vector per text, in order, and exits 0.
A non-zero exit, output that is not that JSON, or no answer within timeout_secs fails the call, and the program is killed.
This is the provider to use when the model needs a credential or a client library: the program owns them, MemCastle never
sees them, and the daemon's own MEMCASTLE_* variables (its auth token included) are removed from the program's environment.
Everything else is inherited, so PATH and the program's own variables work.
The http provider¶
MemCastle posts to {url}/embeddings in the OpenAI format, so OpenAI, Ollama, llama.cpp's server and vLLM all work.
A local server needs no key.
A hosted one takes MEMCASTLE_EMBEDDINGS_API_KEY, sent as a bearer token; prefer that to api_key in the file.
How embedding happens¶
- The daemon embeds in the background.
After anything that writes drawers (mining, a checkpoint, a diary entry, a created drawer) and once at startup,
it queues a low-priority
embedjob that embeds every drawer missing a vector in the default profile. Queued sweeps are coalesced, so a burst of writes leaves one waiting job.memcastle embedqueues one by hand, for example after you first configure a provider.memcastle embed --profile bulk --refresh stale --wing projectselects a profile and refreshes missing or stale inputs;--refresh allre-embeds every drawer in scope, while the default ismissing. Jobs persist their selected model and generation settings and fail with an actionable error if a restarted daemon has changed them; submit a new job after changing a profile. - A drawer is embedded from its first 8,000 characters, because providers cap their input. Its content is stored whole and verbatim whatever the provider sees.
- A query is embedded on the fly.
If the provider fails, a default
autosearch falls back to lexical and logs a warning, while an explicitsemanticorhybridsearch fails withmemcastle::embed::failed. - You can skip the provider altogether and send vectors yourself, see Searching.
Extraction¶
The knowledge graph links entities (people, projects, tools, ...) to each other and to the drawers that mention them,
and graph-aware search follows those links.
The [extraction] section says how MemCastle finds them in mined text.
Nothing is extracted by default, because two of the providers send mined text somewhere else:
choose one on purpose.
Extraction is derived data: it adds entities, links and relationships beside the drawers and never rewrites,
supersedes or deletes one.
See ADR-024.
provider |
What reads the text | Leaves the process? |
|---|---|---|
none |
nothing | no |
heuristic |
a small built-in extractor | no |
command |
a program you run | whatever it does |
http |
an OpenAI-compatible chat endpoint | yes, to that endpoint |
The heuristic extractor¶
Deterministic, with nothing to configure and no model.
It finds runs of capitalised words (Ada Lovelace), @handles and `code spans`, and relates two of them when the
words between them in one sentence are a known phrase (works on, is a member of, depends on, uses, owns,
is part of, is based in, ...).
It never guesses: two names that merely share a sentence are not related, and a sentence it does not recognise yields
entities and no relationship.
For more than that, use a model.
The command extractor¶
[extraction]
provider = "command"
command = ["/usr/local/bin/extract"]
model = "my-model" # passed to the program, optional
MemCastle runs the program, without a shell, once per batch, writes one JSON object to its standard input and reads one from its standard output:
{"model": "my-model",
"vocabulary": {"kinds": ["person", "…"], "predicates": ["works_on", "…"]},
"texts": ["first text", "second text"]}
{"extractions": [
{"entities": [{"name": "Ada", "kind": "person"}, {"name": "MemCastle", "kind": "project"}],
"relations": [{"subject": "Ada", "predicate": "works_on", "object": "MemCastle", "confidence": 0.9}]},
{"entities": [], "relations": []}]}
One extraction per text, in order, and exit 0.
When source preferences are configured, the request also contains "preferences", an array aligned with "texts".
Each entry has level, optional source and optional matched criterion; without preferences the original request
shape is unchanged.
A preference is a cue for evidence selection, not permission to invent a fact or raise its confidence.
A non-zero exit, output that is not that JSON, or no answer within timeout_secs fails the call, and the program is killed.
The program owns the model and any credential, and the daemon's own MEMCASTLE_* variables are removed from its environment.
The http extractor¶
MemCastle posts to {url}/chat/completions in the OpenAI format, one request per drawer, telling the model the
vocabulary and asking for JSON.
With configured source preferences, an additional system message supplies the effective level, connector and matching
criterion, and explicitly tells the model not to infer truth from that context.
A hosted endpoint takes MEMCASTLE_EXTRACTION_API_KEY, sent as a bearer token.
An endpoint that cannot be reached or answers with an error fails the job.
A reply that is not the JSON asked for is read as "nothing found" and logged, so one odd answer cannot block the drawers
behind it.
What is kept¶
Whatever a provider says is read into a closed vocabulary, so the graph does not fill with synonyms.
Entity kinds are person, organization, project, tool, place, concept and other;
predicates are works_on, member_of, depends_on, uses, owns, part_of, located_in and related_to.
Anything else becomes other or related_to.
A relationship needs both ends among the entities of the same answer, and is dropped when it points at itself,
has no sensible confidence or falls below min_confidence.
max_entities and max_relations bound what one drawer contributes.
How extraction happens¶
- The daemon extracts in the background.
After a mining job completes, and once at startup, it queues a low-priority
extractjob, coalesced like the embedding sweep.memcastle extractqueues one by hand. - Only mined drawers (they carry a source origin) and notes are read, and only while they are current. Writing a note queues a sweep too, since nothing else would. A drawer is read once: changing the provider later does not re-read what was already read.
- A drawer's first 8,000 characters are sent, whatever it holds.
- Every fact records the drawer, source document, job and extractor it came from, and holds from the document's own date when the source has one. When a re-mine replaces a drawer, the facts it supported stop being current and the new drawer is read.
Deduplication¶
The [dedup] section controls how eagerly MemCastle recognises the same memory, or the same entity, written twice.
It never merges anything: the most it does is not store an exact copy, link a likely copy to what it resembles,
and let an entity spelled differently converge on the one it is a variant of.
The rules, and what each writer gets back, are on the Deduplication page.
enabled = falsestores every drawer and records no link. Entity names still converge when they differ only in case, punctuation or spacing, which is spelling and not a judgement.near_thresholdis the similarity at which a drawer is linked as anearduplicate of another in its room. Raise it towards1to link only the closest copies; below0.5it would link unrelated drawers, so it is refused. It never makes MemCastle skip a write: only an exact copy does.entity_fuzzy = falseleaves casing, punctuation and alias convergence on, and turns off the one-character typo rule and thepossibly_same_ascandidates.
The database admin endpoint¶
memcastle db start opens a second, SurrealDB-compatible listener inside the running daemon, so SurrealDB Studio can
inspect the live embedded database.
The [db] settings are only the defaults for that command; no setting starts the endpoint.
Each flag of memcastle db start (--bind, --port, --allow-remote, --allow-origin) overrides its setting,
and --allow-origin adds to db.allowed_origins.
The address is 127.0.0.1 port 8000 unless configured otherwise, and the port must differ from server.port.
A db.bind that is not a loopback address is refused when the configuration loads unless db.allow_remote is true,
and the daemon refuses to open it at all unless auth.enabled is also true.
db.allowed_origins lists web page origins, such as https://app.surrealdb.com, that may connect from a browser,
in addition to pages served from this machine and the SurrealDB Studio desktop app, which are always allowed.
The values are compared whole, never as patterns.
See Database access for the workflow and the security model.
Triggers¶
A trigger asks for a miner's run on its own: on a timetable, on a poll, when a file changes, or when a webhook is
called.
Triggers are the [[triggers]] entries of the configuration file, managed from the file or from
memcastle trigger (ADR-043).
Every trigger is disabled until enabled = true, and nothing the daemon does ever sets it.
[[triggers]]
name = "notes-watch" # unique: lowercase letters, digits, `-` and `_`; `reload` is reserved
miner = "notes" # the miner it asks to run
type = "watch" # "schedule", "poll", "webhook" or "watch"
enabled = false # the default
path = "/home/alice/notes" # the other keys depend on the type
debounce = "2s"
[[triggers]]
name = "notes-hook"
miner = "notes"
type = "webhook"
credential = { type = "env", name = "NOTES_HOOK_SECRET" } # or { type = "file", path = "/run/secrets/hook" }
delivery_header = "x-delivery-id"
The settings of each type, what enabling needs, the webhook listener ([webhook] above), its security and
networking prerequisites, bursts, repeats, restarts and failures are on the Triggers page.
[webhook] is read when the daemon starts, and a [[triggers]] entry is read again whenever the file changes.
Runtime assets¶
Some files a release carries are not your configuration or your data. They are read-only, come with the version of MemCastle you installed, and are found by one fixed rule, tried in order:
- An explicit directory, from
--assets-dir,MEMCASTLE_ASSETS_DIRorassets.dir, by the usual precedence. It must be an absolute path to an existing directory. If it does not exist the daemon refuses to start withmemcastle::assets::not_found; it never quietly uses another source in its place. - The installed directory, which an OS package creates:
share/memcastlenext to the executable'sbin/(/usr/share/memcastlefor/usr/bin/memcastle), then/usr/local/share/memcastle, then/usr/share/memcastle. The first that exists is used. A candidate inside your XDG data directory is ignored, because that is where your palace lives. - The assets built into the binary. This is what a standalone download uses, and it needs no directory and no network.
The daemon logs which source it resolved at startup (runtime assets resolved).
The schema and the data migrations are always built into the binary and are not affected by any of this.
Installation describes the package layout.
This is the one root for everything a package ships, in the same layout wherever it is:
| Under the root | What | Read by |
|---|---|---|
sources/<name>/ |
the bundled mining sources, unpacked: a memcastle-source.toml and a source.wasm each |
the daemon, which lists them as installed and runs them in place (see Publishing and installing sources) |
skills/ |
the shared agent skills | memcastle integration, which copies the ones an integration's manifest names into its installed copy |
web/dist/ |
the built web UI | the daemon, under /ui, when web.enable is set |
A standalone binary has none (the dashboard is not built into it, so with web.enable it answers a page that says the
files are missing), so point the setting at a package or checkout only when it supplies assets you need.
The bundled sources are taken from <root>/sources when a directory in it holds a memcastle-source.toml and a
source.wasm, and mining.bundled_dir outranks that.
A root with no such package, like a checkout (whose sources/ holds projects, with their component under dist/),
leaves the sources to the installed assets.
memcastle integration reads the root from the same three places as the daemon; --assets-dir is accepted by its
list, install and update commands.
Miners¶
Source configurations¶
A source configuration names reusable source-native defaults and a credential reference; it has no destination or schedule.
The default ID is the source ID (chatgpt for chatgpt), even when nothing is written in TOML.
Additional configurations live in the global file and have unique IDs:
[[source_configs]]
id = "github-work"
source = "github"
credential = { type = "env", name = "GH_TOKEN" }
[source_configs.options]
include = ["my-team/*"]
[[miners]]
name = "work-wiki"
source = "github"
source_config = "github-work"
wing = "work"
[miners.options]
wiki_include = ["my-team/wiki"]
memcastle source config list|get|set|remove and authenticated /api/source-configs manage this section.
MCP can list configuration summaries but cannot edit or authenticate them.
A miner selects its source's default configuration when source_config is absent.
Source configuration options are applied first and matching miner options replace them;
one-off miner run options replace matching saved options without rewriting either definition.
A change to shared source defaults that may read more material needs --allow-broaden in the CLI
or allow_broaden = true in the REST body; the UI asks for the same explicit choice.
The common mining.max_documents default (zero for unlimited) is overridden by a miner's positive max_documents.
embedding_profile selects a runnable named embedding profile for drawers this miner files.
The resolved vector space is pinned to its durable job; an explicit embedding-job profile outranks the miner override,
which outranks the global default.
Credentials and their paths are never returned by a configuration view; OAuth sign-ins for the default configuration
retain the existing source-keyed credential so upgrades do not lose them.
ChatGPT's current web authorization can select only the default account: an additional named ChatGPT web configuration
cannot authenticate yet; export-import miners remain usable without an account.
Legacy miner credential references still load, but put new reusable credentials on the source configuration.
Miner definitions¶
A miner is a named, persistent definition of what to mine and how: a source or reusable source configuration,
input locator, options, destination, per-job limit and optional schedule.
Definitions live in global [[miners]] entries or one file per miner under $XDG_CONFIG_HOME/memcastle/miners/.
An explicit --config in another directory keeps new miners in that global file and does not load unrelated XDG miners.
Additional directories can be listed under [mining] as miner_dirs = ["/path/to/team/miners"].
Files are read in filename order, after the global section and default directory.
A file contains the same fields as a [[miners]] entry, at its top level rather than inside a table.
For example, ~/.config/memcastle/miners/ChatGPT Dev.toml may contain:
source = "chatgpt"
locator = "/exports/latest.zip"
wing = "personal"
room = "conversations"
max_documents = 300
[schedule]
every = "1d"
at = "03:30"
enabled = false
When name is absent its filename stem is trimmed, lowercased and spaces become hyphens:
ChatGPT Dev.toml defines chatgpt-dev, invoked with memcastle mine chatgpt-dev.
An explicit name takes precedence, and duplicate IDs are reported with both paths rather than silently overridden.
Existing definitions are edited at their originating file; files are checked for concurrent edits and replaced atomically.
They are managed from the file, from memcastle miner and, read-only, from MCP
(ADR-037).
[[miners]]
name = "signal-personal" # unique: lowercase letters, digits, `-` and `_`; `reload` is reserved
source = "signal" # the source adapter: `directory`, or an installed source's name
enabled = true # default true
locator = "+336..." # where in the source to read; the source decides what it means
wing = "signal" # the wing its drawers go to, when it should not be the source's default
room = "conversations" # optional fixed room
max_documents = 500 # overrides the common per-job cap; omit to inherit it
mapping = { wing_from = "project_name", room_from = "conversation_group" } # optional source-independent metadata routing
credential = { type = "env", name = "SIGNAL_TOKEN" } # or { type = "file", path = "/run/secrets/signal" }, or { type = "oauth" }
[miners.options] # source options: strings, numbers, booleans, lists of strings
contacts = ["+336..."]
groups = ["MemCastle"]
window_days = 30
A key that is not listed is an error, not an ignored typo: enable = false must not leave a miner enabled.
options is an open table, so a source can define keys of its own.
Legacy [miners.scope] and [miners.config] tables must be merged by hand into [miners.options] before starting
this version; keys present in both need one chosen value.
Likewise REST callers must replace scope and config with options, and unset_scope and unset_config with
unset_options.
- Identity. A miner's cursor and documents belong to the source it points at, and not to its name.
Renaming, enabling, disabling or changing a credential keeps its cursor
when the acquisition scope and mapping stay the same.
Pointing it at another source or locator creates a different source with its own cursor.
A source such as
chatgptcan also give different selected project scopes separate identities and cursors. Removing a miner, or disabling it, never deletes what it mined. A configured Wing, Room or named source configuration gives a different destination an independent cursor and document set. Amapping.wing_fromormapping.room_fromkey reads a string from each document's metadata. Missing or invalid names fall back to the fixedwing/roomor source defaults. Different mapping definitions have independent cursor and document records, so two miners can reuse one sign-in without treating a differently filed conversation as already ingested. - Secrets. A secret is never written in the file.
credentialnames where it comes from: an environment variable of the daemon, a file, or{ type = "oauth" }for a source that signs in with OAuth (see Credentials), which names nothing because the daemon keeps the tokens itself. A key inoptionsthat reads like a secret (password,token,secret,api_key, ...) is refused, and the daemon reports a credential only asenv,fileoroauthand whether it resolves (foroauth, whether the source is signed in), never its name or path. Handing anenvorfilecredential to a source is the source's side of the contract, which does not take one yet; today the daemon checks that it resolves, so an enabled miner whose variable is unset isunavailable, with the fix. An OAuth sign-in is handed over: a source that declares one is given a fresh access token on each call that asks. Sources requiring it for every mode make a miner unavailable untilmemcastle source auth <source>has been run; a source with on-demand OAuth may also offer an offline mode or an explicit manual-session fallback. - Validation. The shape of every entry is checked when the file is loaded, so a bad entry stops the daemon starting
like a bad
[embeddings]section. An enabled miner is also held to what running needs, when it is created or changed: the source is built in or installed and enabled, the credential resolves, everyoptionskey is one the source declares as an option, and fordirectorythelocatoris an absolute path. A disabled miner may name a source that is not installed yet, so a configuration can be written ahead of the install. - Schedule. A miner runs when asked with
memcastle miner run <name>ormemcastle mine <name>. Add[miners.schedule] every = "1d"and optionallyat = "03:30"(UTC), or[schedule]in a miner file. The timetable remains disabled untilmemcastle miner schedule-enable <name>(or REST/UI) explicitly enables it. The daemon reconciles it through the same trigger supervisor as standalone triggers; removal, a disabled miner or a disabled schedule stops the task without discarding mined data. - Options. The
optionstable holds saved source options:miner runpasses them to the source exactly asmemcastle mine <source> key=valuewould (ADR-042). A scalar is its text and a list of strings is comma-joined; an undeclared key or nested table makes the miner not runnable, so the daemon never silently ignores an option.miner run NAME key=valuereplaces or adds an option for that run only and leaves the saved definition unchanged. A change that may include more material, whether persistent or temporary, requires--allow-broaden. Sources can declare include/exclude/date comparisons; an option without declared comparison semantics requires approval whenever its value changes.directorydeclaressince,includeandexclude(comma-separated relative-path globs);pi,opencodeandclaudedeclaresinceanddir.chatgptdeclares a web-onlyprojectsfilter in addition tomodeandaccountoptions. The externalmemcastle-githubplugin's source requiresincluderepository patterns;excludeand separate wiki patterns can narrow them further.
Claude Code history miner¶
The externally installed claude source reads only session transcripts under ~/.claude/projects.
Define a miner explicitly; no Claude Code client or source installation creates one:
[[miners]]
name = "claude-history"
source = "claude"
locator = "/home/alice/.claude/projects"
enabled = true
[miners.options]
since = "2026-09"
dir = "/home/alice/src/*"
since narrows by transcript modification time and dir matches the working directory recorded in each transcript.
Use a separate miner or run memcastle mine claude --full when a restored transcript predates its cursor.
ChatGPT project miner¶
The external chatgpt source can select projects on its experimental web backend using miner options.
Sign the source in with memcastle source auth chatgpt on the daemon's machine before running web miners.
This source's desktop-compatible OAuth bearer was verified to read history without cookies using a browser-style
User-Agent; the old manually supplied web session remains a fallback (ChatGPT).
[[miners]]
name = "chatgpt-work"
source = "chatgpt"
[miners.options]
projects = ["name:Work", "id:g-p-example"]
mode = "web"
The miner joins the projects list with commas and the source resolves names to unique project IDs.
If a name no longer exists or names collide, the run fails rather than broadening its scope.
Use project IDs for scheduled miners: names can be reused by another project after a rename.
Without projects, a web miner includes ordinary conversations and every project's history; exports do not support
project selection.
Changing or removing an option that broadens project selection still follows the --allow-broaden rule.
GitHub repository and wiki miners¶
The memcastle-github plugin's source requires an explicit repository include pattern.
org/* selects repositories owned by an organization; an exclude pattern overrides inclusion.
The wiki can be selected independently within those repositories with wiki_include and wiki_exclude.
[[miners]]
name = "acme-github"
source = "github"
credential = { type = "env", name = "GH_TOKEN" }
[miners.options]
include = ["acme/*"]
exclude = ["acme/private-experiment"]
wiki_include = ["acme/docs"]
comments = true
reviews = true
since = "2026-01-01"
Set GH_TOKEN or GITHUB_TOKEN in the daemon's environment; when both are set, GH_TOKEN wins.
The miner's env credential reference checks that variable exists, while the source's explicit manifest grant actually
delivers it; choose the same name in both places.
Omit credential for public repositories if unauthenticated GitHub rate limits suffice.
For a wiki-only miner, set issues = false, pulls = false and metadata = false under [miners.options] and define
wiki_include there too.
CLI memcastle miner set accepts the same options with repeatable --option 'include=acme/*' and
--option 'exclude=acme/private-experiment' flags, and the REST miner API accepts the same table.
MCP can list/get this miner but cannot change it.
See GitHub source for Git prerequisites, path/label/topic filters and cursor behavior.
A miner can override the global embedding profile with embedding_profile = "bulk" as a top-level [[miners]] field.
This is not a source acquisition option: it does not go to the source adapter or alter its cursor.
The miner pins its resolved profile on the mine job and queues a separate, pinned, missing-only embedding job limited
to that mined source when mining finishes; a wing further restricts it if set.
An explicit memcastle embed --profile NAME selects its own profile instead.
Set it through memcastle miner set NAME --embedding-profile bulk --wing WING or unset it with
memcastle miner set NAME --unset embedding_profile.
Changing miners while the daemon runs¶
The configured global file and individual miner files are the source of truth, and the daemon is their only writer.
miner set, enable, disable and remove edit only the [[miners]] section, in place:
comments, the other tables and every entry that did not change are left exactly as they were,
and the file is replaced atomically with its permissions kept.
The environment and command-line overrides are not part of the file, so they are never written into it.
- Hand edits are picked up.
The daemon notices that the file changed on the next request about miners, and
memcastle miner reloadreads it at once and says what differs:added,removed,changed,enabled,disabledandbroadened. A miner is never half-applied: each entry is whole or the file is refused. - An invalid edit does not take the miners away.
The daemon keeps the last miners that were valid, reports the error beside them (
miner list,GET /api/miners), and refuses to change miners until the file is fixed, because writing would overwrite what is being repaired. A file that is invalid at startup stops the daemon, like the rest of the configuration. - Reload reconciles enabled schedules. A miner with no explicitly enabled schedule remains manual; a configured timetable starts, changes or stops idempotently. A job that is already queued or running when a miner is disabled or removed finishes, because a job carries the source and locator and not the miner's name.
- Option changes never broaden silently.
miner setrefuses a change it cannot prove does not read more, with the existingmemcastle::miner::scope_broadeneddiagnostic, unless--allow-broadensays it is meant. Positive selections narrow when values are removed; exclusions narrow when values are added; a latersincenarrows. Unknown semantics require approval for changes. A hand edit that may widen a miner is applied, since the file is yours, but the daemon logs it atwarnandminer reloadlists it underbroadened. - A daemon started without a configuration file (one built in code, as in a test) lists miners but refuses to
change them: there is nowhere to keep them.
memcastlealways knows the file:--config,MEMCASTLE_CONFIG, or~/.config/memcastle/config.toml, which is created by the firstminer set.
Credentials¶
A mining source that cannot be reached with a static token declares an OAuth sign-in in its manifest,
and memcastle source auth <source> signs it in (ADR-039).
The daemon keeps what that produces, renews it when a run needs it, and hands the source an access token for each call;
the source never owns a store of its own, and the tokens are never in this file, the palace database, the logs, or
anything a REST or MCP answer returns.
credentials.backend chooses where the tokens are kept:
auto(the default) uses the platform's credential store when one works: the Keychain on macOS, the Credential Manager on Windows, the Secret Service on Linux (GNOME Keyring, KWallet and the like). It is probed on first use and not at startup, since probing can prompt for a keychain password, and when it does not work (a Linux server with no desktop session, a container) the tokens go to an owner-only file instead. A credential is looked for in both, so one written to the file while the keyring was down is still found when it is back.keyringuses the platform store only, and signing in fails where there is none.fileusescredentials.dironly: one<source>.jsonper source, the directory0700and each file0600on Unix, written to a temporary file and renamed so that a crash never leaves half a token. It is the setting for a headless host: the files are exactly as private as the account that runs the daemon. The directory defaults to$XDG_DATA_HOME/memcastle/credentials, and must be absolute.
What is kept is the refresh token, which is what lets a source stay signed in for months, with the scopes granted and a fingerprint of the client, endpoints and scopes the manifest declared at the time. The short-lived access token is held in the daemon's memory, and is written down only for a provider that issues no refresh token, until it expires. If an update to a source changes its client, its endpoints or its scopes, the stored credential no longer matches, counts as missing, and the source asks to be signed in again under the new terms. A credential is shared by every palace on the machine that uses the same keyring or directory, since it belongs to the installed source and not to a palace.
Web UI¶
web.enable = true (or MEMCASTLE_WEB_ENABLE=true) makes the daemon serve its web UI under /ui.
It is off by default: a daemon that does not set it answers nothing at /ui.
There is no command-line flag; daemon start and daemon restart pass the environment and the file to the daemon as usual.
The dashboard's files are found in web/dist/ of the runtime assets,
in a package and in a checkout alike.
If they are missing, the daemon still starts, logs a warning, and answers /ui with a page that says how to install them.
See Web UI for the login, the memory mode and the development workflow.
Not supported: other platforms' conventions¶
Windows is built and tested, but has no dedicated layout: the same dot-directories are used under your home directory. Any platform-specific behaviour would be an explicit design decision, recorded in an ADR; see ADR-010.
See also¶
- Running the daemon for starting, stopping and supervising it.
- Storage and data for what lives in the palace and the registry.
- CLI reference for every flag.
- Troubleshooting for configuration errors.