differix 0.1.0a1__py3-none-any.whl

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 (62) hide show
  1. agents/editor.py +455 -0
  2. agents/researcher.py +405 -0
  3. agents/reviewer.py +374 -0
  4. cli/model_cli.py +430 -0
  5. config.py +557 -0
  6. differix-0.1.0a1.dist-info/METADATA +123 -0
  7. differix-0.1.0a1.dist-info/RECORD +62 -0
  8. differix-0.1.0a1.dist-info/WHEEL +5 -0
  9. differix-0.1.0a1.dist-info/entry_points.txt +2 -0
  10. differix-0.1.0a1.dist-info/top_level.txt +6 -0
  11. main.py +2796 -0
  12. memory/__init__.py +59 -0
  13. memory/embeddings/__init__.py +29 -0
  14. memory/embeddings/factory.py +178 -0
  15. memory/embeddings/providers.py +259 -0
  16. memory/manager.py +186 -0
  17. memory/migrator.py +253 -0
  18. memory/ranker.py +203 -0
  19. memory/schemas.py +127 -0
  20. memory/storage.py +552 -0
  21. tools/agents_chat.py +152 -0
  22. tools/asset_extractor.py +545 -0
  23. tools/asset_manager.py +115 -0
  24. tools/atomic_changeset.py +264 -0
  25. tools/backup_manager.py +122 -0
  26. tools/changelog_engine.py +261 -0
  27. tools/coherence_checker.py +771 -0
  28. tools/compiler.py +531 -0
  29. tools/component_normalizer.py +130 -0
  30. tools/context_assembler.py +631 -0
  31. tools/contract_validator.py +478 -0
  32. tools/cost_predictor.py +141 -0
  33. tools/drift_guard.py +196 -0
  34. tools/file_manager.py +119 -0
  35. tools/handshake.py +351 -0
  36. tools/lesson_manager.py +570 -0
  37. tools/lesson_memory_bridge.py +398 -0
  38. tools/llm_client.py +103 -0
  39. tools/llm_selector.py +362 -0
  40. tools/model_ranker.py +535 -0
  41. tools/palette_extractor.py +1043 -0
  42. tools/plugin_manager.py +204 -0
  43. tools/project_context_gatherer.py +404 -0
  44. tools/project_scanner.py +41 -0
  45. tools/project_structure.py +244 -0
  46. tools/reasoning_logger.py +78 -0
  47. tools/reflection.py +452 -0
  48. tools/sandbox_utils.py +56 -0
  49. tools/security/__init__.py +0 -0
  50. tools/security/behavior_guard.py +83 -0
  51. tools/security/verification_firewall.py +113 -0
  52. tools/security_filter.py +213 -0
  53. tools/session_adr.py +311 -0
  54. tools/skill_manager.py +129 -0
  55. tools/state_manager.py +383 -0
  56. tools/structure_index.py +570 -0
  57. tools/test_schema_consistency.py +19 -0
  58. tools/verification_firewall.py +113 -0
  59. tools/visual_capture.py +65 -0
  60. tools/visual_diff.py +220 -0
  61. tools/visual_reviewer.py +79 -0
  62. tools/web_search.py +371 -0
memory/schemas.py ADDED
@@ -0,0 +1,127 @@
1
+ """
2
+ Data types, enums, and exceptions for the VegeX memory subsystem.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import enum
8
+ import math
9
+ import time
10
+ import uuid
11
+ from typing import Any, Dict, List
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Exceptions
18
+ # ---------------------------------------------------------------------------
19
+ class MemoryStorageError(Exception):
20
+ """Base exception for all memory storage operations."""
21
+
22
+
23
+ class DimensionMismatchError(MemoryStorageError):
24
+ def __init__(self, expected: int, received: int) -> None:
25
+ self.expected = expected
26
+ self.received = received
27
+ super().__init__(
28
+ f"Vector dimension mismatch: expected {expected}, received {received}."
29
+ )
30
+
31
+
32
+ class MemoryNotFoundError(MemoryStorageError):
33
+ def __init__(self, node_id: str) -> None:
34
+ self.node_id = node_id
35
+ super().__init__(f"Memory node not found: {node_id}")
36
+
37
+
38
+ class MemoryValidationError(MemoryStorageError):
39
+ """Raised when a MemoryNode violates schema-level invariants."""
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Enums
44
+ # ---------------------------------------------------------------------------
45
+ class MemoryScope(str, enum.Enum):
46
+ GLOBAL = "global"
47
+ PROJECT = "project"
48
+ LANGUAGE = "language"
49
+ DOMAIN = "domain"
50
+ FILE = "file"
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Core model
55
+ # ---------------------------------------------------------------------------
56
+ class MemoryNode(BaseModel):
57
+ """A single memory node / lesson with A-Score signals."""
58
+
59
+ node_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
60
+ vector: List[float] = Field(...)
61
+ content: str = Field(..., min_length=1)
62
+ metadata: Dict[str, Any] = Field(default_factory=dict)
63
+
64
+ base_trust: float = Field(default=0.3, ge=0.0, le=1.0)
65
+ trigger_count: int = Field(default=0, ge=0)
66
+ helpful_count: int = Field(default=0, ge=0)
67
+ violation_count: int = Field(default=0, ge=0)
68
+ is_critical: bool = Field(default=False)
69
+
70
+ scope_type: MemoryScope = Field(default=MemoryScope.GLOBAL)
71
+ scope_value: str = Field(default="*", min_length=1)
72
+
73
+ created_at: float = Field(default_factory=time.time)
74
+ last_used_at: float = Field(default_factory=time.time)
75
+
76
+ @field_validator("vector")
77
+ @classmethod
78
+ def _validate_vector(cls, v: List[float]) -> List[float]:
79
+ if not v:
80
+ raise MemoryValidationError("Vector cannot be empty.")
81
+ for idx, value in enumerate(v):
82
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
83
+ raise MemoryValidationError(
84
+ f"Vector element at index {idx} is not numeric: {value!r}"
85
+ )
86
+ if math.isnan(value) or math.isinf(value):
87
+ raise MemoryValidationError(
88
+ f"Vector contains NaN/Inf at index {idx}: {value!r}"
89
+ )
90
+ return [float(x) for x in v]
91
+
92
+ model_config = ConfigDict(frozen=False, extra="forbid")
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Ranking weights (A-Score hyperparameters)
97
+ # ---------------------------------------------------------------------------
98
+ class RankingWeights(BaseModel):
99
+ alpha: float = Field(default=1.0, ge=0.0, le=5.0)
100
+ beta_protection: float = Field(default=2.0, ge=0.0, le=10.0)
101
+ lambda_base: float = Field(default=0.01, gt=0.0)
102
+ epsilon: float = Field(default=1e-5, ge=0.0)
103
+
104
+ model_config = ConfigDict(frozen=False, extra="forbid")
105
+
106
+
107
+ class ScoredMemoryNode(BaseModel):
108
+ node: MemoryNode
109
+ score: float
110
+ similarity: float
111
+ scope_match: float
112
+ decay: float
113
+ jitter: float
114
+
115
+ model_config = ConfigDict(frozen=False, extra="forbid")
116
+
117
+
118
+ __all__ = [
119
+ "MemoryNode",
120
+ "MemoryScope",
121
+ "RankingWeights",
122
+ "ScoredMemoryNode",
123
+ "MemoryStorageError",
124
+ "DimensionMismatchError",
125
+ "MemoryNotFoundError",
126
+ "MemoryValidationError",
127
+ ]
memory/storage.py ADDED
@@ -0,0 +1,552 @@
1
+ """
2
+ SQLite-backed persistent storage for MemoryNode instances.
3
+ Source of truth for A-Score signals (B, T, H, V, is_critical, scope, timestamps).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ import math
11
+ import sqlite3
12
+ import threading
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional, Tuple, Union
16
+
17
+ from memory.schemas import (
18
+ DimensionMismatchError,
19
+ MemoryNode,
20
+ MemoryNotFoundError,
21
+ MemoryScope,
22
+ MemoryStorageError,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class SQLiteMemoryStorage:
29
+ """Thread-safe SQLite storage for MemoryNode instances."""
30
+
31
+ _SCHEMA_VERSION = 2
32
+
33
+ _CREATE_TABLE = """
34
+ CREATE TABLE IF NOT EXISTS memory_nodes (
35
+ node_id TEXT PRIMARY KEY,
36
+ vector TEXT NOT NULL,
37
+ content TEXT NOT NULL,
38
+ metadata TEXT NOT NULL DEFAULT '{}',
39
+ base_trust REAL NOT NULL DEFAULT 0.3,
40
+ trigger_count INTEGER NOT NULL DEFAULT 0,
41
+ helpful_count INTEGER NOT NULL DEFAULT 0,
42
+ violation_count INTEGER NOT NULL DEFAULT 0,
43
+ is_critical INTEGER NOT NULL DEFAULT 0,
44
+ scope_type TEXT NOT NULL DEFAULT 'global',
45
+ scope_value TEXT NOT NULL DEFAULT '*',
46
+ created_at REAL NOT NULL,
47
+ last_used_at REAL NOT NULL
48
+ );
49
+ """
50
+
51
+ _CREATE_META_TABLE = """
52
+ CREATE TABLE IF NOT EXISTS schema_meta (
53
+ key TEXT PRIMARY KEY,
54
+ value TEXT NOT NULL,
55
+ updated_at REAL NOT NULL
56
+ );
57
+ """
58
+
59
+ _CREATE_INDEXES = (
60
+ "CREATE INDEX IF NOT EXISTS idx_mn_created_at ON memory_nodes (created_at);",
61
+ "CREATE INDEX IF NOT EXISTS idx_mn_last_used ON memory_nodes (last_used_at);",
62
+ "CREATE INDEX IF NOT EXISTS idx_mn_scope ON memory_nodes (scope_type, scope_value);",
63
+ "CREATE INDEX IF NOT EXISTS idx_mn_critical ON memory_nodes (is_critical);",
64
+ )
65
+
66
+ _MIGRATION_COLUMNS: Tuple[Tuple[str, str], ...] = (
67
+ ("base_trust", "REAL NOT NULL DEFAULT 0.3"),
68
+ ("trigger_count", "INTEGER NOT NULL DEFAULT 0"),
69
+ ("helpful_count", "INTEGER NOT NULL DEFAULT 0"),
70
+ ("violation_count", "INTEGER NOT NULL DEFAULT 0"),
71
+ ("is_critical", "INTEGER NOT NULL DEFAULT 0"),
72
+ ("scope_type", "TEXT NOT NULL DEFAULT 'global'"),
73
+ ("scope_value", "TEXT NOT NULL DEFAULT '*'"),
74
+ ("last_used_at", "REAL NOT NULL DEFAULT 0"),
75
+ )
76
+
77
+ _INSERT_SQL = """
78
+ INSERT INTO memory_nodes (
79
+ node_id, vector, content, metadata,
80
+ base_trust, trigger_count, helpful_count, violation_count, is_critical,
81
+ scope_type, scope_value, created_at, last_used_at
82
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ db_path: Union[str, Path] = ":memory:",
88
+ expected_dim: Optional[int] = None,
89
+ timeout: float = 30.0,
90
+ ) -> None:
91
+ self.db_path = str(db_path)
92
+ self.expected_dim = expected_dim
93
+ self.timeout = timeout
94
+ self._lock = threading.RLock()
95
+ self._conn: Optional[sqlite3.Connection] = None
96
+
97
+ if self.db_path != ":memory:":
98
+ Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
99
+
100
+ self._initialize()
101
+
102
+ def _get_connection(self) -> sqlite3.Connection:
103
+ if self._conn is None:
104
+ try:
105
+ conn = sqlite3.connect(
106
+ self.db_path, timeout=self.timeout, check_same_thread=False
107
+ )
108
+ conn.row_factory = sqlite3.Row
109
+ if self.db_path != ":memory:":
110
+ conn.execute("PRAGMA journal_mode=WAL;")
111
+ conn.execute("PRAGMA synchronous=NORMAL;")
112
+ self._conn = conn
113
+ except sqlite3.Error as exc:
114
+ raise MemoryStorageError(
115
+ f"Failed to open SQLite database at {self.db_path}: {exc}"
116
+ ) from exc
117
+ return self._conn
118
+
119
+ def _initialize(self) -> None:
120
+ try:
121
+ with self._lock:
122
+ conn = self._get_connection()
123
+ with conn:
124
+ conn.execute(self._CREATE_TABLE)
125
+ conn.execute(self._CREATE_META_TABLE)
126
+ for idx_sql in self._CREATE_INDEXES:
127
+ conn.execute(idx_sql)
128
+ self._ensure_schema(conn)
129
+ with conn:
130
+ conn.execute(f"PRAGMA user_version = {self._SCHEMA_VERSION};")
131
+ except sqlite3.Error as exc:
132
+ raise MemoryStorageError(
133
+ f"Failed to initialize memory storage schema: {exc}"
134
+ ) from exc
135
+
136
+ def _ensure_schema(self, conn: sqlite3.Connection) -> None:
137
+ existing = {
138
+ row["name"]
139
+ for row in conn.execute("PRAGMA table_info(memory_nodes);").fetchall()
140
+ }
141
+ for column, definition in self._MIGRATION_COLUMNS:
142
+ if column in existing:
143
+ continue
144
+ try:
145
+ conn.execute(
146
+ f"ALTER TABLE memory_nodes ADD COLUMN {column} {definition};"
147
+ )
148
+ except sqlite3.Error as exc:
149
+ raise MemoryStorageError(
150
+ f"Failed to migrate column '{column}': {exc}"
151
+ ) from exc
152
+
153
+ # ------------------------------------------------------------------ #
154
+ # Config
155
+ # ------------------------------------------------------------------ #
156
+ def set_expected_dim(self, dim: Optional[int]) -> None:
157
+ """Update the vector-dimension constraint. Used by the migrator."""
158
+ self.expected_dim = dim
159
+
160
+ # ------------------------------------------------------------------ #
161
+ # Validation
162
+ # ------------------------------------------------------------------ #
163
+ def _validate_vector(self, vector: List[float]) -> None:
164
+ if not isinstance(vector, list) or not vector:
165
+ raise MemoryStorageError("Vector must be a non-empty list of floats.")
166
+ if self.expected_dim is not None and len(vector) != self.expected_dim:
167
+ raise DimensionMismatchError(self.expected_dim, len(vector))
168
+ for idx, value in enumerate(vector):
169
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
170
+ raise MemoryStorageError(
171
+ f"Vector element at index {idx} is not numeric: {value!r}"
172
+ )
173
+ if math.isnan(value) or math.isinf(value):
174
+ raise MemoryStorageError(
175
+ f"Vector contains NaN/Inf at index {idx}: {value!r}"
176
+ )
177
+
178
+ @staticmethod
179
+ def _serialize_metadata(metadata: dict) -> str:
180
+ try:
181
+ return json.dumps(metadata or {})
182
+ except (TypeError, ValueError) as exc:
183
+ raise MemoryStorageError(
184
+ f"Metadata is not JSON-serializable: {exc}"
185
+ ) from exc
186
+
187
+ def _row_to_node(self, row: sqlite3.Row) -> MemoryNode:
188
+ try:
189
+ vector = json.loads(row["vector"])
190
+ metadata = json.loads(row["metadata"]) if row["metadata"] else {}
191
+ scope_type = MemoryScope(row["scope_type"])
192
+ except (TypeError, ValueError) as exc:
193
+ raise MemoryStorageError(
194
+ f"Corrupted row for node_id={row['node_id']!r}: {exc}"
195
+ ) from exc
196
+
197
+ return MemoryNode(
198
+ node_id=row["node_id"],
199
+ vector=vector,
200
+ content=row["content"],
201
+ metadata=metadata,
202
+ base_trust=float(row["base_trust"]),
203
+ trigger_count=int(row["trigger_count"]),
204
+ helpful_count=int(row["helpful_count"]),
205
+ violation_count=int(row["violation_count"]),
206
+ is_critical=bool(row["is_critical"]),
207
+ scope_type=scope_type,
208
+ scope_value=row["scope_value"],
209
+ created_at=float(row["created_at"]),
210
+ last_used_at=float(row["last_used_at"]),
211
+ )
212
+
213
+ @staticmethod
214
+ def _node_params(node: MemoryNode) -> Tuple:
215
+ return (
216
+ node.node_id,
217
+ json.dumps(node.vector),
218
+ node.content,
219
+ SQLiteMemoryStorage._serialize_metadata(node.metadata),
220
+ float(node.base_trust),
221
+ int(node.trigger_count),
222
+ int(node.helpful_count),
223
+ int(node.violation_count),
224
+ 1 if node.is_critical else 0,
225
+ node.scope_type.value,
226
+ node.scope_value,
227
+ float(node.created_at),
228
+ float(node.last_used_at),
229
+ )
230
+
231
+ # ------------------------------------------------------------------ #
232
+ # Strict public API
233
+ # ------------------------------------------------------------------ #
234
+ def insert(self, node: MemoryNode) -> None:
235
+ if not isinstance(node, MemoryNode):
236
+ raise MemoryStorageError(
237
+ f"insert() expected MemoryNode, got {type(node).__name__}"
238
+ )
239
+ self._validate_vector(node.vector)
240
+ try:
241
+ with self._lock:
242
+ conn = self._get_connection()
243
+ with conn:
244
+ conn.execute(self._INSERT_SQL, self._node_params(node))
245
+ except sqlite3.IntegrityError as exc:
246
+ raise MemoryStorageError(
247
+ f"Memory node '{node.node_id}' already exists: {exc}"
248
+ ) from exc
249
+ except sqlite3.Error as exc:
250
+ raise MemoryStorageError(
251
+ f"Failed to insert memory node '{node.node_id}': {exc}"
252
+ ) from exc
253
+
254
+ def insert_batch(self, nodes: List[MemoryNode]) -> None:
255
+ if not nodes:
256
+ return
257
+ records = []
258
+ for node in nodes:
259
+ if not isinstance(node, MemoryNode):
260
+ raise MemoryStorageError(
261
+ f"insert_batch() expected MemoryNode, got {type(node).__name__}"
262
+ )
263
+ self._validate_vector(node.vector)
264
+ records.append(self._node_params(node))
265
+ try:
266
+ with self._lock:
267
+ conn = self._get_connection()
268
+ with conn:
269
+ conn.executemany(self._INSERT_SQL, records)
270
+ except sqlite3.IntegrityError as exc:
271
+ raise MemoryStorageError(
272
+ f"Batch insert failed due to constraint violation: {exc}"
273
+ ) from exc
274
+ except sqlite3.Error as exc:
275
+ raise MemoryStorageError(f"Batch insert failed: {exc}") from exc
276
+
277
+ def replace_all(self, nodes: List[MemoryNode]) -> None:
278
+ """
279
+ Atomically replace the entire node table with `nodes`.
280
+ Used by the migrator. Preserves nothing implicitly — the caller
281
+ must supply fully-formed nodes.
282
+ """
283
+ records = []
284
+ for node in nodes:
285
+ if not isinstance(node, MemoryNode):
286
+ raise MemoryStorageError(
287
+ f"replace_all() expected MemoryNode, got {type(node).__name__}"
288
+ )
289
+ self._validate_vector(node.vector)
290
+ records.append(self._node_params(node))
291
+ try:
292
+ with self._lock:
293
+ conn = self._get_connection()
294
+ with conn:
295
+ conn.execute("DELETE FROM memory_nodes;")
296
+ if records:
297
+ conn.executemany(self._INSERT_SQL, records)
298
+ except sqlite3.Error as exc:
299
+ raise MemoryStorageError(
300
+ f"Atomic replace_all failed: {exc}"
301
+ ) from exc
302
+
303
+ def get(self, node_id: str) -> MemoryNode:
304
+ if not isinstance(node_id, str) or not node_id:
305
+ raise MemoryStorageError("node_id must be a non-empty string.")
306
+ try:
307
+ with self._lock:
308
+ conn = self._get_connection()
309
+ row = conn.execute(
310
+ "SELECT * FROM memory_nodes WHERE node_id = ?;", (node_id,)
311
+ ).fetchone()
312
+ except sqlite3.Error as exc:
313
+ raise MemoryStorageError(
314
+ f"Failed to retrieve memory node '{node_id}': {exc}"
315
+ ) from exc
316
+ if row is None:
317
+ raise MemoryNotFoundError(node_id)
318
+ return self._row_to_node(row)
319
+
320
+ def query(self, vector: List[float], limit: int = 10) -> List[MemoryNode]:
321
+ if limit <= 0:
322
+ return []
323
+ self._validate_vector(vector)
324
+ try:
325
+ with self._lock:
326
+ conn = self._get_connection()
327
+ rows = conn.execute("SELECT * FROM memory_nodes;").fetchall()
328
+ except sqlite3.Error as exc:
329
+ raise MemoryStorageError(f"Failed to scan memory nodes: {exc}") from exc
330
+
331
+ query_norm = math.sqrt(sum(v * v for v in vector))
332
+ if query_norm == 0.0:
333
+ return []
334
+
335
+ scored: List[Tuple[float, MemoryNode]] = []
336
+ for row in rows:
337
+ node = self._row_to_node(row)
338
+ if len(node.vector) != len(vector):
339
+ continue
340
+ node_norm = math.sqrt(sum(v * v for v in node.vector))
341
+ if node_norm == 0.0:
342
+ continue
343
+ dot = sum(a * b for a, b in zip(vector, node.vector))
344
+ scored.append((dot / (query_norm * node_norm), node))
345
+ scored.sort(key=lambda pair: pair[0], reverse=True)
346
+ return [node for _, node in scored[:limit]]
347
+
348
+ def delete(self, node_id: str) -> bool:
349
+ try:
350
+ with self._lock:
351
+ conn = self._get_connection()
352
+ with conn:
353
+ cur = conn.execute(
354
+ "DELETE FROM memory_nodes WHERE node_id = ?;", (node_id,)
355
+ )
356
+ return cur.rowcount > 0
357
+ except sqlite3.Error as exc:
358
+ raise MemoryStorageError(
359
+ f"Failed to delete memory node '{node_id}': {exc}"
360
+ ) from exc
361
+
362
+ # ------------------------------------------------------------------ #
363
+ # Extended API
364
+ # ------------------------------------------------------------------ #
365
+ def list_all(self) -> List[MemoryNode]:
366
+ try:
367
+ with self._lock:
368
+ conn = self._get_connection()
369
+ rows = conn.execute("SELECT * FROM memory_nodes;").fetchall()
370
+ except sqlite3.Error as exc:
371
+ raise MemoryStorageError(f"Failed to list memory nodes: {exc}") from exc
372
+ return [self._row_to_node(row) for row in rows]
373
+
374
+ def query_scoped(
375
+ self,
376
+ scope_type: Optional[MemoryScope] = None,
377
+ scope_value: Optional[str] = None,
378
+ limit: Optional[int] = None,
379
+ ) -> List[MemoryNode]:
380
+ clauses: List[str] = []
381
+ params: List[object] = []
382
+ if scope_type is not None:
383
+ clauses.append("scope_type = ?")
384
+ params.append(scope_type.value)
385
+ if scope_value is not None:
386
+ clauses.append("scope_value = ?")
387
+ params.append(scope_value)
388
+ sql = "SELECT * FROM memory_nodes"
389
+ if clauses:
390
+ sql += " WHERE " + " AND ".join(clauses)
391
+ sql += " ORDER BY last_used_at DESC"
392
+ if limit is not None and limit > 0:
393
+ sql += f" LIMIT {int(limit)}"
394
+ try:
395
+ with self._lock:
396
+ conn = self._get_connection()
397
+ rows = conn.execute(sql, params).fetchall()
398
+ except sqlite3.Error as exc:
399
+ raise MemoryStorageError(f"Failed to query scoped nodes: {exc}") from exc
400
+ return [self._row_to_node(row) for row in rows]
401
+
402
+ # ------------------------------------------------------------------ #
403
+ # Metadata (schema_meta)
404
+ # ------------------------------------------------------------------ #
405
+ def get_meta(self, key: str) -> Optional[str]:
406
+ try:
407
+ with self._lock:
408
+ conn = self._get_connection()
409
+ row = conn.execute(
410
+ "SELECT value FROM schema_meta WHERE key = ?;", (key,)
411
+ ).fetchone()
412
+ except sqlite3.Error as exc:
413
+ raise MemoryStorageError(
414
+ f"Failed to read schema_meta['{key}']: {exc}"
415
+ ) from exc
416
+ return row["value"] if row else None
417
+
418
+ def set_meta(self, key: str, value: str) -> None:
419
+ try:
420
+ with self._lock:
421
+ conn = self._get_connection()
422
+ with conn:
423
+ conn.execute(
424
+ """
425
+ INSERT INTO schema_meta (key, value, updated_at)
426
+ VALUES (?, ?, ?)
427
+ ON CONFLICT(key) DO UPDATE SET
428
+ value = excluded.value,
429
+ updated_at = excluded.updated_at;
430
+ """,
431
+ (key, value, time.time()),
432
+ )
433
+ except sqlite3.Error as exc:
434
+ raise MemoryStorageError(
435
+ f"Failed to write schema_meta['{key}']: {exc}"
436
+ ) from exc
437
+
438
+ # ------------------------------------------------------------------ #
439
+ # Feedback loop
440
+ # ------------------------------------------------------------------ #
441
+ def record_trigger(
442
+ self, node_ids: List[str], touched_at: Optional[float] = None
443
+ ) -> int:
444
+ return self._apply_deltas(
445
+ node_ids, trigger_delta=1, helpful_delta=0,
446
+ violation_delta=0, touched_at=touched_at,
447
+ )
448
+
449
+ def record_helpful(self, node_ids: List[str]) -> int:
450
+ return self._apply_deltas(
451
+ node_ids, trigger_delta=0, helpful_delta=1, violation_delta=0
452
+ )
453
+
454
+ def record_violation(self, node_ids: List[str]) -> int:
455
+ return self._apply_deltas(
456
+ node_ids, trigger_delta=0, helpful_delta=0, violation_delta=1
457
+ )
458
+
459
+ def _apply_deltas(
460
+ self,
461
+ node_ids: List[str],
462
+ trigger_delta: int,
463
+ helpful_delta: int,
464
+ violation_delta: int,
465
+ touched_at: Optional[float] = None,
466
+ ) -> int:
467
+ if not node_ids:
468
+ return 0
469
+ ts = time.time() if touched_at is None else float(touched_at)
470
+ affected = 0
471
+ try:
472
+ with self._lock:
473
+ conn = self._get_connection()
474
+ with conn:
475
+ for node_id in node_ids:
476
+ cur = conn.execute(
477
+ """
478
+ UPDATE memory_nodes
479
+ SET trigger_count = trigger_count + ?,
480
+ helpful_count = helpful_count + ?,
481
+ violation_count = violation_count + ?,
482
+ last_used_at = ?
483
+ WHERE node_id = ?;
484
+ """,
485
+ (trigger_delta, helpful_delta, violation_delta, ts, node_id),
486
+ )
487
+ affected += cur.rowcount
488
+ except sqlite3.Error as exc:
489
+ raise MemoryStorageError(
490
+ f"Failed to apply feedback deltas: {exc}"
491
+ ) from exc
492
+ return affected
493
+
494
+ def promote(self, node_id: str, base_trust: float = 1.0) -> None:
495
+ if not 0.0 <= base_trust <= 1.0:
496
+ raise MemoryStorageError(
497
+ f"base_trust must be within [0.0, 1.0], got {base_trust!r}"
498
+ )
499
+ try:
500
+ with self._lock:
501
+ conn = self._get_connection()
502
+ with conn:
503
+ cur = conn.execute(
504
+ """
505
+ UPDATE memory_nodes
506
+ SET base_trust = ?, is_critical = 1
507
+ WHERE node_id = ?;
508
+ """,
509
+ (float(base_trust), node_id),
510
+ )
511
+ if cur.rowcount == 0:
512
+ raise MemoryNotFoundError(node_id)
513
+ except MemoryNotFoundError:
514
+ raise
515
+ except sqlite3.Error as exc:
516
+ raise MemoryStorageError(
517
+ f"Failed to promote memory node '{node_id}': {exc}"
518
+ ) from exc
519
+
520
+ def count(self) -> int:
521
+ try:
522
+ with self._lock:
523
+ conn = self._get_connection()
524
+ row = conn.execute("SELECT COUNT(*) FROM memory_nodes;").fetchone()
525
+ return int(row[0]) if row else 0
526
+ except sqlite3.Error as exc:
527
+ raise MemoryStorageError(f"Failed to count memory nodes: {exc}") from exc
528
+
529
+ def clear(self) -> None:
530
+ try:
531
+ with self._lock:
532
+ conn = self._get_connection()
533
+ with conn:
534
+ conn.execute("DELETE FROM memory_nodes;")
535
+ except sqlite3.Error as exc:
536
+ raise MemoryStorageError(f"Failed to clear memory store: {exc}") from exc
537
+
538
+ def close(self) -> None:
539
+ with self._lock:
540
+ if self._conn is not None:
541
+ try:
542
+ self._conn.close()
543
+ except sqlite3.Error:
544
+ pass
545
+ finally:
546
+ self._conn = None
547
+
548
+ def __enter__(self) -> "SQLiteMemoryStorage":
549
+ return self
550
+
551
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
552
+ self.close()