superlocalmemory 3.8.0 → 3.8.2

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 (134) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +2 -2
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +3 -5
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +2 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +3 -5
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +2 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +494 -9
  35. package/src/superlocalmemory/cli/daemon.py +7 -0
  36. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  37. package/src/superlocalmemory/cli/main.py +72 -7
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  39. package/src/superlocalmemory/cli/version_banner.py +17 -3
  40. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  41. package/src/superlocalmemory/core/component_healer.py +144 -0
  42. package/src/superlocalmemory/core/component_registry.py +487 -0
  43. package/src/superlocalmemory/core/config.py +21 -0
  44. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  45. package/src/superlocalmemory/core/embeddings.py +132 -45
  46. package/src/superlocalmemory/core/engine.py +29 -22
  47. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  48. package/src/superlocalmemory/core/ingestion_command.py +154 -25
  49. package/src/superlocalmemory/core/injection.py +12 -7
  50. package/src/superlocalmemory/core/maintenance.py +43 -0
  51. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
  52. package/src/superlocalmemory/core/recall_pipeline.py +42 -4
  53. package/src/superlocalmemory/core/store_pipeline.py +195 -20
  54. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  55. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  56. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  57. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  58. package/src/superlocalmemory/learning/reward.py +50 -0
  59. package/src/superlocalmemory/learning/source_quality.py +523 -1
  60. package/src/superlocalmemory/loops/ledger.py +25 -5
  61. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  62. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  63. package/src/superlocalmemory/mcp/server.py +11 -30
  64. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  65. package/src/superlocalmemory/mcp/tools_core.py +21 -5
  66. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  67. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  68. package/src/superlocalmemory/retrieval/engine.py +53 -21
  69. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  70. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  71. package/src/superlocalmemory/server/config_file.py +90 -0
  72. package/src/superlocalmemory/server/origin.py +50 -0
  73. package/src/superlocalmemory/server/routes/backup.py +293 -70
  74. package/src/superlocalmemory/server/routes/behavioral.py +342 -61
  75. package/src/superlocalmemory/server/routes/brain.py +57 -16
  76. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  77. package/src/superlocalmemory/server/routes/entity.py +100 -23
  78. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  79. package/src/superlocalmemory/server/routes/learning.py +286 -105
  80. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  81. package/src/superlocalmemory/server/routes/memories.py +8 -3
  82. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  83. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  84. package/src/superlocalmemory/server/routes/stats.py +93 -155
  85. package/src/superlocalmemory/server/routes/token.py +3 -13
  86. package/src/superlocalmemory/server/routes/v3_api.py +184 -20
  87. package/src/superlocalmemory/server/unified_daemon.py +732 -41
  88. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  89. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  90. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  91. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  92. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  93. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  94. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  95. package/src/superlocalmemory/storage/schema.py +49 -1
  96. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  97. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  98. package/src/superlocalmemory/ui/index.html +6 -8
  99. package/src/superlocalmemory/ui/js/core.js +52 -9
  100. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  101. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  102. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  103. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  104. package/src/superlocalmemory/ui/js/od-entities.js +65 -22
  105. package/src/superlocalmemory/ui/js/od-graph.js +46 -4
  106. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  107. package/src/superlocalmemory/ui/js/od-memories.js +84 -5
  108. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  109. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  110. package/src/superlocalmemory/ui/js/od-settings.js +186 -63
  111. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  112. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  113. package/src/superlocalmemory/ui/js/settings.js +15 -1
  114. package/plugin-src/.mcp.json +0 -12
  115. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  116. package/plugin-src/agents/slm-loop-runner.md +0 -71
  117. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  118. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  119. package/plugin-src/commands/slm-loop.md +0 -31
  120. package/plugin-src/hooks/.gitkeep +0 -0
  121. package/plugin-src/hooks/hooks.json +0 -102
  122. package/plugin-src/manifest.json +0 -30
  123. package/plugin-src/requirements.txt +0 -1
  124. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  125. package/plugin-src/scripts/ensure-venv.bat +0 -122
  126. package/plugin-src/scripts/ensure-venv.sh +0 -105
  127. package/plugin-src/scripts/slm-launch +0 -62
  128. package/plugin-src/scripts/slm-launch.bat +0 -23
  129. package/plugin-src/settings.json +0 -25
  130. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  131. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  132. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  133. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  134. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -4,21 +4,78 @@
4
4
  """SuperLocalMemory V3 - Stats Routes
5
5
  - AGPL-3.0-or-later
6
6
 
7
- Routes: /api/stats, /api/timeline, /api/patterns
7
+ Routes: /api/stats, /api/timeline
8
8
  """
9
- import json
10
9
  import logging
11
- from collections import defaultdict
10
+ import sqlite3
12
11
  from typing import Optional
13
12
 
14
13
  from fastapi import APIRouter, HTTPException, Query
15
14
 
16
- from .helpers import get_db_connection, dict_factory, get_active_profile, DB_PATH, MEMORY_DIR
15
+ from .helpers import get_db_connection, dict_factory, get_active_profile, DB_PATH
17
16
 
18
17
  logger = logging.getLogger("superlocalmemory.routes.stats")
19
18
  router = APIRouter()
20
19
 
21
20
 
21
+ def _query_ingestion_sources(cursor, profile_id: str) -> list[dict]:
22
+ """Query profile-scoped ingestion provenance without double-counting facts."""
23
+ cursor.execute(
24
+ """
25
+ SELECT COALESCE(NULLIF(TRIM(source_type), ''), 'unknown') AS source_type,
26
+ COUNT(DISTINCT fact_id) AS count
27
+ FROM provenance
28
+ WHERE profile_id = ?
29
+ GROUP BY COALESCE(NULLIF(TRIM(source_type), ''), 'unknown')
30
+ ORDER BY count DESC, source_type ASC
31
+ """,
32
+ (profile_id,),
33
+ )
34
+ return [dict(row) for row in cursor.fetchall()]
35
+
36
+
37
+ def _load_ingestion_sources_with_status(
38
+ cursor, profile_id: str,
39
+ ) -> tuple[list[dict], dict]:
40
+ """Return provenance plus a truthful availability state.
41
+
42
+ Older databases may not have the provenance table. In that case the
43
+ dashboard must report an empty/unknown state rather than inventing a split.
44
+ Lock or I/O failures are distinct from a legitimate legacy-empty state.
45
+ """
46
+ try:
47
+ return _query_ingestion_sources(cursor, profile_id), {
48
+ "available": True,
49
+ "state": "recorded",
50
+ "source": "memory.db:provenance",
51
+ }
52
+ except sqlite3.OperationalError as exc:
53
+ if "no such table" in str(exc).lower():
54
+ return [], {
55
+ "available": True,
56
+ "state": "legacy_not_recorded",
57
+ "source": "memory.db:provenance",
58
+ }
59
+ logger.warning("provenance metrics temporarily unavailable: %s", exc)
60
+ return [], {
61
+ "available": False,
62
+ "state": "temporarily_unavailable",
63
+ "source": "memory.db:provenance",
64
+ }
65
+ except sqlite3.Error as exc:
66
+ logger.warning("provenance metrics unavailable: %s", exc)
67
+ return [], {
68
+ "available": False,
69
+ "state": "temporarily_unavailable",
70
+ "source": "memory.db:provenance",
71
+ }
72
+
73
+
74
+ def _load_ingestion_sources(cursor, profile_id: str) -> list[dict]:
75
+ """Compatibility helper returning only the provenance rows."""
76
+ return _load_ingestion_sources_with_status(cursor, profile_id)[0]
77
+
78
+
22
79
  def _internal_error(detail: str = "Internal server error") -> HTTPException:
23
80
  """SEC-H-02: log full traceback server-side; return a generic message to the client."""
24
81
  logger.exception("stats route error")
@@ -26,7 +83,7 @@ def _internal_error(detail: str = "Internal server error") -> HTTPException:
26
83
 
27
84
 
28
85
  @router.get("/api/stats")
29
- async def get_stats():
86
+ def get_stats():
30
87
  """Get comprehensive system statistics."""
31
88
  try:
32
89
  conn = get_db_connection()
@@ -205,6 +262,9 @@ async def get_stats():
205
262
  importance_dist = cursor.fetchall()
206
263
 
207
264
  db_size = DB_PATH.stat().st_size if DB_PATH.exists() else 0
265
+ ingestion_sources, ingestion_sources_status = (
266
+ _load_ingestion_sources_with_status(cursor, active_profile)
267
+ )
208
268
 
209
269
  if total_graph_nodes > 1:
210
270
  max_edges = (total_graph_nodes * (total_graph_nodes - 1)) / 2
@@ -241,6 +301,8 @@ async def get_stats():
241
301
  if total_graph_nodes > 0 else 0
242
302
  ),
243
303
  },
304
+ "ingestion_sources": ingestion_sources,
305
+ "ingestion_sources_status": ingestion_sources_status,
244
306
  }
245
307
 
246
308
  except Exception:
@@ -248,11 +310,12 @@ async def get_stats():
248
310
 
249
311
 
250
312
  @router.get("/api/timeline")
251
- async def get_timeline(
313
+ def get_timeline(
252
314
  days: int = Query(30, ge=1, le=365),
253
315
  group_by: str = Query("day", pattern="^(day|week|month)$"),
316
+ include_categories: bool = Query(True),
254
317
  ):
255
- """Get temporal view of memory creation with flexible grouping."""
318
+ """Get temporal memory creation; dashboard callers may skip category scans."""
256
319
  try:
257
320
  conn = get_db_connection()
258
321
  conn.row_factory = dict_factory
@@ -286,22 +349,29 @@ async def get_timeline(
286
349
  """, (days, active_profile))
287
350
  timeline = cursor.fetchall()
288
351
 
289
- cursor.execute(f"""
290
- SELECT {date_group} as period, {cat_col} as category, COUNT(*) as count
291
- FROM {table}
292
- WHERE created_at >= datetime('now', '-' || ? || ' days')
293
- AND {cat_col} IS NOT NULL AND {profile_col} = ?
294
- GROUP BY {date_group}, {cat_col} ORDER BY period DESC, count DESC
295
- """, (days, active_profile))
296
- category_trend = cursor.fetchall()
297
-
298
- cursor.execute(f"""
299
- SELECT COUNT(*) as total_memories,
300
- COUNT(DISTINCT {cat_col}) as categories_used
301
- FROM {table}
302
- WHERE created_at >= datetime('now', '-' || ? || ' days') AND {profile_col} = ?
303
- """, (days, active_profile))
304
- period_stats = cursor.fetchone()
352
+ if include_categories:
353
+ cursor.execute(f"""
354
+ SELECT {date_group} as period, {cat_col} as category, COUNT(*) as count
355
+ FROM {table}
356
+ WHERE created_at >= datetime('now', '-' || ? || ' days')
357
+ AND {cat_col} IS NOT NULL AND {profile_col} = ?
358
+ GROUP BY {date_group}, {cat_col} ORDER BY period DESC, count DESC
359
+ """, (days, active_profile))
360
+ category_trend = cursor.fetchall()
361
+
362
+ cursor.execute(f"""
363
+ SELECT COUNT(*) as total_memories,
364
+ COUNT(DISTINCT {cat_col}) as categories_used
365
+ FROM {table}
366
+ WHERE created_at >= datetime('now', '-' || ? || ' days') AND {profile_col} = ?
367
+ """, (days, active_profile))
368
+ period_stats = cursor.fetchone()
369
+ else:
370
+ category_trend = []
371
+ period_stats = {
372
+ "total_memories": sum(int(row["count"] or 0) for row in timeline),
373
+ "categories_used": None,
374
+ }
305
375
 
306
376
  conn.close()
307
377
 
@@ -313,135 +383,3 @@ async def get_timeline(
313
383
 
314
384
  except Exception:
315
385
  raise _internal_error("Timeline error")
316
-
317
-
318
- @router.get("/api/patterns")
319
- async def get_patterns():
320
- """Get learned patterns."""
321
- try:
322
- conn = get_db_connection()
323
- conn.row_factory = dict_factory
324
- cursor = conn.cursor()
325
- active_profile = get_active_profile()
326
-
327
- # Check for V3 learning tables or V2 identity_patterns
328
- patterns = []
329
- table_name = None
330
- for candidate in ('learned_patterns', 'identity_patterns'):
331
- cursor.execute(
332
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
333
- (candidate,),
334
- )
335
- if cursor.fetchone():
336
- table_name = candidate
337
- break
338
-
339
- if not table_name:
340
- conn.close()
341
- # Fall through to V3.1 behavioral pattern store
342
- try:
343
- from superlocalmemory.learning.behavioral import BehavioralPatternStore
344
- store = BehavioralPatternStore(str(MEMORY_DIR / "learning.db"))
345
- raw = store.get_patterns(profile_id=active_profile)
346
- # v3.4.7: Map all pattern types to frontend categories
347
- type_map = {
348
- "tech_preference": "preference",
349
- "interest": "preference",
350
- "entity_preferences": "preference",
351
- "style": "style",
352
- "fact_type_distribution": "style",
353
- "knowledge_structure": "style",
354
- "terminology": "terminology",
355
- "temporal": "workflow",
356
- "session_activity": "workflow",
357
- "workflow": "workflow",
358
- "co_retrieval_clusters": "workflow",
359
- "channel_performance": "performance",
360
- }
361
- grouped = defaultdict(list)
362
- for p in raw:
363
- meta = p.get("metadata", {})
364
- data = meta # metadata IS the data dict
365
- frontend_key = type_map.get(p.get("pattern_type", ""), "preference")
366
- # Extract human-readable value from data fields
367
- readable_value = (
368
- data.get("value")
369
- or data.get("topic")
370
- or data.get("pattern_key", "")
371
- or p.get("pattern_key", "")
372
- )
373
- readable_key = (
374
- data.get("pattern_key")
375
- or data.get("key")
376
- or p.get("pattern_key", "")
377
- )
378
- grouped[frontend_key].append({
379
- "pattern_type": p.get("pattern_type", ""),
380
- "key": readable_key,
381
- "value": readable_value,
382
- "confidence": p.get("confidence", 0),
383
- "evidence_count": data.get("evidence", p.get("evidence_count", 0)),
384
- })
385
- all_patterns = [p for ps in grouped.values() for p in ps]
386
- confs = [p["confidence"] for p in all_patterns if p.get("confidence")]
387
- return {
388
- "patterns": dict(grouped),
389
- "total_patterns": len(all_patterns),
390
- "pattern_types": list(grouped.keys()),
391
- "confidence_stats": {
392
- "avg": sum(confs) / len(confs) if confs else 0,
393
- "min": min(confs) if confs else 0,
394
- "max": max(confs) if confs else 0,
395
- },
396
- }
397
- except Exception:
398
- return {
399
- "patterns": {}, "total_patterns": 0, "pattern_types": [],
400
- "message": "Pattern learning not initialized.",
401
- }
402
-
403
- if table_name == 'identity_patterns':
404
- cursor.execute("""
405
- SELECT pattern_type, key, value, confidence, evidence_count,
406
- updated_at as last_updated
407
- FROM identity_patterns WHERE profile = ?
408
- ORDER BY confidence DESC, evidence_count DESC
409
- """, (active_profile,))
410
- else:
411
- cursor.execute("""
412
- SELECT pattern_type, key, value, confidence, evidence_count,
413
- last_updated
414
- FROM learned_patterns WHERE is_active = 1
415
- ORDER BY confidence DESC, evidence_count DESC
416
- """)
417
-
418
- patterns = cursor.fetchall()
419
-
420
- for pattern in patterns:
421
- if pattern.get('value'):
422
- try:
423
- pattern['value'] = json.loads(pattern['value'])
424
- except Exception:
425
- pass
426
-
427
- grouped = defaultdict(list)
428
- for pattern in patterns:
429
- grouped[pattern['pattern_type']].append(pattern)
430
-
431
- confidences = [p['confidence'] for p in patterns if p.get('confidence')]
432
- confidence_stats = {
433
- "avg": sum(confidences) / len(confidences) if confidences else 0,
434
- "min": min(confidences) if confidences else 0,
435
- "max": max(confidences) if confidences else 0,
436
- }
437
-
438
- conn.close()
439
-
440
- return {
441
- "patterns": dict(grouped), "total_patterns": len(patterns),
442
- "pattern_types": list(grouped.keys()), "confidence_stats": confidence_stats,
443
- }
444
-
445
- except Exception:
446
- logger.exception("patterns route error")
447
- return {"patterns": {}, "total_patterns": 0, "error": "Internal server error"}
@@ -31,21 +31,11 @@ logger = logging.getLogger(__name__)
31
31
  router = APIRouter(tags=["internal"])
32
32
 
33
33
 
34
- _ALLOWED_ORIGIN_PREFIXES = (
35
- "http://127.0.0.1",
36
- "https://127.0.0.1",
37
- "http://localhost",
38
- "https://localhost",
39
- "http://[::1]",
40
- "https://[::1]",
41
- )
42
-
43
-
44
34
  def _origin_is_loopback(origin: str) -> bool:
45
35
  """Return True iff ``origin`` is absent or a loopback URL."""
46
- if not origin:
47
- return True
48
- return any(origin.startswith(p) for p in _ALLOWED_ORIGIN_PREFIXES)
36
+ from superlocalmemory.server.origin import origin_is_loopback
37
+
38
+ return origin_is_loopback(origin)
49
39
 
50
40
 
51
41
  @router.get("/internal/token")