77from datetime import datetime , timezone
88from pathlib import Path
99
10- from .models import MEMORY_TYPES , MemoryRecord
10+ from .models import MEMORY_TYPES , MEMORY_STATUSES , MemoryRecord
1111from .schema import init_db
1212
1313
1414def _now () -> str :
1515 return datetime .now (timezone .utc ).isoformat ()
1616
1717
18+ def _safe_get (row , key , default = "" ):
19+ """Safely get a column value, returning default if column doesn't exist."""
20+ try :
21+ return row [key ]
22+ except (IndexError , KeyError ):
23+ return default
24+
25+
1826def _row_to_record (row : sqlite3 .Row , rank : float | None = None ) -> MemoryRecord :
1927 return MemoryRecord (
2028 id = row ["id" ],
@@ -26,6 +34,13 @@ def _row_to_record(row: sqlite3.Row, rank: float | None = None) -> MemoryRecord:
2634 project = row ["project" ],
2735 confidence = row ["confidence" ],
2836 supersedes = row ["supersedes" ],
37+ status = _safe_get (row , "status" , "active" ),
38+ source_path = _safe_get (row , "source_path" , "" ),
39+ source_section = _safe_get (row , "source_section" , "" ),
40+ source_hash = _safe_get (row , "source_hash" , "" ),
41+ validated_at = _safe_get (row , "validated_at" , "" ),
42+ deprecated_at = _safe_get (row , "deprecated_at" , "" ),
43+ superseded_by = _safe_get (row , "superseded_by" , "" ),
2944 created_at = row ["created_at" ],
3045 updated_at = row ["updated_at" ],
3146 accessed_at = row ["accessed_at" ],
@@ -67,29 +82,42 @@ def add(
6782 project : str | None = None ,
6883 confidence : float = 1.0 ,
6984 supersedes : str = "" ,
85+ status : str = "active" ,
86+ source_path : str = "" ,
87+ source_section : str = "" ,
88+ source_hash : str = "" ,
7089 ) -> MemoryRecord :
7190 if type not in MEMORY_TYPES :
7291 raise ValueError (f"Invalid type '{ type } '. Must be one of: { MEMORY_TYPES } " )
92+ if status not in MEMORY_STATUSES :
93+ raise ValueError (f"Invalid status '{ status } '. Must be one of: { MEMORY_STATUSES } " )
7394
7495 record_id = str (uuid .uuid4 ())
7596 now = _now ()
7697 tags_str = "," .join (tags ) if tags else ""
7798 proj = project if project is not None else self .project
99+ validated_at = now if status == "validated" else ""
78100
79101 self ._conn .execute (
80102 """INSERT INTO memories
81103 (id, type, title, content, tags, source, project, confidence,
82- supersedes, created_at, updated_at, accessed_at, access_count)
83- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)""" ,
104+ supersedes, status, source_path, source_section, source_hash,
105+ validated_at, deprecated_at, superseded_by,
106+ created_at, updated_at, accessed_at, access_count)
107+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', ?, ?, ?, 0)""" ,
84108 (record_id , type , title , content , tags_str , source , proj ,
85- confidence , supersedes , now , now , now ),
109+ confidence , supersedes , status , source_path , source_section ,
110+ source_hash , validated_at , now , now , now ),
86111 )
87112 self ._conn .commit ()
88113
89114 return MemoryRecord (
90115 id = record_id , type = type , title = title , content = content ,
91116 tags = tags or [], source = source , project = proj ,
92117 confidence = confidence , supersedes = supersedes ,
118+ status = status , source_path = source_path ,
119+ source_section = source_section , source_hash = source_hash ,
120+ validated_at = validated_at ,
93121 created_at = now , updated_at = now , accessed_at = now , access_count = 0 ,
94122 )
95123
@@ -112,7 +140,9 @@ def update(self, id: str, **kwargs) -> MemoryRecord | None:
112140 if not record :
113141 return None
114142
115- allowed = {"title" , "content" , "tags" , "type" , "confidence" , "project" , "supersedes" }
143+ allowed = {"title" , "content" , "tags" , "type" , "confidence" , "project" ,
144+ "supersedes" , "status" , "source_path" , "source_section" ,
145+ "source_hash" , "validated_at" , "deprecated_at" , "superseded_by" }
116146 updates = {k : v for k , v in kwargs .items () if k in allowed and v is not None }
117147 if not updates :
118148 return record
@@ -121,6 +151,8 @@ def update(self, id: str, **kwargs) -> MemoryRecord | None:
121151 updates ["tags" ] = "," .join (updates ["tags" ])
122152 if "type" in updates and updates ["type" ] not in MEMORY_TYPES :
123153 raise ValueError (f"Invalid type '{ updates ['type' ]} '. Must be one of: { MEMORY_TYPES } " )
154+ if "status" in updates and updates ["status" ] not in MEMORY_STATUSES :
155+ raise ValueError (f"Invalid status '{ updates ['status' ]} '. Must be one of: { MEMORY_STATUSES } " )
124156
125157 updates ["updated_at" ] = _now ()
126158 set_clause = ", " .join (f"{ k } = ?" for k in updates )
@@ -130,6 +162,70 @@ def update(self, id: str, **kwargs) -> MemoryRecord | None:
130162 self ._conn .commit ()
131163 return self .get (id )
132164
165+ # ── Truth governance lifecycle ──────────────────────────────────
166+
167+ def promote (self , id : str ) -> MemoryRecord | None :
168+ """Promote a memory: hypothesis → active → validated.
169+ Each call advances one step. Validated is the highest trust level."""
170+ record = self .get (id )
171+ if not record :
172+ return None
173+
174+ promotions = {"hypothesis" : "active" , "active" : "validated" }
175+ next_status = promotions .get (record .status )
176+ if not next_status :
177+ return record # Already validated or in a terminal state
178+
179+ now = _now ()
180+ updates = {"status" : next_status , "updated_at" : now }
181+ if next_status == "validated" :
182+ updates ["validated_at" ] = now
183+
184+ set_clause = ", " .join (f"{ k } = ?" for k in updates )
185+ values = list (updates .values ()) + [id ]
186+ self ._conn .execute (f"UPDATE memories SET { set_clause } WHERE id = ?" , values )
187+ self ._conn .commit ()
188+ return self .get (id )
189+
190+ def deprecate (self , id : str , reason : str = "" ) -> MemoryRecord | None :
191+ """Mark a memory as deprecated. Excluded from recall, kept for history."""
192+ record = self .get (id )
193+ if not record :
194+ return None
195+
196+ now = _now ()
197+ content = record .content
198+ if reason :
199+ content = f"{ content } \n \n [DEPRECATED { now [:10 ]} ] { reason } "
200+
201+ self ._conn .execute (
202+ "UPDATE memories SET status = 'deprecated', deprecated_at = ?, "
203+ "content = ?, updated_at = ? WHERE id = ?" ,
204+ (now , content , now , id ),
205+ )
206+ self ._conn .commit ()
207+ return self .get (id )
208+
209+ def supersede (self , old_id : str , new_id : str ) -> tuple [MemoryRecord | None , MemoryRecord | None ]:
210+ """Mark old_id as superseded by new_id. Old memory points to replacement."""
211+ old = self .get (old_id )
212+ new = self .get (new_id )
213+ if not old or not new :
214+ return (old , new )
215+
216+ now = _now ()
217+ self ._conn .execute (
218+ "UPDATE memories SET status = 'superseded', superseded_by = ?, "
219+ "deprecated_at = ?, updated_at = ? WHERE id = ?" ,
220+ (new_id , now , now , old_id ),
221+ )
222+ self ._conn .execute (
223+ "UPDATE memories SET supersedes = ?, updated_at = ? WHERE id = ?" ,
224+ (old_id , now , new_id ),
225+ )
226+ self ._conn .commit ()
227+ return (self .get (old_id ), self .get (new_id ))
228+
133229 def delete (self , id : str ) -> bool :
134230 cur = self ._conn .execute ("DELETE FROM memories WHERE id = ?" , (id ,))
135231 self ._conn .commit ()
@@ -210,24 +306,32 @@ def save_session(self, summary: str, tags: list[str] | None = None) -> MemoryRec
210306 The summary should capture: what's in progress, what's blocked, what's done,
211307 and any decisions made this session.
212308 """
213- # Find and supersede the previous session
309+ # Find previous active session
214310 prev = self ._conn .execute (
215311 "SELECT id FROM memories WHERE type = 'session' AND project = ? "
312+ "AND COALESCE(status, 'active') = 'active' "
216313 "ORDER BY created_at DESC LIMIT 1" ,
217314 (self .project ,),
218315 ).fetchone ()
219316
220- supersedes = prev ["id" ] if prev else ""
317+ prev_id = prev ["id" ] if prev else ""
221318
222- return self .add (
319+ # Create new session
320+ new_record = self .add (
223321 type = "session" ,
224322 title = f"Session state — { self .project or 'default' } " ,
225323 content = summary ,
226324 tags = tags or ["session" , "state" ],
227325 source = "session" ,
228- supersedes = supersedes ,
326+ supersedes = prev_id ,
229327 )
230328
329+ # Actually supersede the old session using the governance lifecycle
330+ if prev_id :
331+ self .supersede (prev_id , new_record .id )
332+
333+ return new_record
334+
231335 def load_session (self ) -> MemoryRecord | None :
232336 """Load the most recent session state for this project.
233337
0 commit comments