memgres 0.5.2__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgres-0.5.2 → memgres-0.7.0}/PKG-INFO +34 -9
- {memgres-0.5.2 → memgres-0.7.0}/README.md +33 -8
- {memgres-0.5.2 → memgres-0.7.0}/memgres/__init__.py +7 -3
- {memgres-0.5.2 → memgres-0.7.0}/memgres/_version.py +1 -1
- memgres-0.7.0/memgres/admin.py +552 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/admin_cli.py +8 -7
- {memgres-0.5.2 → memgres-0.7.0}/memgres/config.py +17 -0
- memgres-0.7.0/memgres/delimiters.py +88 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/embed_worker.py +31 -58
- memgres-0.7.0/memgres/identity.py +1252 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/info.py +3 -0
- memgres-0.7.0/memgres/lines.py +65 -0
- memgres-0.7.0/memgres/links.py +188 -0
- memgres-0.7.0/memgres/mcp_server.py +1079 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0002_identity.sql +13 -3
- memgres-0.7.0/memgres/migrations/0009_create_namespace_right.sql +23 -0
- memgres-0.7.0/memgres/migrations/0010_namespace_alias.sql +24 -0
- memgres-0.7.0/memgres/migrations/0011_drop_default_namespace.sql +23 -0
- memgres-0.7.0/memgres/migrations/0012_user_profile.sql +30 -0
- memgres-0.7.0/memgres/migrations/0013_hash_version.sql +28 -0
- memgres-0.7.0/memgres/migrations/0014_access_request_no_fk.sql +24 -0
- memgres-0.7.0/memgres/migrations/0015_normalize_tags.sql +44 -0
- memgres-0.7.0/memgres/migrations/0016_valid_at.sql +16 -0
- memgres-0.7.0/memgres/migrations/0017_memory_link.sql +40 -0
- memgres-0.7.0/memgres/migrations/0018_links_built.sql +12 -0
- memgres-0.7.0/memgres/migrations/0019_memory_usage.sql +36 -0
- memgres-0.7.0/memgres/migrations/0020_memory_usage_no_fk.sql +17 -0
- memgres-0.7.0/memgres/periodic.py +153 -0
- memgres-0.7.0/memgres/relink.py +157 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/schema.py +60 -2
- {memgres-0.5.2 → memgres-0.7.0}/memgres/search.py +62 -39
- memgres-0.7.0/memgres/server.py +651 -0
- memgres-0.7.0/memgres/store.py +1706 -0
- memgres-0.7.0/memgres/tags.py +90 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/base.py +73 -19
- {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/pgvector.py +18 -6
- {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/qdrant.py +42 -7
- {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/PKG-INFO +34 -9
- {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/SOURCES.txt +31 -1
- {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/entry_points.txt +1 -0
- {memgres-0.5.2 → memgres-0.7.0}/pyproject.toml +1 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_blame_integration.py +3 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_chunk_index.py +146 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_claim_and_reembed.py +3 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_diffing.py +29 -4
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_embed_worker.py +3 -0
- memgres-0.7.0/tests/test_identity_integration.py +592 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_lexical_match.py +3 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_limits.py +9 -0
- memgres-0.7.0/tests/test_links.py +738 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_list.py +72 -1
- memgres-0.7.0/tests/test_mcp_admin_tools.py +329 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_mcp_instructions.py +5 -2
- memgres-0.7.0/tests/test_mcp_recall_schema.py +128 -0
- memgres-0.7.0/tests/test_mcp_tool_visibility.py +349 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_migration_upgrade.py +43 -0
- memgres-0.7.0/tests/test_multi_space_search.py +371 -0
- memgres-0.7.0/tests/test_path_addressing.py +226 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_qdrant_integration.py +3 -0
- memgres-0.7.0/tests/test_require_title.py +183 -0
- memgres-0.7.0/tests/test_retention.py +270 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_roles_bootstrap.py +125 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_search_integration.py +63 -0
- memgres-0.7.0/tests/test_security_integration.py +706 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_segments_store.py +7 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_server_info.py +31 -1
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_server_integration.py +209 -6
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_snippets.py +3 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_store_integration.py +200 -9
- memgres-0.7.0/tests/test_tags.py +268 -0
- memgres-0.7.0/tests/test_usage.py +328 -0
- memgres-0.7.0/tests/test_valid_at.py +201 -0
- memgres-0.7.0/tests/test_write_ergonomics.py +215 -0
- memgres-0.5.2/memgres/identity.py +0 -559
- memgres-0.5.2/memgres/mcp_server.py +0 -446
- memgres-0.5.2/memgres/server.py +0 -514
- memgres-0.5.2/memgres/store.py +0 -664
- memgres-0.5.2/tests/test_identity_integration.py +0 -325
- memgres-0.5.2/tests/test_mcp_recall_schema.py +0 -74
- memgres-0.5.2/tests/test_security_integration.py +0 -263
- {memgres-0.5.2 → memgres-0.7.0}/LICENSE +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/blame.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/bootstrap.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/diffing.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/embeddings.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/healthcheck.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/indexing.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0001_core.sql +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0003_history_author.sql +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0004_title.sql +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0005_chunk_index.sql +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0006_reader_floor.sql +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0007_embed_retry.sql +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0008_service_roles.sql +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/reembed.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/segments.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/__init__.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres/worker.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/dependency_links.txt +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/requires.txt +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/top_level.txt +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/setup.cfg +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_config.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_embeddings.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_healthcheck.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_mcp_http_transport.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_qdrant_ca.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_replace_build.py +0 -0
- {memgres-0.5.2 → memgres-0.7.0}/tests/test_segments.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
|
|
5
5
|
Author: mozgsml
|
|
6
6
|
License-Expression: MIT
|
|
@@ -99,7 +99,7 @@ optional layers on top of the same core:
|
|
|
99
99
|
└─ MCP server write/recall/get/blame/move/forget as MCP tools
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
**Record model:** one memory = one mutable body (up to a configurable ceiling, default 256 KB) plus metadata — `tags` (cross-cutting labels, `text[]` + GIN), a `path` (its place in an `ltree` tree), timestamps, and per-diff provenance (`source`/`reason`, kept in history). A single write/diff is capped smaller (default 16 KB), so large bodies accrue over many authored diffs. **Organization is two orthogonal axes:** the tree is *where a memory lives* (one place, subtree-selectable); tags are *what it's about* (many, overlapping). Both filter either search — narrow a semantic query to a subtree, or list a tag across the tree.
|
|
102
|
+
**Record model:** one memory = one mutable body (up to a configurable ceiling, default 256 KB) plus metadata — `tags` (cross-cutting labels, `text[]` + GIN), a `path` (its place in an `ltree` tree), timestamps, and per-diff provenance (`source`/`reason`, kept in history). A single write/diff is capped smaller (default 16 KB), so large bodies accrue over many authored diffs. **Organization is two orthogonal axes:** the tree is *where a memory lives* (one place, subtree-selectable); tags are *what it's about* (many, overlapping). Both filter either search — narrow a semantic query to a subtree, or list a tag across the tree. **Links are the third axis:** `[[path]]` in a body becomes a real edge — walkable in BOTH directions, so "what relies on this?" has an answer before you change a fact — and a move rewrites the bodies that named the old address, so a link keeps working and an address copied out of a body is still one. Each memory also records how often it surfaces in search and how often it is opened, which is how the part of a corpus nobody reads becomes visible.
|
|
103
103
|
|
|
104
104
|
**Isolation:** optional multi-tenant identity keeps tenants from seeing each other's memories — users own *namespaces*, and rotatable, permission-scoped *tokens* authenticate as a user (turned on with `MEMGRES_KEY_MODE=open|managed`; see [docs/TENANCY.md](docs/TENANCY.md)). Encryption at rest is left to the deployment — Postgres/managed-PG/disk TDE stays transparent to queries, so search keeps working; memgres deliberately does **not** encrypt bodies application-side (that would make them unsearchable, which is why no comparable tool does it either). GDPR erasure is real: `forget()` hard-deletes the row, its vectors, and crypto-shreds the history chain. All limits are env-configurable, so the same code serves a single-user embed and a capped multi-tenant service.
|
|
105
105
|
|
|
@@ -116,7 +116,7 @@ docker compose up
|
|
|
116
116
|
|
|
117
117
|
Defaults suit a single-user setup with no auth. To change limits, the embedding provider, tokens, … drop a `.env` beside it — every `MEMGRES_*` is optional (see [Configuration](#configuration) or [.env.example](.env.example)).
|
|
118
118
|
|
|
119
|
-
**Give it to an LLM / agent — no code (MCP).** Point any URL-capable MCP client (Cursor, Cline, Claude Desktop, …) at the running server; the model gets `memory_write`, `memory_recall`, `memory_get`, `memory_list`, `memory_blame`, `memory_history`, `memory_move`, `memory_forget`, `memory_server_info` as tools:
|
|
119
|
+
**Give it to an LLM / agent — no code (MCP).** Point any URL-capable MCP client (Cursor, Cline, Claude Desktop, …) at the running server; the model gets `memory_write`, `memory_recall`, `memory_get`, `memory_list`, `memory_tags`, `memory_links`, `memory_blame`, `memory_history`, `memory_move`, `memory_forget`, `memory_server_info` as tools:
|
|
120
120
|
|
|
121
121
|
```json
|
|
122
122
|
{
|
|
@@ -191,7 +191,7 @@ Not sure which fits? Start with the decision guide: [docs/CHOOSING.md](docs/CHOO
|
|
|
191
191
|
1. **`docker compose up`** — `pgvector` + service, nothing to configure. For a dedicated vector service instead, `docker compose --profile qdrant up` and set `MEMGRES_VECTOR_BACKEND=qdrant` (Qdrant ranks vectors; Postgres still holds bodies and does tag/subtree/TTL filtering).
|
|
192
192
|
2. **Your own Postgres** — install the `[server]` extra (above), point `MEMGRES_DATABASE_URL` at it, run `memgres-server` (migrates on startup).
|
|
193
193
|
3. **Embedded library** — install the core package, use `Store` directly, no HTTP at all.
|
|
194
|
-
4. **Split service (many clients)** — a stateless API tier that only flags writes plus a scalable `memgres-worker` tier that embeds; see [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) and `deploy/docker-compose.yml`. Switch the embedding model later with `memgres-reembed`.
|
|
194
|
+
4. **Split service (many clients)** — a stateless API tier that only flags writes plus a scalable `memgres-worker` tier that embeds; see [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) and `deploy/docker-compose.yml`. Switch the embedding model later with `memgres-reembed`; rebuild the link graph with `memgres-relink`.
|
|
195
195
|
|
|
196
196
|
Semantic recall is optional: the default `MEMGRES_EMBED_PROVIDER=none` gives you lexical FTS with zero models. Turn on `local` (sentence-transformers), a cloud API (`openai`/`jina`), or any OpenAI-compatible server (LM Studio, Ollama, …) when you want meaning-based search — see [docs/EMBEDDINGS.md](docs/EMBEDDINGS.md) for choosing local vs cloud and [docs/BACKENDS.md](docs/BACKENDS.md) for copy-paste setups. The model id + dimension get stamped into the schema and a later mismatch hard-fails instead of silently returning garbage.
|
|
197
197
|
|
|
@@ -206,12 +206,16 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
206
206
|
| `MEMGRES_MAX_BODY_BYTES` | `262144` | ceiling for a whole record body (256 KB) |
|
|
207
207
|
| `MEMGRES_MAX_WRITE_BYTES` | `16384` | ceiling for one write/diff payload (≤ body) |
|
|
208
208
|
| `MEMGRES_MAX_SOURCE_BYTES` / `_MAX_REASON_BYTES` | `2048` / `1024` | ceilings for a write's `source` / `reason` provenance |
|
|
209
|
-
| `MEMGRES_RETENTION_DAYS` | `0` | `0` =
|
|
209
|
+
| `MEMGRES_RETENTION_DAYS` | `0` | how long data is kept: `0` = forever (off); `>0` = expire N days after **last touch**. Operator-only — there is no per-write TTL. Note the clock restarts on a touch, and a read counts as one unless `MEMGRES_RENEW_ON_READ=false`: the window covers data nobody uses |
|
|
210
210
|
| `MEMGRES_RENEW_ON_READ` | `true` | a read pushes the expiry clock forward |
|
|
211
|
+
| `MEMGRES_RETENTION_SWEEP` | `true` | this process runs the retention sweep. Every server process starts one; set `false` where a dedicated sweeper already runs |
|
|
212
|
+
| `MEMGRES_RETENTION_SWEEP_INTERVAL` | `3600` | seconds between sweeps that DELETE expired rows (and their vectors). Only runs when `RETENTION_DAYS > 0` |
|
|
213
|
+
| `MEMGRES_USAGE_COUNTERS` | `true` | count how often each memory surfaces in search and is read in full (`memory_usage`, a separate table — never the `memory` row, never the hash chain). Off makes reads pure again, for a read-only replica |
|
|
211
214
|
| `MEMGRES_KEY_MODE` | `single` | `single` (no auth, one space) · `open` (bring-your-own token, self-registers) · `managed` (admin-provisioned). See [docs/TENANCY.md](docs/TENANCY.md) |
|
|
212
215
|
| `MEMGRES_ADMIN_TOKEN` | — | global admin bearer for provisioning (managed mode) |
|
|
213
216
|
| `MEMGRES_TOKEN` | — | default token used when a call passes none (single-tenant endpoints) |
|
|
214
217
|
| `MEMGRES_TREE` | `true` | `ltree` path column + GiST index (fast subtree select) |
|
|
218
|
+
| `MEMGRES_REQUIRE_TITLE` | `true` | `true` = a write that stores content must supply `title`. Captions are what name a memory in results and what title-weighted ranking weighs; `move`/`retag` are exempt (they store no content) |
|
|
215
219
|
| `MEMGRES_REQUIRE_PARENT` | `false` | `true` = a node's parent path must already exist |
|
|
216
220
|
| `MEMGRES_HISTORY` | `true` | keep the hash-chained diff history (deleted with the record) |
|
|
217
221
|
| `MEMGRES_FTS_LANGUAGE` | `simple` | Postgres FTS dictionary (`simple`/`english`/…) |
|
|
@@ -220,6 +224,9 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
220
224
|
| `MEMGRES_FULL_BODY` | `false` | force the whole body on every hit (off = auto: short whole, long sliced); `full_body` per call overrides |
|
|
221
225
|
| `MEMGRES_FULL_BODY_MAX_CHARS` | `500` | a body this short is returned whole (`kind="full"`) instead of sliced |
|
|
222
226
|
| `MEMGRES_LIST_PREVIEW_CHARS` | `120` | first-line preview length returned by `memory_list` |
|
|
227
|
+
| `MEMGRES_LIST_BODIES_MAX_BYTES` | `200000` | total body bytes one `bodies=true` browse returns; rows past the cap come back marked `body_omitted`, never dropped |
|
|
228
|
+
| `MEMGRES_MCP_ADMIN_TOOLS` | `auto` | register the `memory_admin_*` control-plane tools over MCP; `auto` = every mode but `single`. A context economy, not a security boundary — each tool authorizes on call |
|
|
229
|
+
| `MEMGRES_MCP_TOOL_VISIBILITY` | `auto` | show each client only the tools its own credential can use, and drop the identity tools where there are no identities (`single`). Display, not authorization — every tool still authorizes on call; `off` lists everything |
|
|
223
230
|
| `MEMGRES_INSTRUCTION` | — | server-side MCP instructions emitted at `initialize` (a client like Claude Code loads it once at connect); unset = omitted; capped at 2 KB |
|
|
224
231
|
| `MEMGRES_VECTOR_BACKEND` | `pgvector` | `pgvector` (same DB) or `qdrant` (set `QDRANT_URL`, `QDRANT_API_KEY`, `MEMGRES_QDRANT_COLLECTION`) |
|
|
225
232
|
| `MEMGRES_EMBED_PROVIDER` | `none` | `none` / `local` / `openai` / `jina` / `openai-compatible` (LM Studio, Ollama, vLLM, TEI…) |
|
|
@@ -244,13 +251,31 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
244
251
|
| `GET` | `/memories/{id}/blame` | line attribution; `?group`, `?text`, `?lines=1,3-5` |
|
|
245
252
|
| `GET` | `/memories/{id}/at/{seq}` | body reconstructed at a version |
|
|
246
253
|
| `GET` | `/recall` | `?q=&k=&mode=&tags=&path_prefix=&match=&snippet=&full_body=` |
|
|
247
|
-
| `GET` | `/memories` | list a subtree, no query: `?path_prefix=&tags=&limit=&offset=` |
|
|
254
|
+
| `GET` | `/memories` | list a subtree, no query: `?path_prefix=&tags=&limit=&offset=&bodies=` |
|
|
255
|
+
| `GET` | `/whoami` | what this token may do, as capabilities |
|
|
256
|
+
| `GET` | `/admin/orphans` | memories stranded by a `single` → managed switch |
|
|
257
|
+
| `POST` | `/admin/adopt-orphans` | move them into a real namespace (idempotent) |
|
|
248
258
|
| `GET` | `/spaces` | namespaces this token can reach (identity modes) |
|
|
259
|
+
| `POST` | `/spaces` | create one of your own (nothing is created by being named) |
|
|
260
|
+
| `POST` | `/spaces/aliases` | give a reachable namespace a name of your own |
|
|
249
261
|
| `GET` | `/info` | effective config: limits, embed provider/model/dim, recall modes, backend |
|
|
250
262
|
| `GET` | `/healthz` | liveness |
|
|
251
263
|
|
|
264
|
+
**`{id}` may be a memory's uuid or its tree path** (`/memories/decisions.pricing`)
|
|
265
|
+
— the segment is read as an id when it parses as a uuid, and as a path
|
|
266
|
+
otherwise, so hyphenated and non-ASCII paths (`ops.rate-limits`) address fine.
|
|
267
|
+
A path that a memory has since moved away from is followed on a read
|
|
268
|
+
(the answer sets `moved_from`) and refused on a write, which is what stops an
|
|
269
|
+
edit meant for one memory from quietly becoming a second memory beside it.
|
|
270
|
+
|
|
252
271
|
Every memory/recall route also takes optional `space` (one of your namespaces by
|
|
253
|
-
name
|
|
272
|
+
name — your own, one shared with you, or an **alias** you set) and `space_id`
|
|
273
|
+
(canonical id, always unambiguous); a search takes a list of either, or
|
|
274
|
+
`space=all` — every namespace you belong to, plus `space=*` for every namespace
|
|
275
|
+
in the deployment if you are a superadmin — and each hit says which namespace
|
|
276
|
+
answered. Addressing a namespace that does not exist is an error, never a new
|
|
277
|
+
one. In
|
|
278
|
+
`open`/`managed` mode
|
|
254
279
|
the token goes in `Authorization: Bearer <token>` or `X-Memgres-Token`; there are
|
|
255
280
|
also request-access and `/admin/*` provisioning routes — see
|
|
256
281
|
[docs/TENANCY.md](docs/TENANCY.md). OpenAPI/Swagger is at `/docs`. Store errors
|
|
@@ -306,8 +331,8 @@ pip install "memgres[mcp]"
|
|
|
306
331
|
```
|
|
307
332
|
|
|
308
333
|
Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
|
|
309
|
-
`memory_list`, `
|
|
310
|
-
`memory_server_info`. Tell it *"remember X"* / *"what do you know about Y?"* and it
|
|
334
|
+
`memory_list`, `memory_tags`, `memory_links`, `memory_blame`, `memory_history`,
|
|
335
|
+
`memory_move`, `memory_forget`, `memory_server_info`. Tell it *"remember X"* / *"what do you know about Y?"* and it
|
|
311
336
|
calls them. (For semantic recall add the embedding env vars — see
|
|
312
337
|
[docs/BACKENDS.md](docs/BACKENDS.md).)
|
|
313
338
|
|
|
@@ -63,7 +63,7 @@ optional layers on top of the same core:
|
|
|
63
63
|
└─ MCP server write/recall/get/blame/move/forget as MCP tools
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
**Record model:** one memory = one mutable body (up to a configurable ceiling, default 256 KB) plus metadata — `tags` (cross-cutting labels, `text[]` + GIN), a `path` (its place in an `ltree` tree), timestamps, and per-diff provenance (`source`/`reason`, kept in history). A single write/diff is capped smaller (default 16 KB), so large bodies accrue over many authored diffs. **Organization is two orthogonal axes:** the tree is *where a memory lives* (one place, subtree-selectable); tags are *what it's about* (many, overlapping). Both filter either search — narrow a semantic query to a subtree, or list a tag across the tree.
|
|
66
|
+
**Record model:** one memory = one mutable body (up to a configurable ceiling, default 256 KB) plus metadata — `tags` (cross-cutting labels, `text[]` + GIN), a `path` (its place in an `ltree` tree), timestamps, and per-diff provenance (`source`/`reason`, kept in history). A single write/diff is capped smaller (default 16 KB), so large bodies accrue over many authored diffs. **Organization is two orthogonal axes:** the tree is *where a memory lives* (one place, subtree-selectable); tags are *what it's about* (many, overlapping). Both filter either search — narrow a semantic query to a subtree, or list a tag across the tree. **Links are the third axis:** `[[path]]` in a body becomes a real edge — walkable in BOTH directions, so "what relies on this?" has an answer before you change a fact — and a move rewrites the bodies that named the old address, so a link keeps working and an address copied out of a body is still one. Each memory also records how often it surfaces in search and how often it is opened, which is how the part of a corpus nobody reads becomes visible.
|
|
67
67
|
|
|
68
68
|
**Isolation:** optional multi-tenant identity keeps tenants from seeing each other's memories — users own *namespaces*, and rotatable, permission-scoped *tokens* authenticate as a user (turned on with `MEMGRES_KEY_MODE=open|managed`; see [docs/TENANCY.md](docs/TENANCY.md)). Encryption at rest is left to the deployment — Postgres/managed-PG/disk TDE stays transparent to queries, so search keeps working; memgres deliberately does **not** encrypt bodies application-side (that would make them unsearchable, which is why no comparable tool does it either). GDPR erasure is real: `forget()` hard-deletes the row, its vectors, and crypto-shreds the history chain. All limits are env-configurable, so the same code serves a single-user embed and a capped multi-tenant service.
|
|
69
69
|
|
|
@@ -80,7 +80,7 @@ docker compose up
|
|
|
80
80
|
|
|
81
81
|
Defaults suit a single-user setup with no auth. To change limits, the embedding provider, tokens, … drop a `.env` beside it — every `MEMGRES_*` is optional (see [Configuration](#configuration) or [.env.example](.env.example)).
|
|
82
82
|
|
|
83
|
-
**Give it to an LLM / agent — no code (MCP).** Point any URL-capable MCP client (Cursor, Cline, Claude Desktop, …) at the running server; the model gets `memory_write`, `memory_recall`, `memory_get`, `memory_list`, `memory_blame`, `memory_history`, `memory_move`, `memory_forget`, `memory_server_info` as tools:
|
|
83
|
+
**Give it to an LLM / agent — no code (MCP).** Point any URL-capable MCP client (Cursor, Cline, Claude Desktop, …) at the running server; the model gets `memory_write`, `memory_recall`, `memory_get`, `memory_list`, `memory_tags`, `memory_links`, `memory_blame`, `memory_history`, `memory_move`, `memory_forget`, `memory_server_info` as tools:
|
|
84
84
|
|
|
85
85
|
```json
|
|
86
86
|
{
|
|
@@ -155,7 +155,7 @@ Not sure which fits? Start with the decision guide: [docs/CHOOSING.md](docs/CHOO
|
|
|
155
155
|
1. **`docker compose up`** — `pgvector` + service, nothing to configure. For a dedicated vector service instead, `docker compose --profile qdrant up` and set `MEMGRES_VECTOR_BACKEND=qdrant` (Qdrant ranks vectors; Postgres still holds bodies and does tag/subtree/TTL filtering).
|
|
156
156
|
2. **Your own Postgres** — install the `[server]` extra (above), point `MEMGRES_DATABASE_URL` at it, run `memgres-server` (migrates on startup).
|
|
157
157
|
3. **Embedded library** — install the core package, use `Store` directly, no HTTP at all.
|
|
158
|
-
4. **Split service (many clients)** — a stateless API tier that only flags writes plus a scalable `memgres-worker` tier that embeds; see [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) and `deploy/docker-compose.yml`. Switch the embedding model later with `memgres-reembed`.
|
|
158
|
+
4. **Split service (many clients)** — a stateless API tier that only flags writes plus a scalable `memgres-worker` tier that embeds; see [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) and `deploy/docker-compose.yml`. Switch the embedding model later with `memgres-reembed`; rebuild the link graph with `memgres-relink`.
|
|
159
159
|
|
|
160
160
|
Semantic recall is optional: the default `MEMGRES_EMBED_PROVIDER=none` gives you lexical FTS with zero models. Turn on `local` (sentence-transformers), a cloud API (`openai`/`jina`), or any OpenAI-compatible server (LM Studio, Ollama, …) when you want meaning-based search — see [docs/EMBEDDINGS.md](docs/EMBEDDINGS.md) for choosing local vs cloud and [docs/BACKENDS.md](docs/BACKENDS.md) for copy-paste setups. The model id + dimension get stamped into the schema and a later mismatch hard-fails instead of silently returning garbage.
|
|
161
161
|
|
|
@@ -170,12 +170,16 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
170
170
|
| `MEMGRES_MAX_BODY_BYTES` | `262144` | ceiling for a whole record body (256 KB) |
|
|
171
171
|
| `MEMGRES_MAX_WRITE_BYTES` | `16384` | ceiling for one write/diff payload (≤ body) |
|
|
172
172
|
| `MEMGRES_MAX_SOURCE_BYTES` / `_MAX_REASON_BYTES` | `2048` / `1024` | ceilings for a write's `source` / `reason` provenance |
|
|
173
|
-
| `MEMGRES_RETENTION_DAYS` | `0` | `0` =
|
|
173
|
+
| `MEMGRES_RETENTION_DAYS` | `0` | how long data is kept: `0` = forever (off); `>0` = expire N days after **last touch**. Operator-only — there is no per-write TTL. Note the clock restarts on a touch, and a read counts as one unless `MEMGRES_RENEW_ON_READ=false`: the window covers data nobody uses |
|
|
174
174
|
| `MEMGRES_RENEW_ON_READ` | `true` | a read pushes the expiry clock forward |
|
|
175
|
+
| `MEMGRES_RETENTION_SWEEP` | `true` | this process runs the retention sweep. Every server process starts one; set `false` where a dedicated sweeper already runs |
|
|
176
|
+
| `MEMGRES_RETENTION_SWEEP_INTERVAL` | `3600` | seconds between sweeps that DELETE expired rows (and their vectors). Only runs when `RETENTION_DAYS > 0` |
|
|
177
|
+
| `MEMGRES_USAGE_COUNTERS` | `true` | count how often each memory surfaces in search and is read in full (`memory_usage`, a separate table — never the `memory` row, never the hash chain). Off makes reads pure again, for a read-only replica |
|
|
175
178
|
| `MEMGRES_KEY_MODE` | `single` | `single` (no auth, one space) · `open` (bring-your-own token, self-registers) · `managed` (admin-provisioned). See [docs/TENANCY.md](docs/TENANCY.md) |
|
|
176
179
|
| `MEMGRES_ADMIN_TOKEN` | — | global admin bearer for provisioning (managed mode) |
|
|
177
180
|
| `MEMGRES_TOKEN` | — | default token used when a call passes none (single-tenant endpoints) |
|
|
178
181
|
| `MEMGRES_TREE` | `true` | `ltree` path column + GiST index (fast subtree select) |
|
|
182
|
+
| `MEMGRES_REQUIRE_TITLE` | `true` | `true` = a write that stores content must supply `title`. Captions are what name a memory in results and what title-weighted ranking weighs; `move`/`retag` are exempt (they store no content) |
|
|
179
183
|
| `MEMGRES_REQUIRE_PARENT` | `false` | `true` = a node's parent path must already exist |
|
|
180
184
|
| `MEMGRES_HISTORY` | `true` | keep the hash-chained diff history (deleted with the record) |
|
|
181
185
|
| `MEMGRES_FTS_LANGUAGE` | `simple` | Postgres FTS dictionary (`simple`/`english`/…) |
|
|
@@ -184,6 +188,9 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
184
188
|
| `MEMGRES_FULL_BODY` | `false` | force the whole body on every hit (off = auto: short whole, long sliced); `full_body` per call overrides |
|
|
185
189
|
| `MEMGRES_FULL_BODY_MAX_CHARS` | `500` | a body this short is returned whole (`kind="full"`) instead of sliced |
|
|
186
190
|
| `MEMGRES_LIST_PREVIEW_CHARS` | `120` | first-line preview length returned by `memory_list` |
|
|
191
|
+
| `MEMGRES_LIST_BODIES_MAX_BYTES` | `200000` | total body bytes one `bodies=true` browse returns; rows past the cap come back marked `body_omitted`, never dropped |
|
|
192
|
+
| `MEMGRES_MCP_ADMIN_TOOLS` | `auto` | register the `memory_admin_*` control-plane tools over MCP; `auto` = every mode but `single`. A context economy, not a security boundary — each tool authorizes on call |
|
|
193
|
+
| `MEMGRES_MCP_TOOL_VISIBILITY` | `auto` | show each client only the tools its own credential can use, and drop the identity tools where there are no identities (`single`). Display, not authorization — every tool still authorizes on call; `off` lists everything |
|
|
187
194
|
| `MEMGRES_INSTRUCTION` | — | server-side MCP instructions emitted at `initialize` (a client like Claude Code loads it once at connect); unset = omitted; capped at 2 KB |
|
|
188
195
|
| `MEMGRES_VECTOR_BACKEND` | `pgvector` | `pgvector` (same DB) or `qdrant` (set `QDRANT_URL`, `QDRANT_API_KEY`, `MEMGRES_QDRANT_COLLECTION`) |
|
|
189
196
|
| `MEMGRES_EMBED_PROVIDER` | `none` | `none` / `local` / `openai` / `jina` / `openai-compatible` (LM Studio, Ollama, vLLM, TEI…) |
|
|
@@ -208,13 +215,31 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
208
215
|
| `GET` | `/memories/{id}/blame` | line attribution; `?group`, `?text`, `?lines=1,3-5` |
|
|
209
216
|
| `GET` | `/memories/{id}/at/{seq}` | body reconstructed at a version |
|
|
210
217
|
| `GET` | `/recall` | `?q=&k=&mode=&tags=&path_prefix=&match=&snippet=&full_body=` |
|
|
211
|
-
| `GET` | `/memories` | list a subtree, no query: `?path_prefix=&tags=&limit=&offset=` |
|
|
218
|
+
| `GET` | `/memories` | list a subtree, no query: `?path_prefix=&tags=&limit=&offset=&bodies=` |
|
|
219
|
+
| `GET` | `/whoami` | what this token may do, as capabilities |
|
|
220
|
+
| `GET` | `/admin/orphans` | memories stranded by a `single` → managed switch |
|
|
221
|
+
| `POST` | `/admin/adopt-orphans` | move them into a real namespace (idempotent) |
|
|
212
222
|
| `GET` | `/spaces` | namespaces this token can reach (identity modes) |
|
|
223
|
+
| `POST` | `/spaces` | create one of your own (nothing is created by being named) |
|
|
224
|
+
| `POST` | `/spaces/aliases` | give a reachable namespace a name of your own |
|
|
213
225
|
| `GET` | `/info` | effective config: limits, embed provider/model/dim, recall modes, backend |
|
|
214
226
|
| `GET` | `/healthz` | liveness |
|
|
215
227
|
|
|
228
|
+
**`{id}` may be a memory's uuid or its tree path** (`/memories/decisions.pricing`)
|
|
229
|
+
— the segment is read as an id when it parses as a uuid, and as a path
|
|
230
|
+
otherwise, so hyphenated and non-ASCII paths (`ops.rate-limits`) address fine.
|
|
231
|
+
A path that a memory has since moved away from is followed on a read
|
|
232
|
+
(the answer sets `moved_from`) and refused on a write, which is what stops an
|
|
233
|
+
edit meant for one memory from quietly becoming a second memory beside it.
|
|
234
|
+
|
|
216
235
|
Every memory/recall route also takes optional `space` (one of your namespaces by
|
|
217
|
-
name
|
|
236
|
+
name — your own, one shared with you, or an **alias** you set) and `space_id`
|
|
237
|
+
(canonical id, always unambiguous); a search takes a list of either, or
|
|
238
|
+
`space=all` — every namespace you belong to, plus `space=*` for every namespace
|
|
239
|
+
in the deployment if you are a superadmin — and each hit says which namespace
|
|
240
|
+
answered. Addressing a namespace that does not exist is an error, never a new
|
|
241
|
+
one. In
|
|
242
|
+
`open`/`managed` mode
|
|
218
243
|
the token goes in `Authorization: Bearer <token>` or `X-Memgres-Token`; there are
|
|
219
244
|
also request-access and `/admin/*` provisioning routes — see
|
|
220
245
|
[docs/TENANCY.md](docs/TENANCY.md). OpenAPI/Swagger is at `/docs`. Store errors
|
|
@@ -270,8 +295,8 @@ pip install "memgres[mcp]"
|
|
|
270
295
|
```
|
|
271
296
|
|
|
272
297
|
Either way the model gets tools `memory_write`, `memory_recall`, `memory_get`,
|
|
273
|
-
`memory_list`, `
|
|
274
|
-
`memory_server_info`. Tell it *"remember X"* / *"what do you know about Y?"* and it
|
|
298
|
+
`memory_list`, `memory_tags`, `memory_links`, `memory_blame`, `memory_history`,
|
|
299
|
+
`memory_move`, `memory_forget`, `memory_server_info`. Tell it *"remember X"* / *"what do you know about Y?"* and it
|
|
275
300
|
calls them. (For semantic recall add the embedding env vars — see
|
|
276
301
|
[docs/BACKENDS.md](docs/BACKENDS.md).)
|
|
277
302
|
|
|
@@ -22,12 +22,14 @@ from .embeddings import Embedder, get_embedder
|
|
|
22
22
|
from .schema import migrate, SchemaMismatch, SCHEMA_VERSION
|
|
23
23
|
from .search import Hit, recall
|
|
24
24
|
from .blame import annotate, annotate_grouped, reconstruct, replay
|
|
25
|
-
from .store import Store, Memory, Conflict, NotFound, TooLarge, NoParent
|
|
25
|
+
from .store import (Store, Memory, Conflict, NotFound, TooLarge, NoParent,
|
|
26
|
+
MissingTitle)
|
|
26
27
|
from .identity import (
|
|
27
28
|
Principal, AuthError, SpaceNotFound,
|
|
28
29
|
resolve, resolve_space, new_token, valid_format,
|
|
29
30
|
create_user, create_namespace, list_spaces,
|
|
30
|
-
|
|
31
|
+
list_users, list_namespaces, list_members,
|
|
32
|
+
issue_token, register_token, revoke_token, list_tokens, token_owner,
|
|
31
33
|
request_access, approve_request, deny_request, list_requests,
|
|
32
34
|
)
|
|
33
35
|
|
|
@@ -35,6 +37,7 @@ __all__ = [
|
|
|
35
37
|
"__version__",
|
|
36
38
|
"Config", "load_config",
|
|
37
39
|
"Store", "Memory", "Conflict", "NotFound", "TooLarge", "NoParent",
|
|
40
|
+
"MissingTitle",
|
|
38
41
|
"make_diff", "apply_diff", "content_hash", "DiffConflict",
|
|
39
42
|
"Embedder", "get_embedder",
|
|
40
43
|
"migrate", "SchemaMismatch", "SCHEMA_VERSION",
|
|
@@ -43,6 +46,7 @@ __all__ = [
|
|
|
43
46
|
"Principal", "AuthError", "SpaceNotFound",
|
|
44
47
|
"resolve", "resolve_space", "new_token", "valid_format",
|
|
45
48
|
"create_user", "create_namespace", "list_spaces",
|
|
46
|
-
"
|
|
49
|
+
"list_users", "list_namespaces", "list_members",
|
|
50
|
+
"issue_token", "register_token", "revoke_token", "list_tokens", "token_owner",
|
|
47
51
|
"request_access", "approve_request", "deny_request", "list_requests",
|
|
48
52
|
]
|