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 +2 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/core/embedding_worker.py +27 -1
- package/src/superlocalmemory/core/embeddings.py +39 -0
- package/src/superlocalmemory/core/recall_worker.py +26 -0
- package/src/superlocalmemory/retrieval/reranker.py +39 -0
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>
|
|
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> · <code>85% Open-Domain (best of any system)</code> · <code>EU AI Act Ready</code>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.3.
|
|
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
|
@@ -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
|
|
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)
|