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.
Files changed (109) hide show
  1. {memgres-0.5.2 → memgres-0.7.0}/PKG-INFO +34 -9
  2. {memgres-0.5.2 → memgres-0.7.0}/README.md +33 -8
  3. {memgres-0.5.2 → memgres-0.7.0}/memgres/__init__.py +7 -3
  4. {memgres-0.5.2 → memgres-0.7.0}/memgres/_version.py +1 -1
  5. memgres-0.7.0/memgres/admin.py +552 -0
  6. {memgres-0.5.2 → memgres-0.7.0}/memgres/admin_cli.py +8 -7
  7. {memgres-0.5.2 → memgres-0.7.0}/memgres/config.py +17 -0
  8. memgres-0.7.0/memgres/delimiters.py +88 -0
  9. {memgres-0.5.2 → memgres-0.7.0}/memgres/embed_worker.py +31 -58
  10. memgres-0.7.0/memgres/identity.py +1252 -0
  11. {memgres-0.5.2 → memgres-0.7.0}/memgres/info.py +3 -0
  12. memgres-0.7.0/memgres/lines.py +65 -0
  13. memgres-0.7.0/memgres/links.py +188 -0
  14. memgres-0.7.0/memgres/mcp_server.py +1079 -0
  15. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0002_identity.sql +13 -3
  16. memgres-0.7.0/memgres/migrations/0009_create_namespace_right.sql +23 -0
  17. memgres-0.7.0/memgres/migrations/0010_namespace_alias.sql +24 -0
  18. memgres-0.7.0/memgres/migrations/0011_drop_default_namespace.sql +23 -0
  19. memgres-0.7.0/memgres/migrations/0012_user_profile.sql +30 -0
  20. memgres-0.7.0/memgres/migrations/0013_hash_version.sql +28 -0
  21. memgres-0.7.0/memgres/migrations/0014_access_request_no_fk.sql +24 -0
  22. memgres-0.7.0/memgres/migrations/0015_normalize_tags.sql +44 -0
  23. memgres-0.7.0/memgres/migrations/0016_valid_at.sql +16 -0
  24. memgres-0.7.0/memgres/migrations/0017_memory_link.sql +40 -0
  25. memgres-0.7.0/memgres/migrations/0018_links_built.sql +12 -0
  26. memgres-0.7.0/memgres/migrations/0019_memory_usage.sql +36 -0
  27. memgres-0.7.0/memgres/migrations/0020_memory_usage_no_fk.sql +17 -0
  28. memgres-0.7.0/memgres/periodic.py +153 -0
  29. memgres-0.7.0/memgres/relink.py +157 -0
  30. {memgres-0.5.2 → memgres-0.7.0}/memgres/schema.py +60 -2
  31. {memgres-0.5.2 → memgres-0.7.0}/memgres/search.py +62 -39
  32. memgres-0.7.0/memgres/server.py +651 -0
  33. memgres-0.7.0/memgres/store.py +1706 -0
  34. memgres-0.7.0/memgres/tags.py +90 -0
  35. {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/base.py +73 -19
  36. {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/pgvector.py +18 -6
  37. {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/qdrant.py +42 -7
  38. {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/PKG-INFO +34 -9
  39. {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/SOURCES.txt +31 -1
  40. {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/entry_points.txt +1 -0
  41. {memgres-0.5.2 → memgres-0.7.0}/pyproject.toml +1 -0
  42. {memgres-0.5.2 → memgres-0.7.0}/tests/test_blame_integration.py +3 -0
  43. {memgres-0.5.2 → memgres-0.7.0}/tests/test_chunk_index.py +146 -0
  44. {memgres-0.5.2 → memgres-0.7.0}/tests/test_claim_and_reembed.py +3 -0
  45. {memgres-0.5.2 → memgres-0.7.0}/tests/test_diffing.py +29 -4
  46. {memgres-0.5.2 → memgres-0.7.0}/tests/test_embed_worker.py +3 -0
  47. memgres-0.7.0/tests/test_identity_integration.py +592 -0
  48. {memgres-0.5.2 → memgres-0.7.0}/tests/test_lexical_match.py +3 -0
  49. {memgres-0.5.2 → memgres-0.7.0}/tests/test_limits.py +9 -0
  50. memgres-0.7.0/tests/test_links.py +738 -0
  51. {memgres-0.5.2 → memgres-0.7.0}/tests/test_list.py +72 -1
  52. memgres-0.7.0/tests/test_mcp_admin_tools.py +329 -0
  53. {memgres-0.5.2 → memgres-0.7.0}/tests/test_mcp_instructions.py +5 -2
  54. memgres-0.7.0/tests/test_mcp_recall_schema.py +128 -0
  55. memgres-0.7.0/tests/test_mcp_tool_visibility.py +349 -0
  56. {memgres-0.5.2 → memgres-0.7.0}/tests/test_migration_upgrade.py +43 -0
  57. memgres-0.7.0/tests/test_multi_space_search.py +371 -0
  58. memgres-0.7.0/tests/test_path_addressing.py +226 -0
  59. {memgres-0.5.2 → memgres-0.7.0}/tests/test_qdrant_integration.py +3 -0
  60. memgres-0.7.0/tests/test_require_title.py +183 -0
  61. memgres-0.7.0/tests/test_retention.py +270 -0
  62. {memgres-0.5.2 → memgres-0.7.0}/tests/test_roles_bootstrap.py +125 -0
  63. {memgres-0.5.2 → memgres-0.7.0}/tests/test_search_integration.py +63 -0
  64. memgres-0.7.0/tests/test_security_integration.py +706 -0
  65. {memgres-0.5.2 → memgres-0.7.0}/tests/test_segments_store.py +7 -0
  66. {memgres-0.5.2 → memgres-0.7.0}/tests/test_server_info.py +31 -1
  67. {memgres-0.5.2 → memgres-0.7.0}/tests/test_server_integration.py +209 -6
  68. {memgres-0.5.2 → memgres-0.7.0}/tests/test_snippets.py +3 -0
  69. {memgres-0.5.2 → memgres-0.7.0}/tests/test_store_integration.py +200 -9
  70. memgres-0.7.0/tests/test_tags.py +268 -0
  71. memgres-0.7.0/tests/test_usage.py +328 -0
  72. memgres-0.7.0/tests/test_valid_at.py +201 -0
  73. memgres-0.7.0/tests/test_write_ergonomics.py +215 -0
  74. memgres-0.5.2/memgres/identity.py +0 -559
  75. memgres-0.5.2/memgres/mcp_server.py +0 -446
  76. memgres-0.5.2/memgres/server.py +0 -514
  77. memgres-0.5.2/memgres/store.py +0 -664
  78. memgres-0.5.2/tests/test_identity_integration.py +0 -325
  79. memgres-0.5.2/tests/test_mcp_recall_schema.py +0 -74
  80. memgres-0.5.2/tests/test_security_integration.py +0 -263
  81. {memgres-0.5.2 → memgres-0.7.0}/LICENSE +0 -0
  82. {memgres-0.5.2 → memgres-0.7.0}/memgres/blame.py +0 -0
  83. {memgres-0.5.2 → memgres-0.7.0}/memgres/bootstrap.py +0 -0
  84. {memgres-0.5.2 → memgres-0.7.0}/memgres/diffing.py +0 -0
  85. {memgres-0.5.2 → memgres-0.7.0}/memgres/embeddings.py +0 -0
  86. {memgres-0.5.2 → memgres-0.7.0}/memgres/healthcheck.py +0 -0
  87. {memgres-0.5.2 → memgres-0.7.0}/memgres/indexing.py +0 -0
  88. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0001_core.sql +0 -0
  89. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0003_history_author.sql +0 -0
  90. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0004_title.sql +0 -0
  91. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0005_chunk_index.sql +0 -0
  92. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0006_reader_floor.sql +0 -0
  93. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0007_embed_retry.sql +0 -0
  94. {memgres-0.5.2 → memgres-0.7.0}/memgres/migrations/0008_service_roles.sql +0 -0
  95. {memgres-0.5.2 → memgres-0.7.0}/memgres/reembed.py +0 -0
  96. {memgres-0.5.2 → memgres-0.7.0}/memgres/segments.py +0 -0
  97. {memgres-0.5.2 → memgres-0.7.0}/memgres/vector/__init__.py +0 -0
  98. {memgres-0.5.2 → memgres-0.7.0}/memgres/worker.py +0 -0
  99. {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/dependency_links.txt +0 -0
  100. {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/requires.txt +0 -0
  101. {memgres-0.5.2 → memgres-0.7.0}/memgres.egg-info/top_level.txt +0 -0
  102. {memgres-0.5.2 → memgres-0.7.0}/setup.cfg +0 -0
  103. {memgres-0.5.2 → memgres-0.7.0}/tests/test_config.py +0 -0
  104. {memgres-0.5.2 → memgres-0.7.0}/tests/test_embeddings.py +0 -0
  105. {memgres-0.5.2 → memgres-0.7.0}/tests/test_healthcheck.py +0 -0
  106. {memgres-0.5.2 → memgres-0.7.0}/tests/test_mcp_http_transport.py +0 -0
  107. {memgres-0.5.2 → memgres-0.7.0}/tests/test_qdrant_ca.py +0 -0
  108. {memgres-0.5.2 → memgres-0.7.0}/tests/test_replace_build.py +0 -0
  109. {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.5.2
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` = keep forever (TTL off); `>0` = expire N days after last touch |
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) and `space_id` (canonical id, for shared spaces). In `open`/`managed` mode
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`, `memory_blame`, `memory_history`, `memory_move`, `memory_forget`,
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` = keep forever (TTL off); `>0` = expire N days after last touch |
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) and `space_id` (canonical id, for shared spaces). In `open`/`managed` mode
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`, `memory_blame`, `memory_history`, `memory_move`, `memory_forget`,
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
- issue_token, register_token, revoke_token, list_tokens,
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
- "issue_token", "register_token", "revoke_token", "list_tokens",
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
  ]
@@ -8,4 +8,4 @@ here at release; nowhere else carries the number.
8
8
  PEP 440: a ``.devN`` suffix marks an unreleased build ahead of the last tag.
9
9
  """
10
10
 
11
- __version__ = "0.5.2"
11
+ __version__ = "0.7.0"