superlocalmemory 3.2.2 → 3.3.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 (53) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +106 -71
  3. package/package.json +1 -2
  4. package/pyproject.toml +16 -1
  5. package/src/superlocalmemory/cli/commands.py +309 -0
  6. package/src/superlocalmemory/cli/main.py +44 -0
  7. package/src/superlocalmemory/core/config.py +282 -11
  8. package/src/superlocalmemory/core/consolidation_engine.py +37 -0
  9. package/src/superlocalmemory/core/engine.py +21 -0
  10. package/src/superlocalmemory/core/engine_wiring.py +58 -8
  11. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +374 -0
  12. package/src/superlocalmemory/dynamics/eap_scheduler.py +276 -0
  13. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +171 -0
  14. package/src/superlocalmemory/encoding/cognitive_consolidator.py +804 -0
  15. package/src/superlocalmemory/hooks/auto_invoker.py +46 -8
  16. package/src/superlocalmemory/hooks/auto_parameterize.py +147 -0
  17. package/src/superlocalmemory/infra/heartbeat_monitor.py +140 -0
  18. package/src/superlocalmemory/infra/pid_manager.py +193 -0
  19. package/src/superlocalmemory/infra/process_reaper.py +572 -0
  20. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +115 -0
  21. package/src/superlocalmemory/learning/forgetting_scheduler.py +263 -0
  22. package/src/superlocalmemory/learning/quantization_scheduler.py +320 -0
  23. package/src/superlocalmemory/math/ebbinghaus.py +309 -0
  24. package/src/superlocalmemory/math/fisher_quantized.py +251 -0
  25. package/src/superlocalmemory/math/hopfield.py +279 -0
  26. package/src/superlocalmemory/math/polar_quant.py +379 -0
  27. package/src/superlocalmemory/math/qjl.py +115 -0
  28. package/src/superlocalmemory/mcp/server.py +2 -0
  29. package/src/superlocalmemory/mcp/tools_v3.py +10 -0
  30. package/src/superlocalmemory/mcp/tools_v33.py +351 -0
  31. package/src/superlocalmemory/parameterization/__init__.py +47 -0
  32. package/src/superlocalmemory/parameterization/pattern_extractor.py +534 -0
  33. package/src/superlocalmemory/parameterization/pii_filter.py +106 -0
  34. package/src/superlocalmemory/parameterization/prompt_injector.py +216 -0
  35. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +275 -0
  36. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +425 -0
  37. package/src/superlocalmemory/retrieval/engine.py +21 -3
  38. package/src/superlocalmemory/retrieval/forgetting_filter.py +145 -0
  39. package/src/superlocalmemory/retrieval/hopfield_channel.py +335 -0
  40. package/src/superlocalmemory/retrieval/quantization_aware_search.py +133 -0
  41. package/src/superlocalmemory/retrieval/spreading_activation.py +1 -1
  42. package/src/superlocalmemory/retrieval/strategy.py +16 -6
  43. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  44. package/src/superlocalmemory/server/routes/agents.py +68 -8
  45. package/src/superlocalmemory/server/routes/learning.py +18 -1
  46. package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
  47. package/src/superlocalmemory/server/routes/v3_api.py +503 -1
  48. package/src/superlocalmemory/storage/database.py +206 -0
  49. package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
  50. package/src/superlocalmemory/storage/migration_v33.py +140 -0
  51. package/src/superlocalmemory/storage/quantized_store.py +261 -0
  52. package/src/superlocalmemory/storage/schema_v32.py +137 -0
  53. package/conftest.py +0 -5
@@ -0,0 +1,216 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3.3
4
+
5
+ """PromptInjector — Inject soft prompts into context with token budget.
6
+
7
+ Priority: soft prompts > regular memories (soft prompts come first).
8
+ Token budget split: prompt_budget (500 default) + memory_budget (1500 default).
9
+ Dedup with existing context.
10
+
11
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from superlocalmemory.storage.database import DatabaseManager
22
+ from superlocalmemory.core.config import ParameterizationConfig
23
+
24
+ from superlocalmemory.parameterization.soft_prompt_generator import (
25
+ SoftPromptGenerator,
26
+ SoftPromptTemplate,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class PromptInjector:
33
+ """Inject soft prompts into conversation context.
34
+
35
+ Handles retrieval from DB, token budget enforcement, and
36
+ combination with regular memory context.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ db: DatabaseManager,
42
+ generator: SoftPromptGenerator,
43
+ config: ParameterizationConfig,
44
+ ) -> None:
45
+ self._db = db
46
+ self._generator = generator
47
+ self._config = config
48
+
49
+ # ------------------------------------------------------------------
50
+ # Public API
51
+ # ------------------------------------------------------------------
52
+
53
+ def get_injection_context(self, profile_id: str) -> str:
54
+ """Retrieve stored soft prompts, assemble for injection.
55
+
56
+ Args:
57
+ profile_id: Profile to load prompts for.
58
+
59
+ Returns:
60
+ Assembled soft prompt text, or "" if disabled/empty.
61
+ """
62
+ if not self._config.enabled:
63
+ return ""
64
+
65
+ rows = self._db.execute(
66
+ "SELECT prompt_id, category, content, confidence, "
67
+ "token_count, retention_score, profile_id, "
68
+ "source_pattern_ids, effectiveness, active, version "
69
+ "FROM soft_prompt_templates "
70
+ "WHERE profile_id = ? AND active = 1 AND retention_score >= 0.1 "
71
+ "ORDER BY confidence DESC",
72
+ (profile_id,),
73
+ )
74
+
75
+ if not rows:
76
+ return ""
77
+
78
+ templates: list[SoftPromptTemplate] = []
79
+ for row in rows:
80
+ raw_ids = row.get("source_pattern_ids", "[]")
81
+ try:
82
+ source_ids = json.loads(raw_ids) if isinstance(raw_ids, str) else raw_ids
83
+ except (json.JSONDecodeError, TypeError):
84
+ source_ids = []
85
+
86
+ templates.append(SoftPromptTemplate(
87
+ prompt_id=row["prompt_id"],
88
+ profile_id=row.get("profile_id", profile_id),
89
+ category=row["category"],
90
+ content=row["content"],
91
+ source_pattern_ids=source_ids,
92
+ confidence=row["confidence"],
93
+ effectiveness=row.get("effectiveness", 0.5),
94
+ token_count=row.get("token_count", 0),
95
+ retention_score=row.get("retention_score", 1.0),
96
+ active=bool(row.get("active", 1)),
97
+ version=row.get("version", 1),
98
+ ))
99
+
100
+ # Budget enforcement
101
+ total = sum(t.token_count for t in templates)
102
+ if total > self._config.max_prompt_tokens:
103
+ selected: list[SoftPromptTemplate] = []
104
+ accumulated = 0
105
+ for t in templates: # Already sorted by confidence DESC
106
+ if accumulated + t.token_count > self._config.max_prompt_tokens:
107
+ break
108
+ selected.append(t)
109
+ accumulated += t.token_count
110
+ templates = selected
111
+
112
+ return self._generator.assemble(templates)
113
+
114
+ def inject_into_context(
115
+ self,
116
+ soft_prompt_text: str,
117
+ memory_context: str,
118
+ total_budget: int | None = None,
119
+ ) -> str:
120
+ """Combine soft prompts with regular memory context.
121
+
122
+ Args:
123
+ soft_prompt_text: Assembled soft prompt text.
124
+ memory_context: Regular memory recall context.
125
+ total_budget: Optional total token budget override.
126
+
127
+ Returns:
128
+ Combined context string with soft prompts first.
129
+ """
130
+ budget = total_budget or (
131
+ self._config.max_prompt_tokens + self._config.max_memory_tokens
132
+ )
133
+ prompt_budget = self._config.max_prompt_tokens
134
+ memory_budget = self._config.max_memory_tokens
135
+
136
+ # Trim soft prompt to its budget
137
+ if soft_prompt_text:
138
+ soft_tokens = SoftPromptGenerator._estimate_tokens(soft_prompt_text)
139
+ if soft_tokens > prompt_budget:
140
+ soft_prompt_text = SoftPromptGenerator._trim_to_tokens(
141
+ soft_prompt_text, prompt_budget,
142
+ )
143
+
144
+ # Trim memory context to its budget
145
+ if memory_context:
146
+ mem_tokens = SoftPromptGenerator._estimate_tokens(memory_context)
147
+ if mem_tokens > memory_budget:
148
+ memory_context = SoftPromptGenerator._trim_to_tokens(
149
+ memory_context, memory_budget,
150
+ )
151
+
152
+ if not soft_prompt_text:
153
+ return memory_context
154
+ if not memory_context:
155
+ return soft_prompt_text
156
+
157
+ return f"{soft_prompt_text}\n\n{memory_context}"
158
+
159
+ def store_prompts(
160
+ self, templates: list[SoftPromptTemplate],
161
+ ) -> int:
162
+ """Persist generated soft prompts, deactivating old versions.
163
+
164
+ Args:
165
+ templates: Generated templates to store.
166
+
167
+ Returns:
168
+ Number of templates stored.
169
+ """
170
+ count = 0
171
+ for template in templates:
172
+ # Deactivate existing for same (profile_id, category)
173
+ self._db.execute(
174
+ "UPDATE soft_prompt_templates SET active = 0, "
175
+ "updated_at = datetime('now') "
176
+ "WHERE profile_id = ? AND category = ? AND active = 1",
177
+ (template.profile_id, template.category),
178
+ )
179
+
180
+ # Get max version for this (profile_id, category)
181
+ version_rows = self._db.execute(
182
+ "SELECT COALESCE(MAX(version), 0) AS max_version "
183
+ "FROM soft_prompt_templates "
184
+ "WHERE profile_id = ? AND category = ?",
185
+ (template.profile_id, template.category),
186
+ )
187
+ max_version = 0
188
+ if version_rows:
189
+ max_version = version_rows[0].get("max_version", 0) or 0
190
+ new_version = max_version + 1
191
+
192
+ # Insert new prompt
193
+ self._db.execute(
194
+ "INSERT INTO soft_prompt_templates "
195
+ "(prompt_id, profile_id, category, content, "
196
+ "source_pattern_ids, confidence, effectiveness, "
197
+ "token_count, retention_score, active, version, "
198
+ "created_at, updated_at) "
199
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, "
200
+ "datetime('now'), datetime('now'))",
201
+ (
202
+ template.prompt_id,
203
+ template.profile_id,
204
+ template.category,
205
+ template.content,
206
+ json.dumps(template.source_pattern_ids),
207
+ template.confidence,
208
+ template.effectiveness,
209
+ template.token_count,
210
+ template.retention_score,
211
+ new_version,
212
+ ),
213
+ )
214
+ count += 1
215
+
216
+ return count
@@ -0,0 +1,275 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3.3
4
+
5
+ """PromptLifecycleManager — Ebbinghaus decay and effectiveness for soft prompts.
6
+
7
+ Applies Ebbinghaus forgetting curve to soft prompts. Higher effectiveness
8
+ and more versions (evidence proxy) slow decay. 48h floor prevents cold-start death.
9
+
10
+ [AUDIT FIX F-2] 48h floor on prompt strength.
11
+
12
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import math
19
+ from datetime import datetime, timezone
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from superlocalmemory.storage.database import DatabaseManager
24
+ from superlocalmemory.math.ebbinghaus import EbbinghausCurve
25
+ from superlocalmemory.core.config import ParameterizationConfig
26
+
27
+ from superlocalmemory.parameterization.pattern_extractor import (
28
+ PatternAssertion,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Minimum prompt strength floor — prevents cold-start death
34
+ _PROMPT_STRENGTH_FLOOR = 48.0
35
+
36
+
37
+ class PromptLifecycleManager:
38
+ """Manage soft prompt lifecycle: effectiveness tracking + Ebbinghaus decay.
39
+
40
+ Effectiveness is updated via session feedback signals.
41
+ Retention decays via Ebbinghaus curve with strength derived from
42
+ effectiveness * version (evidence proxy).
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ db: DatabaseManager,
48
+ ebbinghaus: EbbinghausCurve,
49
+ config: ParameterizationConfig,
50
+ ) -> None:
51
+ self._db = db
52
+ self._ebbinghaus = ebbinghaus
53
+ self._config = config
54
+
55
+ # ------------------------------------------------------------------
56
+ # Effectiveness tracking
57
+ # ------------------------------------------------------------------
58
+
59
+ def update_effectiveness(
60
+ self,
61
+ profile_id: str,
62
+ category: str,
63
+ signals: dict[str, float],
64
+ ) -> float:
65
+ """Update prompt effectiveness from feedback signals.
66
+
67
+ Signals: followed, session_success (positive),
68
+ corrected, session_failure (negative).
69
+ Uses exponential moving average: 0.7 * raw + 0.3 * current.
70
+
71
+ Args:
72
+ profile_id: Target profile.
73
+ category: Prompt category to update.
74
+ signals: Feedback signal dict.
75
+
76
+ Returns:
77
+ New effectiveness score in [0.0, 1.0].
78
+ """
79
+ rows = self._db.execute(
80
+ "SELECT prompt_id, effectiveness, retention_score "
81
+ "FROM soft_prompt_templates "
82
+ "WHERE profile_id = ? AND category = ? AND active = 1",
83
+ (profile_id, category),
84
+ )
85
+ if not rows:
86
+ return 0.5
87
+
88
+ row = rows[0]
89
+ current_effectiveness = row.get("effectiveness", 0.5)
90
+ prompt_id = row["prompt_id"]
91
+
92
+ # Compute signal summary
93
+ positive = (
94
+ signals.get("followed", 0.0)
95
+ + signals.get("session_success", 0.0)
96
+ )
97
+ negative = (
98
+ signals.get("corrected", 0.0)
99
+ + signals.get("session_failure", 0.0)
100
+ )
101
+ total = positive + negative + 1.0 # Laplace smoothing
102
+
103
+ raw_score = positive / total
104
+ new_effectiveness = 0.7 * raw_score + 0.3 * current_effectiveness
105
+ new_effectiveness = max(0.0, min(1.0, new_effectiveness))
106
+
107
+ self._db.execute(
108
+ "UPDATE soft_prompt_templates "
109
+ "SET effectiveness = ?, updated_at = datetime('now') "
110
+ "WHERE prompt_id = ?",
111
+ (new_effectiveness, prompt_id),
112
+ )
113
+
114
+ return new_effectiveness
115
+
116
+ # ------------------------------------------------------------------
117
+ # Ebbinghaus decay
118
+ # ------------------------------------------------------------------
119
+
120
+ def compute_prompt_retention(self, prompt_id: str) -> float:
121
+ """Compute Ebbinghaus retention for a soft prompt.
122
+
123
+ Strength = max(48h floor, 2 * effectiveness * version).
124
+ R(t) = e^(-t/S) via EbbinghausCurve.
125
+
126
+ Args:
127
+ prompt_id: Prompt to compute retention for.
128
+
129
+ Returns:
130
+ Retention score in [0.0, 1.0], or 0.0 if not found.
131
+ """
132
+ rows = self._db.execute(
133
+ "SELECT prompt_id, effectiveness, retention_score, "
134
+ "version, created_at, updated_at "
135
+ "FROM soft_prompt_templates WHERE prompt_id = ?",
136
+ (prompt_id,),
137
+ )
138
+ if not rows:
139
+ return 0.0
140
+
141
+ row = rows[0]
142
+ effectiveness = row.get("effectiveness", 0.5)
143
+ version = max(1, row.get("version", 1))
144
+ updated_at = row.get("updated_at", "")
145
+
146
+ # Compute hours since last update
147
+ now = datetime.now(timezone.utc)
148
+ try:
149
+ last_update = datetime.fromisoformat(updated_at)
150
+ if last_update.tzinfo is None:
151
+ last_update = last_update.replace(tzinfo=timezone.utc)
152
+ hours = (now - last_update).total_seconds() / 3600.0
153
+ except (ValueError, TypeError):
154
+ hours = 0.0
155
+
156
+ # Prompt strength: effectiveness * version proxy
157
+ s_raw = 2.0 * effectiveness * version
158
+ s_prompt = max(_PROMPT_STRENGTH_FLOOR, s_raw)
159
+
160
+ retention = self._ebbinghaus.retention(hours, s_prompt)
161
+ return retention
162
+
163
+ # ------------------------------------------------------------------
164
+ # Lifecycle review
165
+ # ------------------------------------------------------------------
166
+
167
+ def run_lifecycle_review(self, profile_id: str) -> dict:
168
+ """Periodic review: decay prompts, deactivate dead ones.
169
+
170
+ Args:
171
+ profile_id: Profile to review.
172
+
173
+ Returns:
174
+ Stats dict: {reviewed, decayed, removed, refreshed}.
175
+ """
176
+ rows = self._db.execute(
177
+ "SELECT prompt_id, category, effectiveness, retention_score, "
178
+ "version, created_at, updated_at "
179
+ "FROM soft_prompt_templates "
180
+ "WHERE profile_id = ? AND active = 1",
181
+ (profile_id,),
182
+ )
183
+
184
+ stats = {"reviewed": 0, "decayed": 0, "removed": 0, "refreshed": 0}
185
+
186
+ for row in rows:
187
+ prompt_id = row["prompt_id"]
188
+ old_retention = row.get("retention_score", 1.0)
189
+ stats["reviewed"] += 1
190
+
191
+ new_retention = self.compute_prompt_retention(prompt_id)
192
+
193
+ if new_retention < 0.1:
194
+ # Deactivate
195
+ self._db.execute(
196
+ "UPDATE soft_prompt_templates "
197
+ "SET active = 0, retention_score = ?, "
198
+ "updated_at = datetime('now') WHERE prompt_id = ?",
199
+ (new_retention, prompt_id),
200
+ )
201
+ stats["removed"] += 1
202
+ logger.info(
203
+ "Prompt %s (category=%s) deactivated: retention %.3f < 0.1",
204
+ prompt_id, row.get("category", "?"), new_retention,
205
+ )
206
+ else:
207
+ # Update retention score
208
+ self._db.execute(
209
+ "UPDATE soft_prompt_templates "
210
+ "SET retention_score = ?, updated_at = datetime('now') "
211
+ "WHERE prompt_id = ?",
212
+ (new_retention, prompt_id),
213
+ )
214
+ delta = abs(new_retention - old_retention)
215
+ if delta > 0.1:
216
+ stats["decayed"] += 1
217
+
218
+ return stats
219
+
220
+ # ------------------------------------------------------------------
221
+ # Evolution
222
+ # ------------------------------------------------------------------
223
+
224
+ def evolve_prompt(
225
+ self,
226
+ profile_id: str,
227
+ category: str,
228
+ new_pattern: PatternAssertion,
229
+ ) -> str:
230
+ """Handle preference evolution: new pattern vs existing prompt.
231
+
232
+ Args:
233
+ profile_id: Target profile.
234
+ category: Category being evolved.
235
+ new_pattern: New pattern assertion to compare.
236
+
237
+ Returns:
238
+ "new" | "replaced" | "kept_existing" | "user_review_needed"
239
+ """
240
+ rows = self._db.execute(
241
+ "SELECT prompt_id, category, confidence, effectiveness "
242
+ "FROM soft_prompt_templates "
243
+ "WHERE profile_id = ? AND category = ? AND active = 1",
244
+ (profile_id, category),
245
+ )
246
+
247
+ if not rows:
248
+ return "new"
249
+
250
+ current = rows[0]
251
+ current_confidence = current.get("confidence", 0.0)
252
+
253
+ if new_pattern.confidence > current_confidence + 0.1:
254
+ # New pattern clearly better — replace
255
+ self._db.execute(
256
+ "UPDATE soft_prompt_templates "
257
+ "SET active = 0, updated_at = datetime('now') "
258
+ "WHERE prompt_id = ?",
259
+ (current["prompt_id"],),
260
+ )
261
+ return "replaced"
262
+
263
+ if new_pattern.confidence < current_confidence - 0.1:
264
+ logger.info(
265
+ "Keeping existing prompt for %s (conf=%.2f > new=%.2f)",
266
+ category, current_confidence, new_pattern.confidence,
267
+ )
268
+ return "kept_existing"
269
+
270
+ # Close confidence — needs user review
271
+ logger.warning(
272
+ "Equal confidence conflict in %s for %s. User review needed.",
273
+ category, new_pattern.key,
274
+ )
275
+ return "user_review_needed"