plugmem-host 0.5.0

Native host layer for plugmem: file storage with locking, Embedder trait and HTTP embedder implementations.
Documentation
# plugmem settings

This is the canonical configuration reference for `plugmem-host` and every
wrapper that uses it: `plugmem-cli`, `plugmem-mcp` and `plugmem-napi`.

## Config-file discovery

The config file itself is resolved in this order:

1. An explicit `--config PATH` (CLI/MCP) or `config` option (NAPI).
2. `$PLUGMEM_CONFIG`.
3. The platform config directory from `directories::ProjectDirs`:

   - Linux: `$XDG_CONFIG_HOME/plugmem/config.toml`, otherwise
     `~/.config/plugmem/config.toml`.
   - macOS: `~/Library/Application Support/plugmem/config.toml`.
   - Windows: `%APPDATA%\plugmem\config\config.toml`.

4. Built-in defaults when no config file exists.

An explicit path that does not exist is an error. A discovered default config
file is optional; its absence means that all defaults apply.

## Database-path precedence

The database path is resolved separately from the config-file path:

1. An explicit path (`--db` for CLI/MCP, or the NAPI constructor path).
2. `$PLUGMEM_DB`.
3. `[database].path` from this config file.
4. The platform data path.

The default platform data path is:

- Linux: `$XDG_DATA_HOME/plugmem/memory.plugmem`, otherwise
  `~/.local/share/plugmem/memory.plugmem`.
- macOS: `~/Library/Application Support/plugmem/memory.plugmem`.
- Windows: `%LOCALAPPDATA%\plugmem\data\memory.plugmem`.

The database is a snapshot plus adjacent journal and lock files. The host
uses mmap/overlay opens, so a large database is not loaded into RAM in full;
it still requires enough free disk for snapshots and maintenance temporary
files. Use an explicit path when the database belongs on a particular disk or
project.

## Example

```toml
[database]
# Optional. Explicit --db / constructor path and PLUGMEM_DB override this.
path = "/path/to/memory.plugmem"

[engine]
dim = 768              # 0 disables vectors
max_bytes = 2147483648
max_text = 4096
max_blob = 65536

[embedder]
# none | ollama | openai | lmstudio | vllm | llamacpp
kind = "ollama"
url = "http://localhost:11434/v1"
model = "nomic-embed-text"
api_key_env = "OPENAI_API_KEY"

[maintenance]
snapshot_every_ops = 1024
snapshot_journal_bytes = 4194304
maintain_every_forgets = 100

# CLI only: facts per `import` batch.
batch_size = 128

[server]
# MCP only: defaults to half of available cores, at least one.
workers = 4
```

## Sections

### `[database]`

| Key | Default | Meaning |
|---|---:|---|
| `path` | platform data path | Persistent snapshot path. It is overridden by an explicit path and `$PLUGMEM_DB`. |

### `[workspace]`

**Omit this section unless you need it.** Without it there is one database,
addressed by path, and nothing below applies — that is the default and the
common case. A workspace is for one process serving many independent memories
(a database per chat, per tenant), where each request says which one it means.

| Key | Default | Meaning |
|---|---:|---|
| `dir` | unset | Directory of named databases. Setting it is what turns a workspace on. |
| `max_open` | `16` | Databases kept open at once; the least recently used is closed to make room. Must be between 1 and 240 — one open database costs several file descriptors, so an unbounded value would exhaust them somewhere else in the program. |
| `idle_timeout_ms` | `60000` | Close a database unused this long. `0` never closes. |

The layout under `dir` is fixed:

```text
<dir>/registry.plugmem      the registry — an ordinary plugmem database
<dir>/db/<name>.plugmem     the databases themselves
```

A name is `[a-z0-9][a-z0-9_-]*`, at most 64 bytes. It is not a path and cannot
become one: separators, dots and leading dashes are not names, so a name can
only ever resolve to one file directly inside `<dir>/db`.

`idle_timeout_ms` is about **reachability, not memory**. An open database holds
an exclusive file lock, so a long-running server that never let go would make
its databases permanently unreachable from the CLI. The timeout is what returns
them.

### `[engine]`

These are the size-bearing fields accepted from TOML. BM25, fusion, graph and
HNSW tuning fields remain programmatic `plugmem-core::Config` settings for now.

| Key | Default | Meaning |
|---|---:|---|
| `dim` | `0` | Embedding dimension; zero disables vector storage. |
| `max_bytes` | `2147483648` | Ceiling for **each** byte pool, not their sum — see below. |
| `max_text` | `4096` | Maximum fact text length in bytes. |
| `max_blob` | `65536` | Maximum single blob length in bytes. |

`max_bytes` applies to every pool separately (arena pages, the text and
metadata blob heaps, the tag and posting chunk pools, the vector pool), so a
database's total goes several times past it; the pool that binds first is
normally the fact texts. The default is not a capacity judgement — it is the
figure that keeps every pool addressable where `usize` is 32 bits, so a file
written anywhere opens anywhere. Raise it if you need to, and the only thing
you give up is that: a 32-bit host then refuses the file with a typed error
rather than reading it wrongly.

There is no shard-count setting. How many shards each arena gets is derived
from how much the database holds, and `maintain` moves it as that changes —
a thousand facts on a layout meant for a million cost fourteen megabytes
instead of one. `plugmem-cli stats` reports the current layout.

### `[embedder]`

The default is `kind = "none"`; lexical, tag, graph and temporal retrieval
still work without an embedder. `$PLUGMEM_EMBEDDER` overrides
`[embedder].kind`.

| Key | Default | Meaning |
|---|---|---|
| `kind` | `none` | `none`, `ollama`, `openai`, `lmstudio`, `vllm` or `llamacpp`. |
| `url` | unset | OpenAI-compatible `/v1/embeddings` endpoint. Required for an active embedder. |
| `model` | unset | Embedding model name. Required for an active embedder. |
| `api_key_env` | unset | Environment variable containing the bearer token. |

An active embedder also requires `[engine].dim > 0`. All supported providers
use the same OpenAI-compatible HTTP shape.

### `[maintenance]`

| Key | Default | Meaning |
|---|---:|---|
| `snapshot_every_ops` | `1024` | Snapshot after this many mutations. |
| `snapshot_journal_bytes` | `4194304` | Snapshot when the journal reaches this size. |
| `maintain_every_forgets` | off | Run policy maintenance after this many forgets. |
| `batch_size` | `128` | CLI-only `import` batch size; `--batch` overrides it. |

One maintenance trigger has no key and is always on: a database that outgrows
(or falls far below) its shard layout re-shards itself on the next write. It
has to be automatic — the triggers above are opt-in, so otherwise a growing
database would keep the layout it was created with until somebody ran
`maintain` by hand. It is also self-limiting: the thresholds are a doubling up
and a fourfold drop, so it fires a handful of times over a database's life.

One consequence worth expecting: a database written by a version that used the
old fixed layout is stale the moment it opens, so its **first write re-shards
it**, at a cost proportional to its size. That happens once and leaves a
permanently smaller file.

### `[server]`

| Key | Default | Meaning |
|---|---:|---|
| `workers` | half of available cores | MCP worker threads; `--workers` overrides it. |

## Surface-specific overrides

| Surface | Explicit database path | Explicit config path | Extra override |
|---|---|---|---|
| CLI | `--db PATH` | `--config PATH` | `--batch`, `--json` |
| MCP | `--db PATH` | `--config PATH` | `--workers`, `--read-only` |
| NAPI | constructor `path` | `OpenOptions.config` | `OpenOptions.dim`, `readOnly` |
| Host | `Database::open(path, config)` | `Settings::load(path)` | programmatic builder options |

Use the runtime help surfaces when the full reference is not available:

```console
$ plugmem-cli help settings
$ plugmem-cli --json help settings
```

MCP exposes `plugmem_settings_help` with `format: "json"` or `"human"`, and
NAPI exposes `settingsHelp()`.