superlocalmemory 3.3.5 → 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/scripts/postinstall.js +32 -1
- package/src/superlocalmemory/cli/commands.py +129 -9
- package/src/superlocalmemory/cli/main.py +19 -0
- 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/hooks/claude_code_hooks.py +351 -122
- package/src/superlocalmemory/hooks/hook_handlers.py +394 -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
package/scripts/postinstall.js
CHANGED
|
@@ -213,17 +213,48 @@ if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
|
|
|
213
213
|
console.log('');
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
// --- Step 5: Auto-install Claude Code hooks ---
|
|
217
|
+
// "Install once, forget forever" — hooks enable automatic memory lifecycle
|
|
218
|
+
const hooksDisabledFile = path.join(SLM_HOME, 'hooks', '.hooks-disabled');
|
|
219
|
+
if (fs.existsSync(hooksDisabledFile)) {
|
|
220
|
+
console.log('⊘ Claude Code hooks: skipped (user opted out via slm hooks remove)');
|
|
221
|
+
} else {
|
|
222
|
+
console.log('\nInstalling Claude Code hooks (auto-memory lifecycle)...');
|
|
223
|
+
const hookResult = spawnSync(pythonParts[0], [
|
|
224
|
+
...pythonParts.slice(1), '-m', 'superlocalmemory.cli.main', 'hooks', 'install',
|
|
225
|
+
], {
|
|
226
|
+
stdio: 'pipe', timeout: 15000,
|
|
227
|
+
env: {
|
|
228
|
+
...process.env,
|
|
229
|
+
PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || ''),
|
|
230
|
+
PYTHONPATH: path.join(__dirname, '..', 'src') + ':' + (process.env.PYTHONPATH || ''),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (hookResult.status === 0) {
|
|
235
|
+
console.log('✓ Claude Code hooks installed (auto-recall, auto-observe, auto-save)');
|
|
236
|
+
console.log(' SLM: Hooks installed into Claude Code (slm hooks remove to undo)');
|
|
237
|
+
} else {
|
|
238
|
+
console.log('⚠ Claude Code hooks not installed (run: slm hooks install)');
|
|
239
|
+
// Non-fatal — don't block npm install
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
216
243
|
// --- Done ---
|
|
217
244
|
console.log('════════════════════════════════════════════════════════════');
|
|
218
245
|
console.log(' ✓ SuperLocalMemory V3 installed successfully!');
|
|
219
246
|
console.log('');
|
|
220
247
|
console.log(' Quick start:');
|
|
221
|
-
console.log('
|
|
248
|
+
console.log(' Just open Claude Code — memory works automatically!');
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log(' Other commands:');
|
|
222
251
|
console.log(' slm doctor # Pre-flight check (verify everything works)');
|
|
223
252
|
console.log(' slm warmup # Pre-download embedding model (~500MB)');
|
|
224
253
|
console.log(' slm remember "..." # Store a memory');
|
|
225
254
|
console.log(' slm recall "..." # Search memories');
|
|
226
255
|
console.log(' slm dashboard # Open 17-tab web dashboard');
|
|
256
|
+
console.log(' slm hooks status # Check hook installation');
|
|
257
|
+
console.log(' slm hooks remove # Opt out of auto-memory hooks');
|
|
227
258
|
console.log('');
|
|
228
259
|
console.log(' Prerequisites satisfied:');
|
|
229
260
|
console.log(' ✓ Python 3.11+');
|
|
@@ -18,7 +18,16 @@ from argparse import Namespace
|
|
|
18
18
|
|
|
19
19
|
def dispatch(args: Namespace) -> None:
|
|
20
20
|
"""Route CLI command to the appropriate handler."""
|
|
21
|
+
# Auto-install/upgrade hooks on version change (single file read, ~0.1ms)
|
|
22
|
+
if args.command not in ("hooks", "init", "mcp"):
|
|
23
|
+
try:
|
|
24
|
+
from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
|
|
25
|
+
auto_install_if_needed()
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
21
29
|
handlers = {
|
|
30
|
+
"init": cmd_init,
|
|
22
31
|
"setup": cmd_setup,
|
|
23
32
|
"mode": cmd_mode,
|
|
24
33
|
"provider": cmd_provider,
|
|
@@ -923,6 +932,14 @@ def cmd_trace(args: Namespace) -> None:
|
|
|
923
932
|
|
|
924
933
|
def cmd_mcp(_args: Namespace) -> None:
|
|
925
934
|
"""Start the V3 MCP server (stdio transport for IDE integration)."""
|
|
935
|
+
# Auto-install hooks on MCP startup (fast path: ~0.1ms if already current)
|
|
936
|
+
# CRITICAL: No stdout — MCP uses stdio transport, any print corrupts protocol
|
|
937
|
+
try:
|
|
938
|
+
from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
|
|
939
|
+
auto_install_if_needed()
|
|
940
|
+
except Exception:
|
|
941
|
+
pass
|
|
942
|
+
|
|
926
943
|
from superlocalmemory.mcp.server import server
|
|
927
944
|
|
|
928
945
|
server.run(transport="stdio")
|
|
@@ -1142,6 +1159,99 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
1142
1159
|
# -- Active Memory commands (V3.1) ------------------------------------------
|
|
1143
1160
|
|
|
1144
1161
|
|
|
1162
|
+
def cmd_init(args: Namespace) -> None:
|
|
1163
|
+
"""One-command setup: mode + hooks + IDE connect + warmup."""
|
|
1164
|
+
from pathlib import Path
|
|
1165
|
+
from superlocalmemory.core.config import SLMConfig
|
|
1166
|
+
|
|
1167
|
+
force = getattr(args, "force", False)
|
|
1168
|
+
|
|
1169
|
+
config_exists = (Path.home() / ".superlocalmemory" / "config.json").exists()
|
|
1170
|
+
|
|
1171
|
+
print()
|
|
1172
|
+
print("SuperLocalMemory — One-Time Setup")
|
|
1173
|
+
print("=" * 40)
|
|
1174
|
+
|
|
1175
|
+
# Step 1: Mode selection (interactive)
|
|
1176
|
+
if force or not config_exists:
|
|
1177
|
+
print()
|
|
1178
|
+
from superlocalmemory.cli.setup_wizard import run_wizard
|
|
1179
|
+
run_wizard()
|
|
1180
|
+
else:
|
|
1181
|
+
config = SLMConfig.load()
|
|
1182
|
+
print(f"\n Already configured: Mode {config.mode.value.upper()}")
|
|
1183
|
+
print(f" Profile: {config.active_profile}")
|
|
1184
|
+
|
|
1185
|
+
# Step 2: Install hooks (gate always OFF by default)
|
|
1186
|
+
print()
|
|
1187
|
+
print("Installing Claude Code hooks...")
|
|
1188
|
+
from superlocalmemory.hooks.claude_code_hooks import install_hooks, check_status
|
|
1189
|
+
|
|
1190
|
+
status = check_status()
|
|
1191
|
+
|
|
1192
|
+
if status["installed"] and not force:
|
|
1193
|
+
if status["needs_upgrade"]:
|
|
1194
|
+
from superlocalmemory.hooks.claude_code_hooks import upgrade_hooks
|
|
1195
|
+
result = upgrade_hooks()
|
|
1196
|
+
if result.get("upgraded"):
|
|
1197
|
+
print(f" Hooks upgraded: {result['from_version']} -> {result['to_version']}")
|
|
1198
|
+
else:
|
|
1199
|
+
print(f" Upgrade issue: {result.get('reason', result.get('errors', ''))}")
|
|
1200
|
+
else:
|
|
1201
|
+
print(f" Hooks already installed (v{status['version']})")
|
|
1202
|
+
else:
|
|
1203
|
+
result = install_hooks(include_gate=False)
|
|
1204
|
+
if result["success"]:
|
|
1205
|
+
print(f" Hooks installed: {', '.join(result['hooks_added'])}")
|
|
1206
|
+
print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
|
|
1207
|
+
else:
|
|
1208
|
+
print(f" Hook install failed: {result['errors']}")
|
|
1209
|
+
|
|
1210
|
+
# Step 3: IDE connection
|
|
1211
|
+
print()
|
|
1212
|
+
print("Detecting IDEs...")
|
|
1213
|
+
try:
|
|
1214
|
+
from superlocalmemory.hooks.ide_connector import IDEConnector
|
|
1215
|
+
connector = IDEConnector()
|
|
1216
|
+
results = connector.connect_all()
|
|
1217
|
+
for ide_id, ide_status in results.items():
|
|
1218
|
+
print(f" {ide_id}: {ide_status}")
|
|
1219
|
+
except Exception as exc:
|
|
1220
|
+
print(f" IDE detection skipped: {exc}")
|
|
1221
|
+
|
|
1222
|
+
# Step 4: Warmup (embedding model)
|
|
1223
|
+
print()
|
|
1224
|
+
print("Checking embedding model...")
|
|
1225
|
+
try:
|
|
1226
|
+
from superlocalmemory.core.config import SLMConfig as _Cfg
|
|
1227
|
+
cfg = _Cfg.load()
|
|
1228
|
+
model_name = cfg.embedding.model_name
|
|
1229
|
+
print(f" Model: {model_name}")
|
|
1230
|
+
# Quick check: try creating embedding service (auto-downloads if needed)
|
|
1231
|
+
from superlocalmemory.core.embeddings import EmbeddingService
|
|
1232
|
+
svc = EmbeddingService(cfg.embedding)
|
|
1233
|
+
test_result = svc.embed_text("test")
|
|
1234
|
+
if test_result is not None and len(test_result) > 0:
|
|
1235
|
+
print(" Status: ready")
|
|
1236
|
+
else:
|
|
1237
|
+
print(" Status: model not available (run: slm warmup)")
|
|
1238
|
+
except Exception as exc:
|
|
1239
|
+
print(f" Warmup skipped: {exc}")
|
|
1240
|
+
print(" Run 'slm warmup' later to download the embedding model.")
|
|
1241
|
+
|
|
1242
|
+
# Done
|
|
1243
|
+
print()
|
|
1244
|
+
print("=" * 40)
|
|
1245
|
+
print("SLM is active. Your AI now remembers you.")
|
|
1246
|
+
print()
|
|
1247
|
+
print("What happens next:")
|
|
1248
|
+
print(" - Open Claude Code in any project")
|
|
1249
|
+
print(" - SLM auto-injects your memory context")
|
|
1250
|
+
print(" - Decisions, bugs, preferences are captured automatically")
|
|
1251
|
+
print(" - Session summaries saved when you close")
|
|
1252
|
+
print()
|
|
1253
|
+
|
|
1254
|
+
|
|
1145
1255
|
def cmd_hooks(args: Namespace) -> None:
|
|
1146
1256
|
"""Manage Claude Code hooks for invisible memory injection."""
|
|
1147
1257
|
from superlocalmemory.hooks.claude_code_hooks import (
|
|
@@ -1149,28 +1259,38 @@ def cmd_hooks(args: Namespace) -> None:
|
|
|
1149
1259
|
)
|
|
1150
1260
|
|
|
1151
1261
|
action = getattr(args, "action", "status")
|
|
1262
|
+
# Gate is OFF by default. --gate opts in (for brave users).
|
|
1263
|
+
include_gate = getattr(args, "gate", False)
|
|
1264
|
+
|
|
1152
1265
|
if action == "install":
|
|
1153
|
-
result = install_hooks()
|
|
1154
|
-
if result["
|
|
1266
|
+
result = install_hooks(include_gate=include_gate)
|
|
1267
|
+
if result["success"]:
|
|
1155
1268
|
print("SLM hooks installed in Claude Code.")
|
|
1156
|
-
print("
|
|
1269
|
+
print(f" Hook types: {', '.join(result['hooks_added'])}")
|
|
1270
|
+
if include_gate:
|
|
1271
|
+
print(" Gate: ON (enforces session_init — experimental)")
|
|
1272
|
+
print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
|
|
1157
1273
|
else:
|
|
1158
|
-
print(f"Installation
|
|
1274
|
+
print(f"Installation failed: {result['errors']}")
|
|
1159
1275
|
elif action == "remove":
|
|
1160
1276
|
result = remove_hooks()
|
|
1161
|
-
if result["
|
|
1277
|
+
if result["success"]:
|
|
1162
1278
|
print("SLM hooks removed from Claude Code.")
|
|
1163
1279
|
else:
|
|
1164
|
-
print(f"Removal
|
|
1280
|
+
print(f"Removal failed: {result['errors']}")
|
|
1165
1281
|
else:
|
|
1166
1282
|
result = check_status()
|
|
1167
1283
|
if result["installed"]:
|
|
1168
|
-
print("SLM hooks: INSTALLED")
|
|
1169
|
-
print(f"
|
|
1170
|
-
print("
|
|
1284
|
+
print(f"SLM hooks: INSTALLED (v{result['version']})")
|
|
1285
|
+
print(f" Hook types: {', '.join(result['hook_types'])}")
|
|
1286
|
+
print(f" Gate: {'ON' if result['gate_enabled'] else 'OFF'}")
|
|
1287
|
+
if result["needs_upgrade"]:
|
|
1288
|
+
print(f" Update available: {result['version']} -> {result['latest_version']}")
|
|
1289
|
+
print(" Run: slm hooks install")
|
|
1171
1290
|
else:
|
|
1172
1291
|
print("SLM hooks: NOT INSTALLED")
|
|
1173
1292
|
print(" Run: slm hooks install")
|
|
1293
|
+
print(" Or: slm init (full setup)")
|
|
1174
1294
|
|
|
1175
1295
|
|
|
1176
1296
|
def cmd_session_context(args: Namespace) -> None:
|
|
@@ -70,6 +70,12 @@ documentation:
|
|
|
70
70
|
|
|
71
71
|
def main() -> None:
|
|
72
72
|
"""Parse CLI arguments and dispatch to command handlers."""
|
|
73
|
+
# Fast path: hook invocations bypass argparse entirely (stdlib only, ~30ms)
|
|
74
|
+
if len(sys.argv) >= 3 and sys.argv[1] == "hook":
|
|
75
|
+
from superlocalmemory.hooks.hook_handlers import handle_hook
|
|
76
|
+
handle_hook(sys.argv[2])
|
|
77
|
+
return
|
|
78
|
+
|
|
73
79
|
from superlocalmemory.cli.json_output import _get_version
|
|
74
80
|
_ver = _get_version()
|
|
75
81
|
|
|
@@ -85,6 +91,15 @@ def main() -> None:
|
|
|
85
91
|
sub = parser.add_subparsers(dest="command", title="commands")
|
|
86
92
|
|
|
87
93
|
# -- Setup & Config ------------------------------------------------
|
|
94
|
+
init_p = sub.add_parser("init", help="One-command setup: mode + hooks + IDE + warmup")
|
|
95
|
+
init_p.add_argument(
|
|
96
|
+
"--force", action="store_true", help="Re-run full setup even if already configured",
|
|
97
|
+
)
|
|
98
|
+
init_p.add_argument(
|
|
99
|
+
"--gate", action="store_true",
|
|
100
|
+
help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
|
|
101
|
+
)
|
|
102
|
+
|
|
88
103
|
sub.add_parser("setup", help="Interactive first-time setup wizard")
|
|
89
104
|
|
|
90
105
|
mode_p = sub.add_parser("mode", help="Get or set operating mode (a/b/c)")
|
|
@@ -182,6 +197,10 @@ def main() -> None:
|
|
|
182
197
|
"action", nargs="?", default="status",
|
|
183
198
|
choices=["install", "remove", "status"], help="Action (default: status)",
|
|
184
199
|
)
|
|
200
|
+
hooks_p.add_argument(
|
|
201
|
+
"--gate", action="store_true",
|
|
202
|
+
help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
|
|
203
|
+
)
|
|
185
204
|
|
|
186
205
|
ctx_p = sub.add_parser("session-context", help="Print session context (for hooks)")
|
|
187
206
|
ctx_p.add_argument("query", nargs="?", default="", help="Optional context query")
|
|
@@ -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:
|