superlocalmemory 3.8.0 → 3.8.1

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 (118) hide show
  1. package/CHANGELOG.md +60 -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 +1 -1
  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 +1 -1
  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 +1 -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 +1 -1
  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/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/cli/commands.py +134 -7
  34. package/src/superlocalmemory/cli/daemon.py +7 -0
  35. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  36. package/src/superlocalmemory/cli/main.py +10 -4
  37. package/src/superlocalmemory/cli/version_banner.py +17 -3
  38. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  39. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  40. package/src/superlocalmemory/core/embeddings.py +119 -45
  41. package/src/superlocalmemory/core/engine.py +24 -21
  42. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  43. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  44. package/src/superlocalmemory/core/injection.py +12 -7
  45. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  46. package/src/superlocalmemory/core/recall_pipeline.py +5 -3
  47. package/src/superlocalmemory/core/store_pipeline.py +153 -20
  48. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  49. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  50. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  51. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  52. package/src/superlocalmemory/learning/reward.py +50 -0
  53. package/src/superlocalmemory/learning/source_quality.py +523 -1
  54. package/src/superlocalmemory/loops/ledger.py +25 -5
  55. package/src/superlocalmemory/mcp/server.py +11 -30
  56. package/src/superlocalmemory/mcp/tools_core.py +4 -3
  57. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  58. package/src/superlocalmemory/retrieval/engine.py +53 -21
  59. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  60. package/src/superlocalmemory/server/config_file.py +90 -0
  61. package/src/superlocalmemory/server/origin.py +50 -0
  62. package/src/superlocalmemory/server/routes/backup.py +293 -70
  63. package/src/superlocalmemory/server/routes/behavioral.py +336 -59
  64. package/src/superlocalmemory/server/routes/brain.py +57 -16
  65. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  66. package/src/superlocalmemory/server/routes/entity.py +100 -23
  67. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  68. package/src/superlocalmemory/server/routes/learning.py +276 -105
  69. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  70. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  71. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  72. package/src/superlocalmemory/server/routes/stats.py +93 -155
  73. package/src/superlocalmemory/server/routes/token.py +3 -13
  74. package/src/superlocalmemory/server/routes/v3_api.py +64 -20
  75. package/src/superlocalmemory/server/unified_daemon.py +467 -40
  76. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  77. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  78. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  79. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  80. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  81. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  82. package/src/superlocalmemory/storage/schema.py +49 -1
  83. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  84. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  85. package/src/superlocalmemory/ui/index.html +3 -6
  86. package/src/superlocalmemory/ui/js/core.js +52 -9
  87. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  88. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  89. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  90. package/src/superlocalmemory/ui/js/od-entities.js +22 -22
  91. package/src/superlocalmemory/ui/js/od-graph.js +11 -4
  92. package/src/superlocalmemory/ui/js/od-memories.js +47 -5
  93. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  94. package/src/superlocalmemory/ui/js/od-settings.js +113 -59
  95. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  96. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  97. package/src/superlocalmemory/ui/js/settings.js +15 -1
  98. package/plugin-src/.mcp.json +0 -12
  99. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  100. package/plugin-src/agents/slm-loop-runner.md +0 -71
  101. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  102. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  103. package/plugin-src/commands/slm-loop.md +0 -31
  104. package/plugin-src/hooks/.gitkeep +0 -0
  105. package/plugin-src/hooks/hooks.json +0 -102
  106. package/plugin-src/manifest.json +0 -30
  107. package/plugin-src/requirements.txt +0 -1
  108. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  109. package/plugin-src/scripts/ensure-venv.bat +0 -122
  110. package/plugin-src/scripts/ensure-venv.sh +0 -105
  111. package/plugin-src/scripts/slm-launch +0 -62
  112. package/plugin-src/scripts/slm-launch.bat +0 -23
  113. package/plugin-src/settings.json +0 -25
  114. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  115. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  116. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  117. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  118. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -7,8 +7,8 @@
7
7
  All PyTorch/model work runs in a SEPARATE subprocess. The main process
8
8
  (dashboard, MCP, CLI) never imports torch and stays at ~60 MB.
9
9
 
10
- The worker subprocess auto-kills after 2 minutes idle, returning all
11
- memory to the OS. It respawns on next embed call (~3 sec cold start).
10
+ The worker subprocess has a configurable idle timeout and respawns on the
11
+ next embed call when it has been unloaded.
12
12
 
13
13
  Part of Qualixar | Author: Varun Pratap Bhardwaj
14
14
  """
@@ -29,14 +29,14 @@ from typing import TYPE_CHECKING
29
29
 
30
30
  import numpy as np
31
31
 
32
+ from superlocalmemory.core.config import EmbeddingConfig
33
+
32
34
  # Track all live embedding services for atexit cleanup
33
35
  _live_embedding_services: set[weakref.ref] = set()
34
36
 
35
37
  if TYPE_CHECKING:
36
38
  from numpy.typing import NDArray
37
39
 
38
- from superlocalmemory.core.config import EmbeddingConfig
39
-
40
40
  logger = logging.getLogger(__name__)
41
41
 
42
42
  # Fisher variance constants
@@ -63,6 +63,7 @@ class DimensionMismatchError(RuntimeError):
63
63
 
64
64
  _MAX_CONCURRENT_WORKERS = int(os.environ.get("SLM_MAX_EMBEDDING_WORKERS", 1))
65
65
  _embedding_lock_fd: int | None = None
66
+ _embedding_lock_state_guard = threading.Lock()
66
67
 
67
68
 
68
69
  def _embedding_lock_file() -> Path:
@@ -106,57 +107,87 @@ def register_embedding_worker_pid(pid: int) -> None:
106
107
  def acquire_embedding_lock(timeout: float = 5.0) -> bool:
107
108
  """Acquire system-wide embedding worker lock.
108
109
 
109
- v3.4.13: First checks if a worker PID is already alive (fast path).
110
- Falls back to fcntl.flock on Unix. On Windows, falls back to PID check only.
110
+ The caller must re-check the PID file after acquisition before spawning.
111
+ POSIX uses flock; Windows uses a one-byte msvcrt lock.
111
112
  Returns True if lock acquired (safe to spawn), False if another worker active.
112
113
  """
113
114
  global _embedding_lock_fd
114
115
 
115
- # v3.4.13: Fast path if a worker PID is alive, don't even try the lock
116
- if _is_embedding_worker_alive():
117
- return False
118
-
119
- if sys.platform == "win32":
120
- return True # No file locking on Windows — PID check above is the guard
121
-
122
- import fcntl
123
- lock_file = _embedding_lock_file()
124
- lock_file.parent.mkdir(parents=True, exist_ok=True)
116
+ # Serialize local contenders as well as cross-process contenders. The file
117
+ # descriptor stays local until its OS lock succeeds, so a failed acquire
118
+ # can never overwrite and leak the descriptor that owns the live worker.
119
+ with _embedding_lock_state_guard:
120
+ if _embedding_lock_fd is not None:
121
+ return False
122
+ if _is_embedding_worker_alive():
123
+ return False
125
124
 
126
- try:
127
- _embedding_lock_fd = os.open(str(lock_file), os.O_CREAT | os.O_RDWR)
128
- deadline = time.time() + timeout
129
- while time.time() < deadline:
130
- try:
131
- fcntl.flock(_embedding_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
132
- return True
133
- except (BlockingIOError, OSError):
134
- time.sleep(0.2)
135
- # Timeout — another worker holds the lock
136
- os.close(_embedding_lock_fd)
137
- _embedding_lock_fd = None
138
- return False
139
- except Exception:
140
- return True # On error, allow through (don't block functionality)
125
+ lock_file = _embedding_lock_file()
126
+ lock_file.parent.mkdir(parents=True, exist_ok=True)
127
+ candidate_fd: int | None = None
128
+ try:
129
+ candidate_fd = os.open(str(lock_file), os.O_CREAT | os.O_RDWR)
130
+ deadline = time.time() + timeout
131
+ while time.time() < deadline:
132
+ try:
133
+ if sys.platform == "win32":
134
+ import msvcrt
135
+
136
+ if os.fstat(candidate_fd).st_size == 0:
137
+ os.write(candidate_fd, b"\0")
138
+ os.lseek(candidate_fd, 0, os.SEEK_SET)
139
+ msvcrt.locking(candidate_fd, msvcrt.LK_NBLCK, 1)
140
+ else:
141
+ import fcntl
142
+
143
+ fcntl.flock(
144
+ candidate_fd,
145
+ fcntl.LOCK_EX | fcntl.LOCK_NB,
146
+ )
147
+ _embedding_lock_fd = candidate_fd
148
+ return True
149
+ except (BlockingIOError, OSError):
150
+ time.sleep(0.2)
151
+ os.close(candidate_fd)
152
+ return False
153
+ except Exception:
154
+ if candidate_fd is not None:
155
+ try:
156
+ os.close(candidate_fd)
157
+ except OSError:
158
+ pass
159
+ return False
141
160
 
142
161
 
143
162
  def release_embedding_lock() -> None:
144
163
  """Release system-wide embedding worker lock."""
145
164
  global _embedding_lock_fd
146
- if _embedding_lock_fd is not None:
165
+ with _embedding_lock_state_guard:
166
+ if _embedding_lock_fd is None:
167
+ return
147
168
  try:
148
- import fcntl
149
- fcntl.flock(_embedding_lock_fd, fcntl.LOCK_UN)
169
+ if sys.platform == "win32":
170
+ import msvcrt
171
+
172
+ os.lseek(_embedding_lock_fd, 0, os.SEEK_SET)
173
+ msvcrt.locking(_embedding_lock_fd, msvcrt.LK_UNLCK, 1)
174
+ else:
175
+ import fcntl
176
+
177
+ fcntl.flock(_embedding_lock_fd, fcntl.LOCK_UN)
150
178
  os.close(_embedding_lock_fd)
151
179
  except Exception:
152
- pass
180
+ try:
181
+ os.close(_embedding_lock_fd)
182
+ except OSError:
183
+ pass
153
184
  _embedding_lock_fd = None
154
185
 
155
186
 
156
- _IDLE_TIMEOUT_SECONDS = 300 # 5 minutes — balance cold-start vs RAM.
157
- # V3.4.37: Reduced from 1800 300. Holding 1.1 GB for 30 min idle
158
- # wastes RAM on laptops. 5 min covers bursty session_init+recall
159
- # patterns while freeing memory between sessions.
187
+ _IDLE_TIMEOUT_SECONDS = 1800 # 30 minutes — keep interactive sessions warm.
188
+ # V3.8.1: existing-user soaks showed that the five-minute policy repeatedly
189
+ # recycled a ~1.1 GB local model and imposed 20-30 second cold starts. The
190
+ # explicit environment override remains available for low-RAM installations.
160
191
  _IDLE_TIMEOUT_SECONDS = int(os.environ.get("SLM_EMBED_IDLE_TIMEOUT", _IDLE_TIMEOUT_SECONDS))
161
192
  # V3.3.21: Configurable response timeout — 180s default, but batch ingestion
162
193
  # (2-turn chunks across 10 conversations) needs 600s+ to survive cold-start
@@ -186,6 +217,7 @@ class EmbeddingService:
186
217
  self._last_used: float = 0.0
187
218
  self._idle_timer: threading.Timer | None = None
188
219
  self._worker_ready = False
220
+ self._owns_worker_lock = False
189
221
  self._request_count: int = 0
190
222
  self._http_client: object | None = None
191
223
 
@@ -478,18 +510,32 @@ class EmbeddingService:
478
510
  v3.4.13: Machine-wide singleton — checks PID file before spawning.
479
511
  Only ONE embedding_worker can exist at a time on the machine.
480
512
  """
481
- if self._worker_proc is not None and self._worker_proc.poll() is None:
482
- return
483
- self._worker_proc = None
513
+ if self._worker_proc is not None:
514
+ if self._worker_proc.poll() is None:
515
+ return
516
+ # An unexpectedly exited child still leaves this service holding
517
+ # its lifetime flock. Fully close the dead process and release that
518
+ # lock before attempting the normal acquire/spawn sequence. Merely
519
+ # dropping the Popen reference makes the process deadlock against
520
+ # its own old flock until the acquire timeout expires.
521
+ self._kill_worker()
484
522
 
485
- # v3.4.13: Check if another worker is already alive (machine-wide)
523
+ # Serialize the check/spawn/register sequence across processes. Checking
524
+ # the PID file without this lock allows two cold callers to both see no
525
+ # worker and launch memory-heavy children.
526
+ if not acquire_embedding_lock():
527
+ logger.debug("Embedding worker owned by another process")
528
+ self._available = False
529
+ return
486
530
  if _is_embedding_worker_alive():
487
- logger.debug("Embedding worker already alive (PID file), skipping spawn")
531
+ release_embedding_lock()
532
+ logger.debug("Embedding worker already alive after lock acquisition")
488
533
  self._available = False
489
534
  return
490
535
 
491
536
  # V3.3.28: Check memory pressure before spawning
492
537
  if not self._check_memory_pressure():
538
+ release_embedding_lock()
493
539
  logger.warning("Skipping embedding worker spawn due to memory pressure")
494
540
  self._available = False
495
541
  return
@@ -524,9 +570,23 @@ class EmbeddingService:
524
570
  )
525
571
  # v3.4.13: Register PID for machine-wide singleton guard
526
572
  register_embedding_worker_pid(self._worker_proc.pid)
573
+ self._owns_worker_lock = True
527
574
  logger.info("Embedding worker spawned (PID %d)", self._worker_proc.pid)
528
575
  self._worker_ready = True
529
576
  except Exception as exc:
577
+ failed_proc = self._worker_proc
578
+ if failed_proc is not None:
579
+ try:
580
+ failed_proc.terminate()
581
+ failed_proc.wait(timeout=3)
582
+ except Exception:
583
+ try:
584
+ failed_proc.kill()
585
+ failed_proc.wait(timeout=3)
586
+ except Exception:
587
+ pass
588
+ release_embedding_lock()
589
+ self._owns_worker_lock = False
530
590
  logger.warning(
531
591
  "Failed to spawn embedding worker: %s. "
532
592
  "Run 'slm doctor' to verify your Python environment. "
@@ -576,9 +636,23 @@ class EmbeddingService:
576
636
  stream.close()
577
637
  except (BrokenPipeError, OSError, ValueError):
578
638
  pass
639
+ if getattr(self, "_owns_worker_lock", False):
640
+ try:
641
+ pid_file = _embedding_pid_file()
642
+ if (
643
+ proc is not None
644
+ and pid_file.exists()
645
+ and pid_file.read_text().strip() == str(proc.pid)
646
+ ):
647
+ pid_file.unlink(missing_ok=True)
648
+ except (OSError, ValueError):
649
+ pass
650
+ finally:
651
+ self._owns_worker_lock = False
652
+ release_embedding_lock()
579
653
 
580
654
  def _reset_idle_timer(self) -> None:
581
- """Reset idle timer — kills worker after 2 min inactivity."""
655
+ """Reset the configurable worker-idle timer."""
582
656
  if self._idle_timer is not None:
583
657
  self._idle_timer.cancel()
584
658
  self._idle_timer = threading.Timer(
@@ -180,6 +180,15 @@ class MemoryEngine:
180
180
  except Exception as exc:
181
181
  logger.warning("V3.4.6 schema migration failed: %s", exc)
182
182
 
183
+ # V3.4.7: Apply "Learning Brain" schema before deferred migrations.
184
+ # M029 adds composite history indexes to these runtime tables, so the
185
+ # tables must exist before the migration runner records M029 complete.
186
+ try:
187
+ from superlocalmemory.storage.schema_v347 import apply_v347_schema
188
+ apply_v347_schema(str(self._db.db_path))
189
+ except Exception as exc:
190
+ logger.warning("V3.4.7 schema migration failed: %s", exc)
191
+
183
192
  # v3.6.15: apply ALL pending migrations — including DEFERRED ones like
184
193
  # M016 (scope/shared_with columns) — for DIRECT-engine usage: `slm
185
194
  # remember --sync`, the Python API, and LangChain/CrewAI integrations.
@@ -213,13 +222,6 @@ class MemoryEngine:
213
222
  f"ingestion migration failed: {exc}"
214
223
  ) from exc
215
224
 
216
- # V3.4.7: Apply "Learning Brain" schema (tool_events, behavioral_assertions)
217
- try:
218
- from superlocalmemory.storage.schema_v347 import apply_v347_schema
219
- apply_v347_schema(str(self._db.db_path))
220
- except Exception as exc:
221
- logger.warning("V3.4.7 schema migration failed: %s", exc)
222
-
223
225
  # V3.4.10: Apply "Fortress" schema (backup_destinations, entity_blacklist)
224
226
  try:
225
227
  from superlocalmemory.storage.schema_v3410 import apply_v3410_schema
@@ -360,16 +362,17 @@ class MemoryEngine:
360
362
 
361
363
  self._check_embedding_migration()
362
364
 
363
- # V3.3.13: Background maintenance scheduler (Langevin/Ebbinghaus/Sheaf)
364
- if self._config.forgetting.enabled:
365
- try:
366
- from superlocalmemory.core.maintenance_scheduler import MaintenanceScheduler
367
- self._maintenance_scheduler = MaintenanceScheduler(
368
- self._db, self._config, self._profile_id,
369
- )
370
- self._maintenance_scheduler.start()
371
- except Exception as exc:
372
- logger.debug("Maintenance scheduler init failed: %s", exc)
365
+ # Lifecycle/tier evaluation, bounded housekeeping, and backup checks
366
+ # must continue even when optional forgetting/math maintenance is
367
+ # disabled. The scheduler itself gates those optional calculations.
368
+ try:
369
+ from superlocalmemory.core.maintenance_scheduler import MaintenanceScheduler
370
+ self._maintenance_scheduler = MaintenanceScheduler(
371
+ self._db, self._config, self._profile_id,
372
+ )
373
+ self._maintenance_scheduler.start()
374
+ except Exception as exc:
375
+ logger.debug("Maintenance scheduler init failed: %s", exc)
373
376
 
374
377
  def _process_pending_memories(self) -> None:
375
378
  """Process pending memories from store-first async pattern.
@@ -624,10 +627,10 @@ class MemoryEngine:
624
627
  ``put_nowait`` and the actual ``pending_outcomes`` INSERT runs
625
628
  on a background worker.
626
629
 
627
- ``fast=True`` is the latency-bounded path: it skips spreading
628
- activation and remote agentic verification while retaining semantic,
629
- lexical, graph, temporal, and Hopfield retrieval. Use full recall when
630
- maximum multi-round quality matters more than response time.
630
+ ``fast=True`` is the latency-bounded path: it retains all six local
631
+ retrieval channels but skips remote agentic verification. Use
632
+ ``fast=False`` only when maximum multi-round quality matters more than
633
+ response time.
631
634
 
632
635
  Multi-scope: ``include_global`` / ``include_shared`` control which
633
636
  scopes participate in retrieval. ``None`` (the default) means "use the