superlocalmemory 3.8.13 → 4.0.0

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 (212) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -121
  3. package/README.md +65 -63
  4. package/docs/pi-dev-integration.md +1 -1
  5. package/package.json +6 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +1 -1
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +1 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +1 -1
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  29. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  31. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  32. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +11 -4
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/cli/commands.py +125 -11
  41. package/src/superlocalmemory/cli/daemon.py +5 -1
  42. package/src/superlocalmemory/cli/main.py +35 -2
  43. package/src/superlocalmemory/cli/ops_cmd.py +281 -0
  44. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  45. package/src/superlocalmemory/compliance/audit.py +65 -0
  46. package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
  47. package/src/superlocalmemory/compliance/gdpr.py +416 -20
  48. package/src/superlocalmemory/compliance/retention.py +74 -22
  49. package/src/superlocalmemory/compliance/scheduler.py +78 -9
  50. package/src/superlocalmemory/core/actor_context.py +166 -0
  51. package/src/superlocalmemory/core/admission.py +549 -0
  52. package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
  53. package/src/superlocalmemory/core/config.py +202 -24
  54. package/src/superlocalmemory/core/consolidation_engine.py +13 -13
  55. package/src/superlocalmemory/core/context_cache.py +28 -0
  56. package/src/superlocalmemory/core/embeddings.py +64 -2
  57. package/src/superlocalmemory/core/engine.py +7 -2
  58. package/src/superlocalmemory/core/engine_ingestion.py +65 -3
  59. package/src/superlocalmemory/core/engine_wiring.py +36 -9
  60. package/src/superlocalmemory/core/ingest_policy.py +38 -0
  61. package/src/superlocalmemory/core/maintenance.py +255 -0
  62. package/src/superlocalmemory/core/modes.py +40 -13
  63. package/src/superlocalmemory/core/mutations.py +437 -44
  64. package/src/superlocalmemory/core/operation_policy.py +92 -0
  65. package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
  66. package/src/superlocalmemory/core/operation_request.py +127 -0
  67. package/src/superlocalmemory/core/ops_remediation.py +542 -0
  68. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  69. package/src/superlocalmemory/core/remember_runtime.py +202 -4
  70. package/src/superlocalmemory/core/remote_mode.py +20 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +150 -0
  72. package/src/superlocalmemory/core/topic_signature.py +19 -4
  73. package/src/superlocalmemory/core/transactions/__init__.py +78 -0
  74. package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
  75. package/src/superlocalmemory/core/transactions/erasure.py +825 -0
  76. package/src/superlocalmemory/core/transactions/manifest.py +255 -0
  77. package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
  78. package/src/superlocalmemory/core/transactions/obligations.py +272 -0
  79. package/src/superlocalmemory/core/transactions/owners.py +114 -0
  80. package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
  81. package/src/superlocalmemory/core/transactions/service.py +330 -0
  82. package/src/superlocalmemory/core/worker_pool.py +33 -5
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
  84. package/src/superlocalmemory/encoding/emotional.py +75 -14
  85. package/src/superlocalmemory/encoding/scene_builder.py +115 -13
  86. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  87. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  88. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  89. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  90. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  91. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  92. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  93. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  94. package/src/superlocalmemory/evolution/types.py +30 -8
  95. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  96. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  97. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  98. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  99. package/src/superlocalmemory/infra/backup.py +434 -7
  100. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  101. package/src/superlocalmemory/infra/self_heal.py +401 -0
  102. package/src/superlocalmemory/learning/feedback.py +52 -9
  103. package/src/superlocalmemory/loops/engine.py +10 -0
  104. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  105. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  106. package/src/superlocalmemory/mcp/profiles.py +5 -0
  107. package/src/superlocalmemory/mcp/resources.py +8 -0
  108. package/src/superlocalmemory/mcp/server.py +51 -4
  109. package/src/superlocalmemory/mcp/shared.py +19 -0
  110. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  111. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  112. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  113. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  114. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  115. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  116. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  117. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  118. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  119. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  120. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  121. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  122. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  123. package/src/superlocalmemory/mesh/broker.py +124 -46
  124. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  125. package/src/superlocalmemory/mesh/discovery.py +365 -0
  126. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  127. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  128. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  129. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  130. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  131. package/src/superlocalmemory/optimize/config/store.py +45 -0
  132. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  133. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  134. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  135. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  136. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  137. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  138. package/src/superlocalmemory/retrieval/engine.py +19 -4
  139. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  140. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  141. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  142. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  143. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  144. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  145. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  146. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  147. package/src/superlocalmemory/server/api.py +5 -5
  148. package/src/superlocalmemory/server/egress_policy.py +258 -0
  149. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  150. package/src/superlocalmemory/server/route_mutations.py +20 -0
  151. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  152. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  153. package/src/superlocalmemory/server/routes/events.py +15 -0
  154. package/src/superlocalmemory/server/routes/memories.py +56 -3
  155. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  156. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  157. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  158. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  159. package/src/superlocalmemory/server/routes/ws.py +86 -0
  160. package/src/superlocalmemory/server/ui.py +6 -6
  161. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  162. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  163. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  164. package/src/superlocalmemory/storage/database.py +329 -24
  165. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  166. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  167. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  168. package/src/superlocalmemory/storage/migration_runner.py +140 -417
  169. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  170. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  171. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  172. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  173. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  174. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  175. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  176. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
  177. package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
  178. package/src/superlocalmemory/storage/schema.py +67 -0
  179. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  180. package/src/superlocalmemory/trust/scorer.py +28 -4
  181. package/src/superlocalmemory/ui/index.html +14 -3
  182. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  183. package/src/superlocalmemory/ui/js/brain.js +6 -4
  184. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  185. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  186. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  187. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  188. package/src/superlocalmemory/ui/js/modal.js +272 -5
  189. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  190. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  191. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  192. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  193. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  194. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  195. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  196. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  197. package/src/superlocalmemory/ui/js/settings.js +7 -1
  198. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  199. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  200. package/src/superlocalmemory/cli/post_install.py +0 -114
  201. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  202. package/src/superlocalmemory/core/db_pool.py +0 -80
  203. package/src/superlocalmemory/core/error_catalog.py +0 -113
  204. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  205. package/src/superlocalmemory/core/priority_queue.py +0 -61
  206. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  207. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  208. package/src/superlocalmemory/core/slmignore.py +0 -125
  209. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  210. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  211. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  212. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -1,61 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
-
5
- """Weighted fair scheduler for high/low job lanes.
6
-
7
- Part of Qualixar | Author: Varun Pratap Bhardwaj
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import threading
13
- from typing import Literal
14
-
15
- Lane = Literal["high", "low"]
16
-
17
-
18
- class WFQScheduler:
19
- """Deficit-round-robin-ish scheduler over two lanes.
20
-
21
- Tracks served counts and picks the lane whose ratio is furthest
22
- below its target share. Approximates the target ratio over any
23
- rolling window without requiring a time source.
24
- """
25
-
26
- def __init__(self, high_weight: int = 70, low_weight: int = 30) -> None:
27
- if high_weight <= 0 or low_weight <= 0:
28
- raise ValueError("weights must be positive")
29
- self.high_weight = high_weight
30
- self.low_weight = low_weight
31
- self._served = {"high": 0, "low": 0}
32
- self._lock = threading.Lock()
33
-
34
- def pick_lane(self, *, has_high: bool, has_low: bool) -> Lane | None:
35
- if not has_high and not has_low:
36
- return None
37
- if has_high and not has_low:
38
- return "high"
39
- if has_low and not has_high:
40
- return "low"
41
- with self._lock:
42
- total = self._served["high"] + self._served["low"]
43
- if total == 0:
44
- return "high"
45
- total_weight = self.high_weight + self.low_weight
46
- high_target = self.high_weight / total_weight
47
- low_target = self.low_weight / total_weight
48
- high_ratio = self._served["high"] / total
49
- low_ratio = self._served["low"] / total
50
- # Pick whichever lane is further below its target
51
- high_deficit = high_target - high_ratio
52
- low_deficit = low_target - low_ratio
53
- return "high" if high_deficit >= low_deficit else "low"
54
-
55
- def record_served(self, lane: Lane) -> None:
56
- with self._lock:
57
- self._served[lane] += 1
58
-
59
- def snapshot(self) -> dict[str, int]:
60
- with self._lock:
61
- return dict(self._served)
@@ -1,216 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
-
5
- """SuperLocalMemory v3.4.5 — Graph Pruning Engine.
6
-
7
- Reduces graph edge count without losing meaningful connections.
8
- Strategies:
9
- 1. Chain collapse: A→B→C → remove B→C if A→B exists with higher weight
10
- 2. Garbage entity removal: remove edges connected to garbage entities
11
- 3. Low-activity edge decay: edges between entities not accessed in 90+ days
12
-
13
- CRITICAL RULE: NEVER delete atomic_facts. Only prune graph_edges.
14
- Edges are derivable from facts — they can be regenerated.
15
- Facts are the permanent record.
16
-
17
- Part of Qualixar | Author: Varun Pratap Bhardwaj
18
- """
19
-
20
- from __future__ import annotations
21
-
22
- import logging
23
- from datetime import datetime, timedelta, UTC
24
- from typing import TYPE_CHECKING
25
-
26
- if TYPE_CHECKING:
27
- from superlocalmemory.storage.database import DatabaseManager
28
-
29
- logger = logging.getLogger(__name__)
30
-
31
- # Thresholds
32
- LOW_ACTIVITY_DAYS = 90
33
- CHAIN_COLLAPSE_MIN_WEIGHT_RATIO = 0.8
34
- BATCH_SIZE = 500
35
-
36
-
37
- # ---------------------------------------------------------------------------
38
- # Public API
39
- # ---------------------------------------------------------------------------
40
-
41
- def prune_graph(
42
- db: DatabaseManager,
43
- profile_id: str = "default",
44
- dry_run: bool = False,
45
- ) -> dict[str, int]:
46
- """Prune graph edges using all strategies.
47
-
48
- Returns counts of edges removed per strategy.
49
- Safe to run repeatedly — idempotent.
50
- """
51
- stats = {
52
- "chain_collapsed": 0,
53
- "garbage_removed": 0,
54
- "low_activity_decayed": 0,
55
- "total_removed": 0,
56
- "edges_before": 0,
57
- "edges_after": 0,
58
- }
59
-
60
- # Count before
61
- rows = db.execute(
62
- "SELECT COUNT(*) as c FROM graph_edges", ()
63
- )
64
- stats["edges_before"] = rows[0]["c"] if rows else 0
65
-
66
- # Strategy 1: Chain collapse
67
- collapsed = _collapse_chains(db, profile_id, dry_run)
68
- stats["chain_collapsed"] = collapsed
69
-
70
- # Strategy 2: Garbage entity edges
71
- garbage = _remove_garbage_edges(db, profile_id, dry_run)
72
- stats["garbage_removed"] = garbage
73
-
74
- # Strategy 3: Low-activity edge decay
75
- decayed = _decay_low_activity_edges(db, profile_id, dry_run)
76
- stats["low_activity_decayed"] = decayed
77
-
78
- stats["total_removed"] = collapsed + garbage + decayed
79
-
80
- # Count after
81
- rows = db.execute(
82
- "SELECT COUNT(*) as c FROM graph_edges", ()
83
- )
84
- stats["edges_after"] = rows[0]["c"] if rows else 0
85
-
86
- if stats["total_removed"] > 0:
87
- logger.info(
88
- "Graph pruning: %d edges removed (%d → %d)",
89
- stats["total_removed"], stats["edges_before"], stats["edges_after"],
90
- )
91
-
92
- return stats
93
-
94
-
95
- # ---------------------------------------------------------------------------
96
- # Strategy 1: Chain Collapse
97
- # ---------------------------------------------------------------------------
98
-
99
- def _collapse_chains(db: DatabaseManager, profile_id: str, dry_run: bool) -> int:
100
- """Collapse redundant chain edges.
101
-
102
- If A→B (weight=0.9) and B→C (weight=0.5), and A→C also exists,
103
- remove B→C if A→C weight >= B→C weight * threshold.
104
-
105
- This preserves the semantic connection (A→C is stronger) while
106
- removing intermediate edges.
107
- """
108
- try:
109
- rows = db.execute("""
110
- SELECT ge1.source_id as a, ge1.target_id as b, ge1.weight as w_ab,
111
- ge2.source_id as b2, ge2.target_id as c, ge2.weight as w_bc,
112
- ge3.weight as w_ac
113
- FROM graph_edges ge1
114
- JOIN graph_edges ge2 ON ge1.target_id = ge2.source_id
115
- LEFT JOIN graph_edges ge3 ON ge1.source_id = ge3.source_id
116
- AND ge2.target_id = ge3.target_id
117
- WHERE ge3.weight >= ge2.weight * ?
118
- LIMIT ?
119
- """, (CHAIN_COLLAPSE_MIN_WEIGHT_RATIO, BATCH_SIZE))
120
- except Exception as exc:
121
- logger.warning("Chain collapse query failed: %s", exc)
122
- return 0
123
-
124
- remove_ids = []
125
- for row in rows:
126
- b_id = row["b"]
127
- c_id = row["c"]
128
- # Remove B→C edge
129
- if not dry_run:
130
- db.execute(
131
- "DELETE FROM graph_edges WHERE source_id = ? AND target_id = ?",
132
- (b_id, c_id),
133
- )
134
- remove_ids.append((b_id, c_id))
135
-
136
- if remove_ids and not dry_run:
137
- logger.info("Chain collapse: removed %d edges", len(remove_ids))
138
-
139
- return len(remove_ids)
140
-
141
-
142
- # ---------------------------------------------------------------------------
143
- # Strategy 2: Garbage Entity Edges
144
- # ---------------------------------------------------------------------------
145
-
146
- def _remove_garbage_edges(db: DatabaseManager, profile_id: str, dry_run: bool) -> int:
147
- """Remove edges connected to garbage/blacklisted entities."""
148
- try:
149
- rows = db.execute("""
150
- SELECT ge.source_id, ge.target_id
151
- FROM graph_edges ge
152
- WHERE ge.source_id IN (SELECT term FROM entity_blacklist)
153
- OR ge.target_id IN (SELECT term FROM entity_blacklist)
154
- LIMIT ?
155
- """, (BATCH_SIZE,))
156
- except Exception:
157
- return 0
158
-
159
- count = 0
160
- for row in rows:
161
- if not dry_run:
162
- db.execute(
163
- "DELETE FROM graph_edges WHERE source_id = ? AND target_id = ?",
164
- (row["source_id"], row["target_id"]),
165
- )
166
- count += 1
167
-
168
- if count and not dry_run:
169
- logger.info("Garbage edges removed: %d", count)
170
-
171
- return count
172
-
173
-
174
- # ---------------------------------------------------------------------------
175
- # Strategy 3: Low-Activity Edge Decay
176
- # ---------------------------------------------------------------------------
177
-
178
- def _decay_low_activity_edges(
179
- db: DatabaseManager, profile_id: str, dry_run: bool,
180
- ) -> int:
181
- """Remove edges between entities not accessed in 90+ days.
182
-
183
- Only removes edges where BOTH connected entities have no recent access.
184
- """
185
- cutoff = (datetime.now(UTC) - timedelta(days=LOW_ACTIVITY_DAYS)).isoformat()
186
-
187
- try:
188
- rows = db.execute("""
189
- SELECT ge.source_id, ge.target_id
190
- FROM graph_edges ge
191
- WHERE ge.source_id NOT IN (
192
- SELECT DISTINCT entity_id FROM fact_access_log
193
- WHERE accessed_at >= ?
194
- )
195
- AND ge.target_id NOT IN (
196
- SELECT DISTINCT entity_id FROM fact_access_log
197
- WHERE accessed_at >= ?
198
- )
199
- LIMIT ?
200
- """, (cutoff, cutoff, BATCH_SIZE))
201
- except Exception:
202
- return 0
203
-
204
- count = 0
205
- for row in rows:
206
- if not dry_run:
207
- db.execute(
208
- "DELETE FROM graph_edges WHERE source_id = ? AND target_id = ?",
209
- (row["source_id"], row["target_id"]),
210
- )
211
- count += 1
212
-
213
- if count and not dry_run:
214
- logger.info("Low-activity edges decayed: %d", count)
215
-
216
- return count
@@ -1,73 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
-
5
- """Queue-backed dispatcher coordinator.
6
-
7
- Part of Qualixar | Author: Varun Pratap Bhardwaj
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import os
13
- from pathlib import Path
14
- from typing import Any
15
-
16
- from superlocalmemory.core import rate_limit as rl
17
- from superlocalmemory.core import recall_queue as rq
18
- from superlocalmemory.core.engine_lock import EngineRWLock
19
-
20
-
21
- class QueueDispatcher:
22
- """Coordinates rate-limit check, enqueue, and poll for callers."""
23
-
24
- def __init__(
25
- self,
26
- *,
27
- db_path: Path | str,
28
- global_rps: float = 100.0,
29
- per_pid_rps: float = 30.0,
30
- per_agent_rps: float = 10.0,
31
- ) -> None:
32
- self.queue = rq.RecallQueue(db_path=db_path)
33
- self.engine_lock = EngineRWLock()
34
- self._rate = rl.LayeredRateLimiter(
35
- global_rps=global_rps,
36
- per_pid_rps=per_pid_rps,
37
- per_agent_rps=per_agent_rps,
38
- )
39
- # Module handles kept for test introspection.
40
- self.rl = rl
41
- self.rq = rq
42
-
43
- def _check_rate(self, *, pid: int, agent_id: str | None) -> None:
44
- self._rate.check_and_consume(pid=pid, agent_id=agent_id)
45
-
46
- def dispatch(
47
- self,
48
- *,
49
- query: str,
50
- limit_n: int,
51
- mode: str,
52
- agent_id: str,
53
- session_id: str,
54
- tenant_id: str = "",
55
- namespace: str = "",
56
- priority: str = "high",
57
- stall_timeout_s: float = 25.0,
58
- timeout_s: float = 30.0,
59
- ) -> dict[str, Any]:
60
- self._check_rate(pid=os.getpid(), agent_id=agent_id)
61
- rid = self.queue.enqueue(
62
- query=query, limit_n=limit_n, mode=mode,
63
- agent_id=agent_id, session_id=session_id,
64
- tenant_id=tenant_id, namespace=namespace,
65
- priority=priority, stall_timeout_s=stall_timeout_s,
66
- )
67
- try:
68
- return self.queue.poll_result(rid, timeout_s=timeout_s)
69
- finally:
70
- self.queue.unsubscribe(rid)
71
-
72
- def close(self) -> None:
73
- self.queue.close()
@@ -1,125 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory v3.4.22 — Stage 8 SB-5
4
-
5
- """Path-level opt-out via ``.slmignore``.
6
-
7
- A repository-scoped escape hatch: drop a ``.slmignore`` at any
8
- ancestor of a workspace and SLM will skip hooks / recall / remember
9
- for any path inside that ancestor. Roughly the shape of ``.gitignore``
10
- but with simpler matching — one path pattern per line, ``#`` comments,
11
- whitespace stripped.
12
-
13
- Matching rules (intentionally minimal):
14
-
15
- - Lines starting with ``#`` or empty ⇒ ignored.
16
- - A line like ``node_modules`` matches any path segment named
17
- ``node_modules`` anywhere in the resolved absolute path.
18
- - A line starting with ``/`` is an absolute-prefix match
19
- (``/Users/me/secret`` matches everything under that dir).
20
- - Glob chars ``*`` / ``?`` are treated literally — keep it boring,
21
- avoid re-implementing ``.gitignore``'s subtleties.
22
-
23
- Cache: the parsed ignore list is memoised per ignore-file path + mtime.
24
- Look-up cost at the hook hot path is O(depth × patterns) and
25
- patterns ≤ 50 in practice.
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- import os
31
- from pathlib import Path
32
-
33
- _FILENAME = ".slmignore"
34
- _CACHE: dict[tuple[str, float], tuple[str, ...]] = {}
35
- _CACHE_CAP = 64
36
-
37
-
38
- def _load_patterns(ignore_path: Path) -> tuple[str, ...]:
39
- """Parse one ``.slmignore`` file, memoised by path + mtime."""
40
- try:
41
- stat = ignore_path.stat()
42
- except OSError:
43
- return ()
44
- key = (str(ignore_path), stat.st_mtime)
45
- cached = _CACHE.get(key)
46
- if cached is not None:
47
- return cached
48
- try:
49
- raw = ignore_path.read_text(encoding="utf-8", errors="ignore")
50
- except OSError:
51
- return ()
52
- patterns: list[str] = []
53
- for line in raw.splitlines():
54
- stripped = line.strip()
55
- if not stripped or stripped.startswith("#"):
56
- continue
57
- patterns.append(stripped)
58
- result = tuple(patterns)
59
- if len(_CACHE) >= _CACHE_CAP:
60
- _CACHE.clear()
61
- _CACHE[key] = result
62
- return result
63
-
64
-
65
- def _iter_ancestor_ignores(target: Path) -> list[tuple[Path, tuple[str, ...]]]:
66
- """Walk from ``target`` up to the filesystem root collecting ignore files."""
67
- hits: list[tuple[Path, tuple[str, ...]]] = []
68
- seen_dirs: set[Path] = set()
69
- probe = target if target.is_dir() else target.parent
70
- while probe not in seen_dirs:
71
- seen_dirs.add(probe)
72
- candidate = probe / _FILENAME
73
- if candidate.is_file():
74
- patterns = _load_patterns(candidate)
75
- if patterns:
76
- hits.append((candidate.parent, patterns))
77
- if probe.parent == probe:
78
- break
79
- probe = probe.parent
80
- return hits
81
-
82
-
83
- def path_is_ignored(target: str | Path) -> bool:
84
- """Return True iff any ancestor ``.slmignore`` ignores the given path.
85
-
86
- Absolute paths are resolved (symlinks preserved — we match on name,
87
- not realpath, so a symlink into an ignored dir still matches).
88
- Non-existent targets are allowed; we walk the theoretical ancestry.
89
- """
90
- p = Path(target)
91
- try:
92
- abs_path = p.resolve(strict=False)
93
- except OSError:
94
- abs_path = p
95
- segments = set(abs_path.parts)
96
- for ignore_dir, patterns in _iter_ancestor_ignores(abs_path):
97
- for pat in patterns:
98
- if pat.startswith("/"):
99
- # Absolute prefix: the ignore file's directory provides the
100
- # anchor for relative-looking absolute patterns.
101
- candidate = Path(pat)
102
- try:
103
- abs_path.relative_to(candidate)
104
- return True
105
- except ValueError:
106
- continue
107
- else:
108
- # Match any path segment.
109
- if pat in segments:
110
- return True
111
- # Also honour a per-ignore-dir relative path.
112
- rel = abs_path.relative_to(ignore_dir) if (
113
- ignore_dir in abs_path.parents or ignore_dir == abs_path
114
- ) else None
115
- if rel is not None and pat in set(rel.parts):
116
- return True
117
- return False
118
-
119
-
120
- def clear_cache() -> None:
121
- """Test-only helper: drop the memoised pattern cache."""
122
- _CACHE.clear()
123
-
124
-
125
- __all__ = ("path_is_ignored", "clear_cache")
@@ -1,140 +0,0 @@
1
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
- # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
-
5
- """SuperLocalMemory V3 -- Parent Heartbeat Monitor.
6
-
7
- Daemon thread that checks if the parent process (IDE/Claude session) is
8
- still alive. If the parent dies, initiates graceful shutdown to prevent
9
- zombie SLM processes consuming 1.5-2 GB each.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import logging
15
- import os
16
- import threading
17
- from typing import Callable, Optional
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
-
22
- class HeartbeatMonitor:
23
- """Monitor parent process liveness via a daemon thread.
24
-
25
- When the parent PID is detected as dead, calls the provided
26
- shutdown_callback. The monitoring thread is a daemon thread
27
- (auto-dies with the main process per HR-06).
28
- """
29
-
30
- def __init__(
31
- self,
32
- parent_pid: int,
33
- interval_seconds: int,
34
- shutdown_callback: Callable[[], None],
35
- ) -> None:
36
- self._parent_pid = parent_pid
37
- self._interval = interval_seconds
38
- self._shutdown_callback = shutdown_callback
39
- self._thread: Optional[threading.Thread] = None
40
- self._stop_event = threading.Event()
41
- self._running = False
42
-
43
- # -- Lifecycle ----------------------------------------------------------
44
-
45
- def start(self) -> None:
46
- """Start the heartbeat monitoring daemon thread."""
47
- if self._running:
48
- logger.warning("Heartbeat monitor already running")
49
- return
50
-
51
- # HR-02 equivalent: refuse to monitor PID 0 or 1
52
- if self._parent_pid <= 1:
53
- logger.warning(
54
- "Refusing to monitor PID %d (<= 1), heartbeat not started",
55
- self._parent_pid,
56
- )
57
- return
58
-
59
- self._stop_event.clear()
60
- self._thread = threading.Thread(
61
- target=self._monitor_loop,
62
- name="slm-heartbeat",
63
- daemon=True,
64
- )
65
- self._thread.start()
66
- self._running = True
67
- logger.info(
68
- "Heartbeat monitor started: watching parent PID %d every %ds",
69
- self._parent_pid,
70
- self._interval,
71
- )
72
-
73
- def stop(self) -> None:
74
- """Stop the heartbeat monitor gracefully."""
75
- if not self._running:
76
- return
77
-
78
- self._stop_event.set()
79
- if self._thread is not None and self._thread.is_alive():
80
- self._thread.join(timeout=self._interval + 2)
81
-
82
- self._running = False
83
- logger.info("Heartbeat monitor stopped")
84
-
85
- # -- Properties ---------------------------------------------------------
86
-
87
- @property
88
- def is_running(self) -> bool:
89
- """Whether the monitor thread is active."""
90
- return self._running
91
-
92
- # -- Internal -----------------------------------------------------------
93
-
94
- def _monitor_loop(self) -> None:
95
- """Heartbeat loop running in daemon thread.
96
-
97
- Uses threading.Event.wait(timeout) instead of time.sleep()
98
- because Event.wait() is immediately interruptible by stop(),
99
- while sleep() blocks for the full duration.
100
- """
101
- logger.debug(
102
- "Heartbeat loop started for parent PID %d", self._parent_pid
103
- )
104
-
105
- while not self._stop_event.is_set():
106
- stopped = self._stop_event.wait(timeout=self._interval)
107
- if stopped:
108
- break
109
-
110
- if not self._is_parent_alive():
111
- logger.warning(
112
- "Parent PID %d died, initiating graceful shutdown",
113
- self._parent_pid,
114
- )
115
- try:
116
- self._shutdown_callback()
117
- except Exception:
118
- logger.exception("Shutdown callback failed")
119
- break
120
-
121
- logger.debug("Heartbeat loop exited")
122
-
123
- def _is_parent_alive(self) -> bool:
124
- """Check if parent PID is still a running process.
125
-
126
- Conservative: returns True on PermissionError (parent exists
127
- but is owned by another user).
128
- """
129
- if self._parent_pid <= 1:
130
- return False
131
-
132
- try:
133
- os.kill(self._parent_pid, 0)
134
- return True
135
- except ProcessLookupError:
136
- return False
137
- except PermissionError:
138
- return True # Alive, different user -- conservative
139
- except OSError:
140
- return False