yourmemory 1.4.77__tar.gz → 1.4.78__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 (61) hide show
  1. {yourmemory-1.4.77 → yourmemory-1.4.78}/PKG-INFO +1 -1
  2. {yourmemory-1.4.77 → yourmemory-1.4.78}/pyproject.toml +1 -1
  3. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/app.py +2 -1
  4. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/migrate.py +73 -0
  5. yourmemory-1.4.78/src/routes/pools.py +236 -0
  6. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/retrieve.py +29 -4
  7. {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/PKG-INFO +1 -1
  8. {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/SOURCES.txt +1 -0
  9. {yourmemory-1.4.77 → yourmemory-1.4.78}/LICENSE +0 -0
  10. {yourmemory-1.4.77 → yourmemory-1.4.78}/README.md +0 -0
  11. {yourmemory-1.4.77 → yourmemory-1.4.78}/memory_mcp.py +0 -0
  12. {yourmemory-1.4.77 → yourmemory-1.4.78}/setup.cfg +0 -0
  13. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/__init__.py +0 -0
  14. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/connection.py +0 -0
  15. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/duckdb_schema.sql +0 -0
  16. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/schema.sql +0 -0
  17. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/db/sqlite_schema.sql +0 -0
  18. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/__init__.py +0 -0
  19. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/backend.py +0 -0
  20. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/graph_store.py +0 -0
  21. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/neo4j_backend.py +0 -0
  22. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/networkx_backend.py +0 -0
  23. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/graph/svo_extract.py +0 -0
  24. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/__init__.py +0 -0
  25. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_observe.py +0 -0
  26. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_recall.py +0 -0
  27. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_recall.sh +0 -0
  28. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_server.py +0 -0
  29. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_store.py +0 -0
  30. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/hook_templates/yourmemory_user.sh +0 -0
  31. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/jobs/decay_job.py +0 -0
  32. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/__init__.py +0 -0
  33. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/agents.py +0 -0
  34. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/audit.py +0 -0
  35. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/compact.py +0 -0
  36. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/dsar.py +0 -0
  37. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/graph_viz.py +0 -0
  38. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/memories.py +0 -0
  39. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/proxy.py +0 -0
  40. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/routes/ui.py +0 -0
  41. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/__init__.py +0 -0
  42. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/agent_registry.py +0 -0
  43. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/api_keys.py +0 -0
  44. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/audit.py +0 -0
  45. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/compaction.py +0 -0
  46. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/decay.py +0 -0
  47. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/embed.py +0 -0
  48. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/extract.py +0 -0
  49. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/extract_fallback.py +0 -0
  50. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/resolve.py +0 -0
  51. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/resolve_fallback.py +0 -0
  52. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/retrieve.py +0 -0
  53. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/session.py +0 -0
  54. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/temporal.py +0 -0
  55. {yourmemory-1.4.77 → yourmemory-1.4.78}/src/services/utils.py +0 -0
  56. {yourmemory-1.4.77 → yourmemory-1.4.78}/tests/test_features.py +0 -0
  57. {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/dependency_links.txt +0 -0
  58. {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/entry_points.txt +0 -0
  59. {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/requires.txt +0 -0
  60. {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory.egg-info/top_level.txt +0 -0
  61. {yourmemory-1.4.77 → yourmemory-1.4.78}/yourmemory_run.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yourmemory
3
- Version: 1.4.77
3
+ Version: 1.4.78
4
4
  Summary: Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native
5
5
  Author: Sachit Misra
6
6
  Author-email: mishrasachit1@gmail.com
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "yourmemory"
7
- version = "1.4.77"
7
+ version = "1.4.78"
8
8
  description = "Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -13,7 +13,7 @@ from collections import defaultdict, deque
13
13
  from threading import Lock
14
14
  from fastapi import Request
15
15
  from fastapi.responses import JSONResponse
16
- from src.routes import memories, retrieve, agents, ui, graph_viz, proxy, audit, dsar, compact
16
+ from src.routes import memories, retrieve, agents, ui, graph_viz, proxy, audit, dsar, compact, pools
17
17
  from src.jobs.decay_job import run as run_decay
18
18
  from src.services.audit import prune_expired as prune_audit
19
19
  from src.db.migrate import migrate
@@ -52,6 +52,7 @@ app.include_router(proxy.router)
52
52
  app.include_router(audit.router)
53
53
  app.include_router(dsar.router)
54
54
  app.include_router(compact.router)
55
+ app.include_router(pools.router)
55
56
 
56
57
 
57
58
  # ── Rate limiting (abuse prevention) ────────────────────────────────────────────
@@ -220,6 +220,76 @@ def _create_archive_table(conn, backend: str) -> None:
220
220
  """)
221
221
 
222
222
 
223
+ def _create_pool_tables(conn, backend: str) -> None:
224
+ """Shared memory pools (team / institutional memory). A pool's memories live in the
225
+ `memories` table under a namespaced user_id ('pool:<id>'), so they reuse all existing
226
+ machinery (embedding, dedup, recall, decay, compaction, audit). These two tables hold
227
+ the pool registry and role-based membership. Idempotent across backends."""
228
+ if backend == "postgres":
229
+ cur = conn.cursor()
230
+ cur.execute("""
231
+ CREATE TABLE IF NOT EXISTS pools (
232
+ pool_id TEXT PRIMARY KEY,
233
+ name TEXT,
234
+ owner TEXT,
235
+ created_at TIMESTAMPTZ DEFAULT NOW()
236
+ );
237
+ CREATE TABLE IF NOT EXISTS pool_members (
238
+ pool_id TEXT NOT NULL,
239
+ member_id TEXT NOT NULL,
240
+ role TEXT NOT NULL DEFAULT 'reader',
241
+ can_read BOOLEAN NOT NULL DEFAULT TRUE,
242
+ can_write BOOLEAN NOT NULL DEFAULT FALSE,
243
+ added_at TIMESTAMPTZ DEFAULT NOW(),
244
+ PRIMARY KEY (pool_id, member_id)
245
+ );
246
+ CREATE INDEX IF NOT EXISTS idx_pool_member ON pool_members(member_id);
247
+ """)
248
+ conn.commit()
249
+ cur.close()
250
+ elif backend == "duckdb":
251
+ try:
252
+ conn.execute("""
253
+ CREATE TABLE IF NOT EXISTS pools (
254
+ pool_id VARCHAR PRIMARY KEY,
255
+ name VARCHAR,
256
+ owner VARCHAR,
257
+ created_at TIMESTAMP DEFAULT now()
258
+ )""")
259
+ conn.execute("""
260
+ CREATE TABLE IF NOT EXISTS pool_members (
261
+ pool_id VARCHAR NOT NULL,
262
+ member_id VARCHAR NOT NULL,
263
+ role VARCHAR NOT NULL DEFAULT 'reader',
264
+ can_read BOOLEAN NOT NULL DEFAULT TRUE,
265
+ can_write BOOLEAN NOT NULL DEFAULT FALSE,
266
+ added_at TIMESTAMP DEFAULT now(),
267
+ PRIMARY KEY (pool_id, member_id)
268
+ )""")
269
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_pool_member ON pool_members(member_id)")
270
+ except Exception as exc:
271
+ print(f"pool tables (duckdb) skipped: {exc}", file=sys.stderr)
272
+ else: # sqlite
273
+ conn.executescript("""
274
+ CREATE TABLE IF NOT EXISTS pools (
275
+ pool_id TEXT PRIMARY KEY,
276
+ name TEXT,
277
+ owner TEXT,
278
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
279
+ );
280
+ CREATE TABLE IF NOT EXISTS pool_members (
281
+ pool_id TEXT NOT NULL,
282
+ member_id TEXT NOT NULL,
283
+ role TEXT NOT NULL DEFAULT 'reader',
284
+ can_read INTEGER NOT NULL DEFAULT 1,
285
+ can_write INTEGER NOT NULL DEFAULT 0,
286
+ added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
287
+ PRIMARY KEY (pool_id, member_id)
288
+ );
289
+ CREATE INDEX IF NOT EXISTS idx_pool_member ON pool_members(member_id);
290
+ """)
291
+
292
+
223
293
  def migrate():
224
294
  backend = get_backend()
225
295
 
@@ -262,6 +332,9 @@ def migrate():
262
332
  # ── Archive of originals compressed into summaries (memory compaction) ──
263
333
  _create_archive_table(conn, backend)
264
334
 
335
+ # ── Shared memory pools (team / institutional memory) ──
336
+ _create_pool_tables(conn, backend)
337
+
265
338
  # ── Post-schema FTS setup ─────────────────────────────────────────────
266
339
  if backend == "sqlite":
267
340
  # Backfill any rows that existed before the FTS table was created.
@@ -0,0 +1,236 @@
1
+ """
2
+ Shared memory pools — team / institutional memory.
3
+
4
+ A pool is a shared namespace many agents contribute to and everyone with access can
5
+ query. Individual agents still write their own personal memories; the pool is the
6
+ team-level layer on top. Pool memories are stored in the `memories` table under a
7
+ namespaced user_id (`pool:<id>`), so they reuse embedding, dedup, recall, decay,
8
+ compaction, and audit for free.
9
+
10
+ POST /pools create a pool
11
+ GET /pools list pools
12
+ POST /pools/{id}/members add a member with a role (reader/contributor/admin)
13
+ GET /pools/{id}/members list members
14
+ POST /pools/{id}/memories write a memory into the pool (writer role required)
15
+ GET /pools/{id}/memories list pool memories (reader role required)
16
+ POST /pools/{id}/retrieve semantic search within the pool
17
+
18
+ Role → permissions: reader = read · contributor = read+write · admin = read+write+manage.
19
+
20
+ > NOTE: in the default local deployment the caller identity (`memberId`) is supplied by
21
+ > the client and access checks are advisory — real enforcement arrives with hosted auth.
22
+ > The pool model, membership, and union recall are the durable substrate that hosted
23
+ > RBAC will enforce.
24
+ """
25
+
26
+ from fastapi import APIRouter, HTTPException, Query
27
+ from pydantic import BaseModel
28
+ from typing import Optional, List
29
+
30
+ from src.db.connection import get_backend, get_conn, duckdb_rows
31
+ from src.services.audit import log_event
32
+
33
+ router = APIRouter()
34
+
35
+ POOL_NS = "pool:" # memories namespace prefix
36
+ _ROLE_PERMS = {
37
+ "reader": (True, False),
38
+ "contributor": (True, True),
39
+ "admin": (True, True),
40
+ }
41
+
42
+
43
+ def _ns(pool_id: str) -> str:
44
+ return f"{POOL_NS}{pool_id.strip().lower()}"
45
+
46
+
47
+ def _ph(backend: str) -> str:
48
+ return "%s" if backend == "postgres" else "?"
49
+
50
+
51
+ def _member(conn, backend: str, pool_id: str, member_id: str) -> Optional[dict]:
52
+ p = _ph(backend)
53
+ sql = f"SELECT role, can_read, can_write FROM pool_members WHERE pool_id = {p} AND member_id = {p}"
54
+ args = (pool_id, member_id)
55
+ if backend == "duckdb":
56
+ rows = duckdb_rows(conn.execute(sql, list(args)))
57
+ return rows[0] if rows else None
58
+ cur = conn.cursor(); cur.execute(sql, args)
59
+ row = cur.fetchone(); cur.close()
60
+ if not row:
61
+ return None
62
+ return {"role": row[0], "can_read": bool(row[1]), "can_write": bool(row[2])}
63
+
64
+
65
+ def _require(pool_id: str, member_id: str, need: str):
66
+ """Advisory access check. need = 'read' | 'write'. Returns the membership dict."""
67
+ if not member_id:
68
+ raise HTTPException(403, "memberId required to access a pool.")
69
+ backend = get_backend(); conn = get_conn()
70
+ try:
71
+ m = _member(conn, backend, pool_id.strip().lower(), member_id.strip().lower())
72
+ finally:
73
+ conn.close()
74
+ if not m:
75
+ raise HTTPException(403, f"'{member_id}' is not a member of pool '{pool_id}'.")
76
+ if need == "read" and not m["can_read"]:
77
+ raise HTTPException(403, "Member lacks read access to this pool.")
78
+ if need == "write" and not m["can_write"]:
79
+ raise HTTPException(403, "Member lacks write access to this pool.")
80
+ return m
81
+
82
+
83
+ # ── Create / list pools ─────────────────────────────────────────────────────────
84
+
85
+ class CreatePoolRequest(BaseModel):
86
+ pool_id: str
87
+ name: Optional[str] = ""
88
+ owner: str
89
+
90
+
91
+ @router.post("/pools")
92
+ def create_pool(req: CreatePoolRequest):
93
+ pool_id = req.pool_id.strip().lower()
94
+ owner = req.owner.strip().lower()
95
+ if not pool_id or not owner:
96
+ raise HTTPException(422, "pool_id and owner are required.")
97
+ backend = get_backend(); conn = get_conn(); p = _ph(backend)
98
+ try:
99
+ # Create the pool and make the owner an admin member.
100
+ if backend == "duckdb":
101
+ conn.execute(f"INSERT INTO pools (pool_id, name, owner) VALUES (?,?,?) ON CONFLICT (pool_id) DO NOTHING",
102
+ [pool_id, req.name, owner])
103
+ conn.execute("INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
104
+ "VALUES (?,?,?,?,?) ON CONFLICT (pool_id, member_id) DO UPDATE SET role=excluded.role",
105
+ [pool_id, owner, "admin", True, True])
106
+ else:
107
+ cur = conn.cursor()
108
+ cur.execute(f"INSERT INTO pools (pool_id, name, owner) VALUES ({p},{p},{p}) ON CONFLICT (pool_id) DO NOTHING",
109
+ (pool_id, req.name, owner))
110
+ cur.execute(f"INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
111
+ f"VALUES ({p},{p},{p},{p},{p}) ON CONFLICT (pool_id, member_id) DO UPDATE SET role=excluded.role",
112
+ (pool_id, owner, "admin", True, True))
113
+ conn.commit(); cur.close()
114
+ finally:
115
+ conn.close()
116
+ log_event("write", "pool_create", owner, detail={"pool_id": pool_id})
117
+ return {"pool_id": pool_id, "name": req.name, "owner": owner, "role": "admin"}
118
+
119
+
120
+ @router.get("/pools")
121
+ def list_pools(memberId: Optional[str] = Query(None, description="Filter to pools this member belongs to")):
122
+ backend = get_backend(); conn = get_conn(); p = _ph(backend)
123
+ try:
124
+ if memberId:
125
+ mid = memberId.strip().lower()
126
+ sql = (f"SELECT p.pool_id, p.name, p.owner, m.role FROM pools p "
127
+ f"JOIN pool_members m ON p.pool_id = m.pool_id WHERE m.member_id = {p} ORDER BY p.pool_id")
128
+ args = (mid,)
129
+ else:
130
+ sql = "SELECT pool_id, name, owner, '' AS role FROM pools ORDER BY pool_id"
131
+ args = ()
132
+ if backend == "duckdb":
133
+ rows = duckdb_rows(conn.execute(sql, list(args)))
134
+ else:
135
+ cur = conn.cursor(); cur.execute(sql, args)
136
+ cols = [d[0] for d in cur.description]
137
+ rows = [dict(zip(cols, r)) for r in cur.fetchall()]; cur.close()
138
+ finally:
139
+ conn.close()
140
+ return {"count": len(rows), "pools": rows}
141
+
142
+
143
+ # ── Membership ──────────────────────────────────────────────────────────────────
144
+
145
+ class AddMemberRequest(BaseModel):
146
+ member_id: str
147
+ role: str = "reader" # reader | contributor | admin
148
+
149
+
150
+ @router.post("/pools/{pool_id}/members")
151
+ def add_member(pool_id: str, req: AddMemberRequest):
152
+ pool_id = pool_id.strip().lower()
153
+ member_id = req.member_id.strip().lower()
154
+ role = req.role.strip().lower()
155
+ if role not in _ROLE_PERMS:
156
+ raise HTTPException(422, f"role must be one of {list(_ROLE_PERMS)}")
157
+ can_read, can_write = _ROLE_PERMS[role]
158
+ backend = get_backend(); conn = get_conn(); p = _ph(backend)
159
+ try:
160
+ if backend == "duckdb":
161
+ conn.execute("INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
162
+ "VALUES (?,?,?,?,?) ON CONFLICT (pool_id, member_id) DO UPDATE SET "
163
+ "role=excluded.role, can_read=excluded.can_read, can_write=excluded.can_write",
164
+ [pool_id, member_id, role, can_read, can_write])
165
+ else:
166
+ cur = conn.cursor()
167
+ cur.execute(f"INSERT INTO pool_members (pool_id, member_id, role, can_read, can_write) "
168
+ f"VALUES ({p},{p},{p},{p},{p}) ON CONFLICT (pool_id, member_id) DO UPDATE SET "
169
+ f"role=excluded.role, can_read=excluded.can_read, can_write=excluded.can_write",
170
+ (pool_id, member_id, role, can_read, can_write))
171
+ conn.commit(); cur.close()
172
+ finally:
173
+ conn.close()
174
+ return {"pool_id": pool_id, "member_id": member_id, "role": role,
175
+ "can_read": can_read, "can_write": can_write}
176
+
177
+
178
+ @router.get("/pools/{pool_id}/members")
179
+ def list_members(pool_id: str):
180
+ pool_id = pool_id.strip().lower()
181
+ backend = get_backend(); conn = get_conn(); p = _ph(backend)
182
+ sql = f"SELECT member_id, role, can_read, can_write FROM pool_members WHERE pool_id = {p} ORDER BY member_id"
183
+ try:
184
+ if backend == "duckdb":
185
+ rows = duckdb_rows(conn.execute(sql, [pool_id]))
186
+ else:
187
+ cur = conn.cursor(); cur.execute(sql, (pool_id,))
188
+ cols = [d[0] for d in cur.description]
189
+ rows = [dict(zip(cols, r)) for r in cur.fetchall()]; cur.close()
190
+ finally:
191
+ conn.close()
192
+ return {"pool_id": pool_id, "count": len(rows), "members": rows}
193
+
194
+
195
+ # ── Pool memories: write / list / retrieve (reuse the memory engine) ────────────
196
+
197
+ class PoolMemoryRequest(BaseModel):
198
+ memberId: str
199
+ content: str
200
+ importance: float = 0.5
201
+ category: Optional[str] = None
202
+
203
+
204
+ @router.post("/pools/{pool_id}/memories")
205
+ def write_pool_memory(pool_id: str, req: PoolMemoryRequest):
206
+ _require(pool_id, req.memberId, "write")
207
+ from src.routes.memories import add_memory, MemoryRequest
208
+ result = add_memory(MemoryRequest(
209
+ userId=_ns(pool_id), content=req.content, importance=req.importance))
210
+ log_event("write", "pool_store", req.memberId.strip().lower(),
211
+ detail={"pool_id": pool_id.strip().lower(), "id": result.get("id")})
212
+ return {"pool_id": pool_id.strip().lower(), **result}
213
+
214
+
215
+ @router.get("/pools/{pool_id}/memories")
216
+ def list_pool_memories(pool_id: str, memberId: str = Query(...), limit: int = Query(50, ge=1, le=500)):
217
+ _require(pool_id, memberId, "read")
218
+ from src.routes.memories import list_memories
219
+ return list_memories(userId=_ns(pool_id), limit=limit)
220
+
221
+
222
+ class PoolRetrieveRequest(BaseModel):
223
+ memberId: str
224
+ query: str
225
+ topK: int = 6
226
+
227
+
228
+ @router.post("/pools/{pool_id}/retrieve")
229
+ def retrieve_pool(pool_id: str, req: PoolRetrieveRequest):
230
+ _require(pool_id, req.memberId, "read")
231
+ from src.services.retrieve import retrieve as _retrieve
232
+ result = _retrieve(_ns(pool_id), req.query, req.topK)
233
+ log_event("read", "pool_retrieve", req.memberId.strip().lower(),
234
+ detail={"pool_id": pool_id.strip().lower(),
235
+ "count": len(result.get("memories", []))})
236
+ return result
@@ -21,20 +21,45 @@ class RetrieveRequest(BaseModel):
21
21
  scope: Optional[List[str]] = None # hard filter by context_paths scope
22
22
  currentPath: Optional[str] = None # spatial boost: current file/dir path
23
23
  noGraph: bool = False # ablation: skip graph expansion
24
+ pools: Optional[List[str]] = None # also search these shared pools (if a member)
24
25
 
25
26
 
26
27
  @router.post("/retrieve")
27
28
  def retrieve_memories(req: RetrieveRequest):
28
29
  user_id = req.userId.strip().lower()
29
30
 
30
- # ── Recall throttling ──────────────────────────────────────────────────
31
- cached = recall_cached(user_id, req.query)
32
- if cached is not None:
33
- return cached
31
+ # ── Recall throttling (bypassed when pools are requested — results differ) ──
32
+ if not req.pools:
33
+ cached = recall_cached(user_id, req.query)
34
+ if cached is not None:
35
+ return cached
34
36
 
35
37
  result = retrieve(user_id, req.query, req.topK, current_path=req.currentPath, no_graph=req.noGraph,
36
38
  score_threshold=req.scoreThreshold, scope=req.scope, expand_k=req.expandK)
37
39
 
40
+ # ── Union with shared pools the caller may read (institutional memory) ──
41
+ if req.pools:
42
+ try:
43
+ from src.routes.pools import _ns, _member
44
+ from src.db.connection import get_backend as _gb, get_conn as _gc
45
+ backend = _gb(); conn = _gc()
46
+ try:
47
+ readable = [p.strip().lower() for p in req.pools
48
+ if (lambda m: m and m["can_read"])(_member(conn, backend, p.strip().lower(), user_id))]
49
+ finally:
50
+ conn.close()
51
+ merged = list(result.get("memories", []))
52
+ for pid in readable:
53
+ pr = retrieve(_ns(pid), req.query, req.topK)
54
+ for mm in pr.get("memories", []):
55
+ mm["pool"] = pid
56
+ merged.append(mm)
57
+ merged.sort(key=lambda m: m.get("score", 0), reverse=True)
58
+ result["memories"] = merged[:req.topK]
59
+ result["memoriesFound"] = len(result["memories"])
60
+ except Exception:
61
+ pass
62
+
38
63
  # ── Session wrap-up tracking ───────────────────────────────────────────
39
64
  session_track(user_id, [m["id"] for m in result.get("memories", [])])
40
65
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yourmemory
3
- Version: 1.4.77
3
+ Version: 1.4.78
4
4
  Summary: Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native
5
5
  Author: Sachit Misra
6
6
  Author-email: mishrasachit1@gmail.com
@@ -31,6 +31,7 @@ src/routes/compact.py
31
31
  src/routes/dsar.py
32
32
  src/routes/graph_viz.py
33
33
  src/routes/memories.py
34
+ src/routes/pools.py
34
35
  src/routes/proxy.py
35
36
  src/routes/retrieve.py
36
37
  src/routes/ui.py
File without changes
File without changes
File without changes
File without changes
File without changes