superlocalmemory 3.3.6 → 3.3.7

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.
package/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  </p>
4
4
 
5
5
  <h1 align="center">SuperLocalMemory V3.3</h1>
6
- <p align="center"><strong>The first local-only AI memory to break 74% retrieval on LoCoMo.<br/>No cloud. No APIs. No data leaves your machine.</strong></p>
6
+ <p align="center"><strong>Every other AI forgets. Yours won't.</strong><br/><em>Infinite memory for Claude Code, Cursor, Windsurf & 17+ AI tools.</em></p>
7
+ <p align="center"><code>v3.3.6</code> — Install once. Every session remembers the last. Automatically.</p>
7
8
 
8
9
  <p align="center">
9
10
  <code>+16pp vs Mem0 (zero cloud)</code> &nbsp;·&nbsp; <code>85% Open-Domain (best of any system)</code> &nbsp;·&nbsp; <code>EU AI Act Ready</code>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.3.6",
3
+ "version": "3.3.7",
4
4
  "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
5
5
  "keywords": [
6
6
  "ai-memory",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.3.6"
3
+ version = "3.3.7"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -23,9 +23,10 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
23
23
  from __future__ import annotations
24
24
 
25
25
  import json
26
+ import os
26
27
  import signal
27
28
  import sys
28
- import os
29
+ import threading
29
30
 
30
31
  # Force CPU BEFORE any torch import
31
32
  os.environ["CUDA_VISIBLE_DEVICES"] = ""
@@ -41,8 +42,33 @@ if sys.platform != "win32":
41
42
  signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
42
43
 
43
44
 
45
+ def _start_parent_watchdog() -> None:
46
+ """Monitor parent process — self-terminate if parent dies.
47
+
48
+ Prevents orphaned workers that consume 500-800 MB each when the parent
49
+ process crashes, is killed, or exits without cleanup.
50
+
51
+ V3.3.7: Added after incident where orphaned workers consumed 33 GB.
52
+ """
53
+ parent_pid = os.getppid()
54
+
55
+ def _watch() -> None:
56
+ import time
57
+ while True:
58
+ time.sleep(5)
59
+ try:
60
+ os.kill(parent_pid, 0)
61
+ except OSError:
62
+ os._exit(0)
63
+
64
+ t = threading.Thread(target=_watch, daemon=True, name="parent-watchdog")
65
+ t.start()
66
+
67
+
44
68
  def _worker_main() -> None:
45
69
  """Main loop: read JSON requests from stdin, write responses to stdout."""
70
+ _start_parent_watchdog() # V3.3.7: self-terminate if parent dies
71
+
46
72
  import numpy as np
47
73
 
48
74
  model = None
@@ -15,6 +15,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import atexit
18
19
  import json
19
20
  import logging
20
21
  import os
@@ -22,11 +23,15 @@ import subprocess
22
23
  import sys
23
24
  import threading
24
25
  import time
26
+ import weakref
25
27
  from pathlib import Path
26
28
  from typing import TYPE_CHECKING
27
29
 
28
30
  import numpy as np
29
31
 
32
+ # Track all live embedding services for atexit cleanup
33
+ _live_embedding_services: set[weakref.ref] = set()
34
+
30
35
  if TYPE_CHECKING:
31
36
  from numpy.typing import NDArray
32
37
 
@@ -69,6 +74,17 @@ class EmbeddingService:
69
74
  self._worker_ready = False
70
75
  self._request_count: int = 0
71
76
 
77
+ # Register for atexit cleanup (prevent orphaned workers)
78
+ ref = weakref.ref(self, _live_embedding_services.discard)
79
+ _live_embedding_services.add(ref)
80
+
81
+ def __del__(self) -> None:
82
+ """Kill worker subprocess when service is garbage-collected."""
83
+ try:
84
+ self._kill_worker()
85
+ except Exception:
86
+ pass
87
+
72
88
  @property
73
89
  def is_available(self) -> bool:
74
90
  """Check if embedding service can produce embeddings."""
@@ -338,3 +354,26 @@ class EmbeddingService:
338
354
  raise DimensionMismatchError(
339
355
  f"Embedding dimension {actual} != expected {self._config.dimension}"
340
356
  )
357
+
358
+
359
+ # ---------------------------------------------------------------------------
360
+ # Module-level atexit: kill ALL embedding workers on process exit
361
+ # ---------------------------------------------------------------------------
362
+
363
+ def _cleanup_all_embedding_services() -> None:
364
+ """Kill all embedding worker subprocesses on interpreter exit.
365
+
366
+ Prevents orphaned 500-800 MB sentence-transformer workers surviving
367
+ after parent exits (especially during test runs with parallel agents).
368
+ """
369
+ for ref in list(_live_embedding_services):
370
+ svc = ref()
371
+ if svc is not None:
372
+ try:
373
+ svc._kill_worker()
374
+ except Exception:
375
+ pass
376
+ _live_embedding_services.clear()
377
+
378
+
379
+ atexit.register(_cleanup_all_embedding_services)
@@ -20,6 +20,7 @@ import json
20
20
  import os
21
21
  import signal
22
22
  import sys
23
+ import threading
23
24
 
24
25
  # Force CPU BEFORE any torch import
25
26
  os.environ["CUDA_VISIBLE_DEVICES"] = ""
@@ -34,6 +35,29 @@ os.environ["TORCH_DEVICE"] = "cpu"
34
35
  if sys.platform != "win32":
35
36
  signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
36
37
 
38
+
39
+ def _start_parent_watchdog() -> None:
40
+ """Monitor parent process — self-terminate if parent dies.
41
+
42
+ Prevents orphaned workers that consume 500+ MB each when the parent
43
+ process crashes, is killed, or exits without cleanup.
44
+
45
+ V3.3.7: Added after incident where orphaned workers consumed 33 GB.
46
+ """
47
+ parent_pid = os.getppid()
48
+
49
+ def _watch() -> None:
50
+ import time
51
+ while True:
52
+ time.sleep(5)
53
+ try:
54
+ os.kill(parent_pid, 0)
55
+ except OSError:
56
+ os._exit(0)
57
+
58
+ t = threading.Thread(target=_watch, daemon=True, name="parent-watchdog")
59
+ t.start()
60
+
37
61
  _engine = None
38
62
 
39
63
 
@@ -209,6 +233,8 @@ def _handle_status() -> dict:
209
233
 
210
234
  def _worker_main() -> None:
211
235
  """Main loop: read JSON requests from stdin, write responses to stdout."""
236
+ _start_parent_watchdog() # V3.3.7: self-terminate if parent dies
237
+
212
238
  for line in sys.stdin:
213
239
  line = line.strip()
214
240
  if not line:
@@ -16,6 +16,7 @@ License: MIT
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import atexit
19
20
  import json
20
21
  import logging
21
22
  import os
@@ -23,10 +24,14 @@ import subprocess
23
24
  import sys
24
25
  import threading
25
26
  import time
27
+ import weakref
26
28
  from typing import Any
27
29
 
28
30
  from superlocalmemory.storage.models import AtomicFact
29
31
 
32
+ # Track all live reranker instances for atexit cleanup
33
+ _live_rerankers: set[weakref.ref] = set()
34
+
30
35
  logger = logging.getLogger(__name__)
31
36
 
32
37
  _IDLE_TIMEOUT_SECONDS = 120 # 2 min → kill worker
@@ -64,11 +69,22 @@ class CrossEncoderReranker:
64
69
  self._idle_timer: threading.Timer | None = None
65
70
  self._request_count: int = 0
66
71
 
72
+ # Register for atexit cleanup (prevent orphaned workers)
73
+ ref = weakref.ref(self, _live_rerankers.discard)
74
+ _live_rerankers.add(ref)
75
+
67
76
  # Start background warmup immediately — worker loads model
68
77
  # while the rest of init continues. First recall gets instant
69
78
  # fallback; second recall uses the warm model.
70
79
  self._start_background_warmup()
71
80
 
81
+ def __del__(self) -> None:
82
+ """Kill worker subprocess when reranker is garbage-collected."""
83
+ try:
84
+ self._kill_worker()
85
+ except Exception:
86
+ pass
87
+
72
88
  # ------------------------------------------------------------------
73
89
  # Background warmup (non-blocking model load)
74
90
  # ------------------------------------------------------------------
@@ -330,3 +346,26 @@ class CrossEncoderReranker:
330
346
  if resp is None:
331
347
  return False
332
348
  return resp.get("ok", False)
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Module-level atexit: kill ALL reranker workers on process exit
353
+ # ---------------------------------------------------------------------------
354
+
355
+ def _cleanup_all_rerankers() -> None:
356
+ """Kill all reranker worker subprocesses on interpreter exit.
357
+
358
+ Prevents orphaned 1.3 GB ONNX/PyTorch workers surviving after
359
+ parent exits (especially during test runs with parallel agents).
360
+ """
361
+ for ref in list(_live_rerankers):
362
+ reranker = ref()
363
+ if reranker is not None:
364
+ try:
365
+ reranker._kill_worker()
366
+ except Exception:
367
+ pass
368
+ _live_rerankers.clear()
369
+
370
+
371
+ atexit.register(_cleanup_all_rerankers)