@@ -20,6 +20,13 @@ def _default_db_path() -> str:
2020 return os .path .join (base , "memory.db" )
2121
2222
23+ def _norm_namespace (namespace : str | None ) -> str :
24+ """SQLite partition key; empty string means legacy default (same as namespace=None)."""
25+ if namespace is None or namespace == "" :
26+ return ""
27+ return namespace
28+
29+
2330class MemoryStore :
2431 """Local persistent store for memory records. Uses SQLite."""
2532
@@ -60,8 +67,17 @@ def _init_schema(self) -> None:
6067 conn .execute ("ALTER TABLE memory ADD COLUMN archived INTEGER DEFAULT 0" )
6168 except sqlite3 .OperationalError :
6269 pass
70+ try :
71+ conn .execute (
72+ "ALTER TABLE memory ADD COLUMN namespace TEXT NOT NULL DEFAULT ''"
73+ )
74+ except sqlite3 .OperationalError :
75+ pass
76+ conn .execute (
77+ "CREATE INDEX IF NOT EXISTS ix_memory_namespace ON memory(namespace)"
78+ )
6379
64- def store (self , record : MemoryRecord ) -> str :
80+ def store (self , record : MemoryRecord , namespace : str | None = None ) -> str :
6581 """Store a memory record. Returns record id. Redacts PII if compliance.pii_redaction enabled."""
6682 content = record .content
6783 try :
@@ -84,12 +100,13 @@ def store(self, record: MemoryRecord) -> str:
84100 embedding_json = json .dumps (emb ) if emb is not None else None
85101 archived = row .get ("archived" , 0 )
86102 run_id = row .get ("run_id" , "" ) or ""
103+ ns = _norm_namespace (namespace )
87104 with self ._conn () as conn :
88105 conn .execute (
89106 """
90107 INSERT OR REPLACE INTO memory
91- (memory_id, memory_type, content, tags, timestamp, source_task, embedding, run_id, archived)
92- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
108+ (memory_id, memory_type, content, tags, timestamp, source_task, embedding, run_id, archived, namespace )
109+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
93110 """ ,
94111 (
95112 row ["memory_id" ],
@@ -101,27 +118,39 @@ def store(self, record: MemoryRecord) -> str:
101118 embedding_json ,
102119 run_id ,
103120 archived ,
121+ ns ,
104122 ),
105123 )
106124 return row ["memory_id" ]
107125
108- def retrieve (self , memory_id : str ) -> MemoryRecord | None :
126+ def retrieve (self , memory_id : str , namespace : str | None = None ) -> MemoryRecord | None :
109127 """Retrieve a single record by id."""
128+ ns = _norm_namespace (namespace )
110129 with self ._conn () as conn :
111130 conn .row_factory = sqlite3 .Row
112131 cur = conn .execute (
113- "SELECT memory_id, memory_type, content, tags, timestamp, source_task, embedding, run_id, archived FROM memory WHERE memory_id = ?" ,
114- (memory_id ,),
132+ """
133+ SELECT memory_id, memory_type, content, tags, timestamp, source_task, embedding, run_id, archived,
134+ COALESCE(namespace, '') AS namespace
135+ FROM memory WHERE memory_id = ? AND COALESCE(namespace, '') = ?
136+ """ ,
137+ (memory_id , ns ),
115138 )
116139 row = cur .fetchone ()
117140 if row is None :
118141 return None
119- return _row_to_record (dict (row ))
142+ d = dict (row )
143+ d .pop ("namespace" , None )
144+ return _row_to_record (d )
120145
121- def delete (self , memory_id : str ) -> bool :
146+ def delete (self , memory_id : str , namespace : str | None = None ) -> bool :
122147 """Delete a record. Returns True if something was deleted."""
148+ ns = _norm_namespace (namespace )
123149 with self ._conn () as conn :
124- cur = conn .execute ("DELETE FROM memory WHERE memory_id = ?" , (memory_id ,))
150+ cur = conn .execute (
151+ "DELETE FROM memory WHERE memory_id = ? AND COALESCE(namespace, '') = ?" ,
152+ (memory_id , ns ),
153+ )
125154 return cur .rowcount > 0
126155
127156 def list_memory (
@@ -132,12 +161,14 @@ def list_memory(
132161 tag_contains : str | None = None ,
133162 include_archived : bool = False ,
134163 run_id_filter : str | None = None ,
164+ namespace : str | None = None ,
135165 ) -> list [MemoryRecord ]:
136- """List records, optionally filtered by type, tag, archived, run_id, with limit/offset."""
166+ """List records, optionally filtered by type, tag, archived, run_id, namespace, with limit/offset."""
167+ ns = _norm_namespace (namespace )
137168 with self ._conn () as conn :
138169 conn .row_factory = sqlite3 .Row
139- conditions = []
140- params = []
170+ conditions = ["COALESCE(namespace, '') = ?" ]
171+ params : list = [ns ]
141172 if memory_type is not None :
142173 conditions .append ("memory_type = ?" )
143174 params .append (memory_type .value )
@@ -149,40 +180,64 @@ def list_memory(
149180 if run_id_filter is not None :
150181 conditions .append ("run_id = ?" )
151182 params .append (run_id_filter )
152- where = ( " WHERE " + " AND " .join (conditions )) if conditions else ""
183+ where = " WHERE " + " AND " .join (conditions )
153184 params .extend ([limit , offset ])
154185 cur = conn .execute (
155186 f"""
156187 SELECT memory_id, memory_type, content, tags, timestamp, source_task, embedding,
157- COALESCE(run_id, '') as run_id, COALESCE(archived, 0) as archived
188+ COALESCE(run_id, '') as run_id, COALESCE(archived, 0) as archived,
189+ COALESCE(namespace, '') as namespace
158190 FROM memory{ where } ORDER BY timestamp DESC LIMIT ? OFFSET ?
159191 """ ,
160192 params ,
161193 )
162194 rows = cur .fetchall ()
163- return [_row_to_record (dict (r )) for r in rows ]
195+ out = []
196+ for r in rows :
197+ d = dict (r )
198+ d .pop ("namespace" , None )
199+ out .append (_row_to_record (d ))
200+ return out
164201
165- def list_all_ids (self , memory_type : MemoryType | None = None ) -> list [str ]:
202+ def list_all_ids (self , memory_type : MemoryType | None = None , namespace : str | None = None ) -> list [str ]:
166203 """List all memory ids (for index sync)."""
204+ ns = _norm_namespace (namespace )
167205 with self ._conn () as conn :
168206 if memory_type is not None :
169207 cur = conn .execute (
170- "SELECT memory_id FROM memory WHERE memory_type = ?" ,
171- (memory_type .value ,),
208+ """
209+ SELECT memory_id FROM memory
210+ WHERE memory_type = ? AND COALESCE(namespace, '') = ?
211+ """ ,
212+ (memory_type .value , ns ),
172213 )
173214 else :
174- cur = conn .execute ("SELECT memory_id FROM memory" )
215+ cur = conn .execute (
216+ "SELECT memory_id FROM memory WHERE COALESCE(namespace, '') = ?" ,
217+ (ns ,),
218+ )
175219 return [r [0 ] for r in cur .fetchall ()]
176220
177- def set_archived (self , memory_id : str , archived : bool = True ) -> bool :
221+ def set_archived (self , memory_id : str , archived : bool = True , namespace : str | None = None ) -> bool :
178222 """v1.8: Mark a record as archived (e.g. after consolidation)."""
223+ ns = _norm_namespace (namespace )
179224 with self ._conn () as conn :
180225 cur = conn .execute (
181- "UPDATE memory SET archived = ? WHERE memory_id = ?" ,
182- (1 if archived else 0 , memory_id ),
226+ """
227+ UPDATE memory SET archived = ?
228+ WHERE memory_id = ? AND COALESCE(namespace, '') = ?
229+ """ ,
230+ (1 if archived else 0 , memory_id , ns ),
183231 )
184232 return cur .rowcount > 0
185233
234+ def purge_namespace (self , namespace : str ) -> None :
235+ """Remove all memory rows for a logical namespace (e.g. when a project is deleted)."""
236+ if not namespace or not str (namespace ).strip ():
237+ return
238+ with self ._conn () as conn :
239+ conn .execute ("DELETE FROM memory WHERE namespace = ?" , (namespace ,))
240+
186241
187242def _row_to_record (row : dict ) -> MemoryRecord :
188243 tags_str = row .get ("tags" ) or ""
0 commit comments