@@ -1075,6 +1075,8 @@ def _apply_schema(self, previous_version: int) -> None:
10751075 self ._backfill_memory_entities_v5 ()
10761076 if previous_version < 5 or mem_links_need_temporal_backfill :
10771077 self ._migrate_mem_link_history_v5 ()
1078+ if previous_version < 6 :
1079+ self ._migrate_code_file_history_v6 ()
10781080 # Classify pre-v3 edges. Existing rows defaulted to semantic during ALTER TABLE;
10791081 # infer their more specific logical layer from the relationship label.
10801082 if previous_version < 3 :
@@ -1247,6 +1249,29 @@ def _migrate_mem_link_history_v5(self) -> None:
12471249 (stamp , stamp ),
12481250 )
12491251
1252+ def _migrate_code_file_history_v6 (self ) -> None :
1253+ """Seed temporal file manifests from the v5 current-file snapshot."""
1254+ stamp = now_ts ()
1255+ rows = self .conn .execute ("SELECT * FROM code_files" ).fetchall ()
1256+ for row in rows :
1257+ existing = self .conn .execute (
1258+ "SELECT 1 FROM code_file_history WHERE repo_id=? AND file=? "
1259+ "AND valid_to IS NULL AND expired_at IS NULL" ,
1260+ (row ["repo_id" ], row ["file" ]),
1261+ ).fetchone ()
1262+ if existing is None :
1263+ started = row ["indexed_at" ] if row ["indexed_at" ] is not None else stamp
1264+ self .conn .execute (
1265+ "INSERT INTO code_file_history("
1266+ "repo_id, file, lang, content_hash, size_bytes, mtime_ns, backend, "
1267+ "indexed_at, valid_from, ingested_at) VALUES (?,?,?,?,?,?,?,?,?,?)" ,
1268+ (
1269+ row ["repo_id" ], row ["file" ], row ["lang" ], row ["content_hash" ],
1270+ row ["size_bytes" ], row ["mtime_ns" ], row ["backend" ],
1271+ row ["indexed_at" ], started , started ,
1272+ ),
1273+ )
1274+
12501275 def _backfill_claim_identity_v5 (self ) -> None :
12511276 """Lift already-present metadata hints into indexed, optional claim columns."""
12521277 rows = self .conn .execute (
@@ -2709,10 +2734,29 @@ def add_link(self, a: str, b: str, relation: str = "related",
27092734 normalize_graph_layer (layer , relation ).value
27102735 if layer is not None else None
27112736 )
2737+ graph_layer = requested_layer or normalize_graph_layer (None , relation ).value
27122738 started_transaction = not self .conn .in_transaction
27132739 if started_transaction :
27142740 self .conn .execute ("BEGIN IMMEDIATE" )
27152741 try :
2742+ # A sync bundle may carry a closed link interval. It has no live row to
2743+ # match below, so recognize an exact historical version before inserting
2744+ # it again on every replay. ``IS`` deliberately gives NULL-safe equality.
2745+ exact = self .conn .execute (
2746+ "SELECT 1 FROM mem_links "
2747+ "WHERE ((a=? AND b=?) OR (a=? AND b=?)) AND relation=? "
2748+ "AND layer=? AND reason=? AND valid_from IS ? AND valid_to IS ? "
2749+ "AND valid_to_recorded_at IS ? AND ingested_at IS ? AND expired_at IS ? "
2750+ "LIMIT 1" ,
2751+ (
2752+ a , b , b , a , relation , graph_layer , reason ,
2753+ valid_from , valid_to , valid_to_recorded_at , ingested_at , expired_at ,
2754+ ),
2755+ ).fetchone ()
2756+ if exact is not None :
2757+ if started_transaction :
2758+ self .conn .commit ()
2759+ return
27162760 existing = self .conn .execute (
27172761 "SELECT rowid, a, b, relation, layer, reason, created_at, "
27182762 "valid_from, valid_to, valid_to_recorded_at, ingested_at, expired_at "
@@ -2768,7 +2812,6 @@ def add_link(self, a: str, b: str, relation: str = "related",
27682812 # for ``commit=False``; the old no-op path never opened a transaction.
27692813 self .conn .commit ()
27702814 return
2771- graph_layer = requested_layer or normalize_graph_layer (None , relation ).value
27722815 stamp = now_ts ()
27732816 world_start = stamp if valid_from is None else valid_from
27742817 system_start = stamp if ingested_at is None else ingested_at
@@ -2818,7 +2861,14 @@ def get_links(self, memory_id: str, *,
28182861 def edges_in_scope (self , flt : Optional [SearchFilter ] = None ,
28192862 * , at : Optional [float ] = None ,
28202863 limit : Optional [int ] = None ) -> list [Edge ]:
2821- """Edges visible at ``at``/``filter.valid_at`` and ``filter.known_at``."""
2864+ """Edges visible at ``at``/``filter.valid_at`` and ``filter.known_at``.
2865+
2866+ Normalized supports are authoritative for edges that have them. The edge row
2867+ aggregates its support starts for current-read efficiency, but independently
2868+ minimizing world and system time can fabricate a pair no source established.
2869+ A historical read must therefore see at least one individually visible support.
2870+ Legacy direct edges with no normalized support retain the edge-row fallback.
2871+ """
28222872 valid_at , known_at = _temporal_anchors (flt , valid_at = at )
28232873 sql = ("SELECT * FROM edges WHERE (valid_from IS NULL OR valid_from<=?) "
28242874 "AND (valid_to IS NULL OR ?<valid_to "
@@ -2829,6 +2879,16 @@ def edges_in_scope(self, flt: Optional[SearchFilter] = None,
28292879 params : list [Any ] = [
28302880 valid_at , valid_at , known_at , known_at , known_at ,
28312881 ]
2882+ support_visibility , support_params = _temporal_visibility_sql (
2883+ "s" , flt , valid_at = valid_at
2884+ )
2885+ sql += (
2886+ " AND (NOT EXISTS (SELECT 1 FROM edge_supports any_support "
2887+ "WHERE any_support.edge_id=edges.id) OR EXISTS (SELECT 1 FROM "
2888+ "edge_supports s WHERE s.edge_id=edges.id AND "
2889+ + support_visibility + "))"
2890+ )
2891+ params .extend (support_params )
28322892 if flt and flt .workspace_id :
28332893 sql += " AND workspace_id=?"
28342894 params .append (flt .workspace_id )
@@ -2851,9 +2911,13 @@ def edges_in_scope(self, flt: Optional[SearchFilter] = None,
28512911 def links_among (self , ids : list [str ], * ,
28522912 layers : Optional [list [GraphLayer ]] = None ,
28532913 flt : Optional [SearchFilter ] = None ,
2914+ include_invalid : bool = False ,
28542915 limit : Optional [int ] = None ) -> list [dict ]:
28552916 """Return memory links visible under both temporal anchors.
28562917
2918+ ``include_invalid`` is for full-state replication only: a closed interval is
2919+ state that must synchronize even though normal graph reads do not expose it.
2920+
28572921 Chunk only the indexed ``a`` side and filter ``b`` against an in-memory set.
28582922 This keeps every statement below SQLite's portable variable limit while
28592923 preserving exact pair semantics for graphs containing thousands of memories.
@@ -2879,15 +2943,17 @@ def links_among(self, ids: list[str], *,
28792943 sql = (
28802944 "SELECT a, b, relation, layer, reason, created_at, valid_from, valid_to, "
28812945 "valid_to_recorded_at, ingested_at, expired_at FROM mem_links "
2882- f"WHERE a IN ({ marks } ) "
2883- f"AND { visibility_sql } "
2946+ f"WHERE a IN ({ marks } )"
28842947 )
2885- params : list [Any ] = [* chunk , * visibility_params ]
2948+ params : list [Any ] = [* chunk ]
2949+ if not include_invalid :
2950+ sql += f" AND { visibility_sql } "
2951+ params .extend (visibility_params )
28862952 if layers is not None :
28872953 layer_marks = "," .join ("?" for _ in layers )
28882954 sql += f" AND layer IN ({ layer_marks } )"
28892955 params .extend (_enum (layer ) for layer in layers )
2890- sql += " ORDER BY a, b, relation"
2956+ sql += " ORDER BY a, b, relation, valid_from, ingested_at "
28912957 found = self .conn .execute (sql , params ).fetchall ()
28922958 for row in found :
28932959 if row ["b" ] not in wanted :
@@ -2917,6 +2983,16 @@ def neighbors(self, node_ids: list[str], *, at: Optional[float] = None,
29172983 * node_ids , * node_ids ,
29182984 valid_at , valid_at , known_at , known_at , known_at ,
29192985 ]
2986+ support_visibility , support_params = _temporal_visibility_sql (
2987+ "s" , flt , valid_at = valid_at
2988+ )
2989+ sql += (
2990+ " AND (NOT EXISTS (SELECT 1 FROM edge_supports any_support "
2991+ "WHERE any_support.edge_id=edges.id) OR EXISTS (SELECT 1 FROM "
2992+ "edge_supports s WHERE s.edge_id=edges.id AND "
2993+ + support_visibility + "))"
2994+ )
2995+ params .extend (support_params )
29202996 if layers is not None :
29212997 if not layers :
29222998 return []
@@ -3009,14 +3085,22 @@ def get_code_file(self, repo_id: str, file: str) -> Optional[dict]:
30093085
30103086 def list_code_files (self , repo_id : str , * ,
30113087 languages : Optional [set ] = None ,
3088+ flt : Optional [SearchFilter ] = None ,
30123089 limit : Optional [int ] = None ) -> list [dict ]:
3013- sql = "SELECT * FROM code_files WHERE repo_id=?"
3090+ """Return the current manifest, or its bi-temporal history when anchored."""
3091+ historical = bool (flt and flt .historical )
3092+ table = "code_file_history" if historical else "code_files"
3093+ sql = f"SELECT * FROM { table } WHERE repo_id=?"
30143094 params : list [Any ] = [repo_id ]
3095+ if historical :
3096+ temporal , temporal_params = _temporal_visibility_sql ("" , flt )
3097+ sql += " AND " + temporal
3098+ params .extend (temporal_params )
30153099 if languages :
30163100 marks = "," .join ("?" for _ in languages )
30173101 sql += f" AND lang IN ({ marks } )"
30183102 params .extend (sorted (languages ))
3019- sql += " ORDER BY file"
3103+ sql += " ORDER BY file" + ( ", version" if historical else "" )
30203104 if limit is not None :
30213105 sql += " LIMIT ?"
30223106 params .append (max (0 , int (limit ))) # never -1 == SQLite "unlimited"
@@ -3025,6 +3109,34 @@ def list_code_files(self, repo_id: str, *,
30253109 def upsert_code_file (self , * , repo_id : str , file : str , lang : str ,
30263110 content_hash : str , size_bytes : int , mtime_ns : int ,
30273111 backend : str , commit : bool = True ) -> None :
3112+ stamp = now_ts ()
3113+ current_history = self .conn .execute (
3114+ "SELECT version, lang, content_hash, size_bytes, mtime_ns, backend "
3115+ "FROM code_file_history WHERE repo_id=? AND file=? "
3116+ "AND valid_to IS NULL AND expired_at IS NULL" ,
3117+ (repo_id , file ),
3118+ ).fetchone ()
3119+ unchanged = current_history is not None and (
3120+ current_history ["lang" ], current_history ["content_hash" ],
3121+ int (current_history ["size_bytes" ] or 0 ), int (current_history ["mtime_ns" ] or 0 ),
3122+ current_history ["backend" ] or "" ,
3123+ ) == (lang , content_hash , int (size_bytes ), int (mtime_ns ), backend )
3124+ if not unchanged :
3125+ if current_history is not None :
3126+ self .conn .execute (
3127+ "UPDATE code_file_history SET valid_to=?, valid_to_recorded_at=? "
3128+ "WHERE version=?" ,
3129+ (stamp , stamp , current_history ["version" ]),
3130+ )
3131+ self .conn .execute (
3132+ "INSERT INTO code_file_history("
3133+ "repo_id, file, lang, content_hash, size_bytes, mtime_ns, backend, "
3134+ "indexed_at, valid_from, ingested_at) VALUES (?,?,?,?,?,?,?,?,?,?)" ,
3135+ (
3136+ repo_id , file , lang , content_hash , int (size_bytes ), int (mtime_ns ),
3137+ backend , stamp , stamp , stamp ,
3138+ ),
3139+ )
30283140 self .conn .execute (
30293141 "INSERT INTO code_files(repo_id, file, lang, content_hash, size_bytes, "
30303142 "mtime_ns, backend, indexed_at) VALUES (?,?,?,?,?,?,?,?) "
@@ -3033,13 +3145,19 @@ def upsert_code_file(self, *, repo_id: str, file: str, lang: str,
30333145 "size_bytes=excluded.size_bytes, mtime_ns=excluded.mtime_ns, "
30343146 "backend=excluded.backend, indexed_at=excluded.indexed_at" ,
30353147 (repo_id , file , lang , content_hash , int (size_bytes ), int (mtime_ns ),
3036- backend , now_ts () ),
3148+ backend , stamp ),
30373149 )
30383150 if commit :
30393151 self .conn .commit ()
30403152
30413153 def remove_code_file (self , repo_id : str , file : str , * , commit : bool = True ) -> None :
30423154 self .clear_symbols_for_file (repo_id , file , commit = False )
3155+ stamp = now_ts ()
3156+ self .conn .execute (
3157+ "UPDATE code_file_history SET valid_to=?, valid_to_recorded_at=? "
3158+ "WHERE repo_id=? AND file=? AND valid_to IS NULL AND expired_at IS NULL" ,
3159+ (stamp , stamp , repo_id , file ),
3160+ )
30433161 self .conn .execute ("DELETE FROM code_files WHERE repo_id=? AND file=?" , (repo_id , file ))
30443162 if commit :
30453163 self .conn .commit ()
0 commit comments