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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Claude Code hook handlers — zero-dependency, cross-platform.
|
|
6
|
+
|
|
7
|
+
All handlers use ONLY Python stdlib (sys, os, json, tempfile, subprocess, time).
|
|
8
|
+
No SLM imports in the hot path. Called via: slm hook <start|gate|init-done|checkpoint|stop>
|
|
9
|
+
|
|
10
|
+
The main() entry point in cli/main.py has a fast path that dispatches here
|
|
11
|
+
BEFORE argparse or any heavy imports.
|
|
12
|
+
|
|
13
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Cross-platform temp paths
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
_TMP = tempfile.gettempdir()
|
|
29
|
+
_MARKER = os.path.join(_TMP, "slm-session-initialized")
|
|
30
|
+
_START_TIME = os.path.join(_TMP, "slm-session-start-time")
|
|
31
|
+
_ACTIVITY_LOG = os.path.join(_TMP, "slm-session-activity")
|
|
32
|
+
_LAST_CONSOLIDATION = os.path.join(
|
|
33
|
+
os.path.expanduser("~"), ".superlocalmemory", ".last-consolidation",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def handle_hook(action: str) -> None:
|
|
38
|
+
"""Dispatch to the appropriate hook handler. Called from main() fast path."""
|
|
39
|
+
handlers = {
|
|
40
|
+
"start": _hook_start,
|
|
41
|
+
"gate": _hook_gate,
|
|
42
|
+
"init-done": _hook_init_done,
|
|
43
|
+
"checkpoint": _hook_checkpoint,
|
|
44
|
+
"stop": _hook_stop,
|
|
45
|
+
}
|
|
46
|
+
handler = handlers.get(action)
|
|
47
|
+
if handler is None:
|
|
48
|
+
print(f"Unknown hook action: {action}", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
handler()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# 1. SESSION START — SessionStart hook
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def _hook_start() -> None:
|
|
58
|
+
"""Clean markers, inject SQL-fast context, print session_init mandate."""
|
|
59
|
+
# Clean stale markers from previous sessions
|
|
60
|
+
for f in (_MARKER, _START_TIME, _ACTIVITY_LOG):
|
|
61
|
+
try:
|
|
62
|
+
os.remove(f)
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Record session start time
|
|
67
|
+
with open(_START_TIME, "w") as f:
|
|
68
|
+
f.write(str(int(time.time())))
|
|
69
|
+
|
|
70
|
+
# Initialize activity log
|
|
71
|
+
with open(_ACTIVITY_LOG, "w") as f:
|
|
72
|
+
f.write("")
|
|
73
|
+
|
|
74
|
+
# Reap orphan MCP processes (background, best-effort)
|
|
75
|
+
try:
|
|
76
|
+
if sys.platform != "win32":
|
|
77
|
+
subprocess.Popen(
|
|
78
|
+
["sh", "-c",
|
|
79
|
+
"ps -eo pid,args 2>/dev/null"
|
|
80
|
+
" | grep -E 'node.*\\.bin/|node.*slm |uv tool uvx'"
|
|
81
|
+
" | grep -v grep"
|
|
82
|
+
" | awk '{print $1, $NF}'"
|
|
83
|
+
" | sort -k2,2 -k1,1rn"
|
|
84
|
+
" | awk '{if($2==p)print $1; p=$2}'"
|
|
85
|
+
" | xargs kill 2>/dev/null"],
|
|
86
|
+
stdout=subprocess.DEVNULL,
|
|
87
|
+
stderr=subprocess.DEVNULL,
|
|
88
|
+
)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
# Print session context (SQL-fast path, <500ms)
|
|
93
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
94
|
+
project_name = os.path.basename(project_dir)
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
["slm", "session-context", project_name],
|
|
98
|
+
capture_output=True, text=True, timeout=12,
|
|
99
|
+
)
|
|
100
|
+
if result.stdout.strip():
|
|
101
|
+
print(result.stdout.strip())
|
|
102
|
+
except Exception:
|
|
103
|
+
print("# SLM Session Context — unavailable")
|
|
104
|
+
|
|
105
|
+
# Mandatory session_init instruction
|
|
106
|
+
print()
|
|
107
|
+
print("## MANDATORY: SLM Session Init")
|
|
108
|
+
print("BEFORE your first response, call:")
|
|
109
|
+
print(f" mcp__superlocalmemory__session_init with project_path='{project_dir}'"
|
|
110
|
+
" and a topic from the user's first message")
|
|
111
|
+
print("session_init returns both context AND memories — no separate recall needed.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# 2. GATE — PreToolUse hook (default, enforces session_init)
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def _hook_gate() -> None:
|
|
119
|
+
"""Block non-SLM tools until session_init has been called.
|
|
120
|
+
|
|
121
|
+
Fast path (~30ms): marker file exists → exit 0.
|
|
122
|
+
Slow path (~80ms): parse JSON stdin, allow SLM tools, block rest.
|
|
123
|
+
"""
|
|
124
|
+
# Fast path: already initialized
|
|
125
|
+
if os.path.exists(_MARKER):
|
|
126
|
+
sys.exit(0)
|
|
127
|
+
|
|
128
|
+
# Safety: if session-start never ran, don't gate (avoid lockout)
|
|
129
|
+
if not os.path.exists(_START_TIME):
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
# Parse tool name from stdin
|
|
133
|
+
tool_name = ""
|
|
134
|
+
if not sys.stdin.isatty():
|
|
135
|
+
try:
|
|
136
|
+
data = json.load(sys.stdin)
|
|
137
|
+
tool_name = data.get("tool_name", "")
|
|
138
|
+
except Exception:
|
|
139
|
+
# Can't parse input — don't block (safety)
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
|
|
142
|
+
# Allow SLM tools through (needed to call session_init itself)
|
|
143
|
+
if tool_name.startswith("mcp__superlocalmemory__"):
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
|
|
146
|
+
# Allow ToolSearch through (needed to fetch SLM tool schemas)
|
|
147
|
+
if tool_name == "ToolSearch":
|
|
148
|
+
sys.exit(0)
|
|
149
|
+
|
|
150
|
+
# Block everything else
|
|
151
|
+
print("[SLM-GATE] BLOCKED: Call mcp__superlocalmemory__session_init"
|
|
152
|
+
" before using other tools.")
|
|
153
|
+
sys.exit(2)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# 3. INIT DONE — PostToolUse hook for session_init
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def _hook_init_done() -> None:
|
|
161
|
+
"""Create marker file to lift the gate for the rest of the session."""
|
|
162
|
+
with open(_MARKER, "w") as f:
|
|
163
|
+
f.write(str(int(time.time())))
|
|
164
|
+
sys.exit(0)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# 4. CHECKPOINT — PostToolUse hook for Write|Edit
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
_OBSERVE_COOLDOWN = 300 # 5 minutes per file
|
|
172
|
+
_RECALL_INTERVAL = 900 # 15 minutes
|
|
173
|
+
_LEARN_INTERVAL = 1800 # 30 minutes
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _hook_checkpoint() -> None:
|
|
177
|
+
"""Auto-observe file changes + periodic recall/learn reminders.
|
|
178
|
+
|
|
179
|
+
1. Directly calls `slm observe` for file change tracking (no Claude needed)
|
|
180
|
+
2. Suggests richer observe to Claude
|
|
181
|
+
3. Periodic recall refresh reminder
|
|
182
|
+
4. Periodic learn/patterns reminder
|
|
183
|
+
"""
|
|
184
|
+
now = int(time.time())
|
|
185
|
+
|
|
186
|
+
# Parse file_path from stdin
|
|
187
|
+
file_path = ""
|
|
188
|
+
if not sys.stdin.isatty():
|
|
189
|
+
try:
|
|
190
|
+
data = json.load(sys.stdin)
|
|
191
|
+
tool_input = data.get("tool_input", {})
|
|
192
|
+
if isinstance(tool_input, dict):
|
|
193
|
+
file_path = tool_input.get("file_path", "")
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
# --- Auto-observe file change (direct, no Claude needed) ---
|
|
198
|
+
if file_path:
|
|
199
|
+
basename = os.path.basename(file_path)
|
|
200
|
+
lock_file = os.path.join(_TMP, f"slm-obs-{_safe_hash(file_path)}")
|
|
201
|
+
|
|
202
|
+
if _cooldown_elapsed(lock_file, _OBSERVE_COOLDOWN, now):
|
|
203
|
+
_write_timestamp(lock_file, now)
|
|
204
|
+
|
|
205
|
+
# Direct observe — SLM records the change even if Claude ignores
|
|
206
|
+
try:
|
|
207
|
+
subprocess.Popen(
|
|
208
|
+
["slm", "observe", f"File changed: {basename}"],
|
|
209
|
+
stdout=subprocess.DEVNULL,
|
|
210
|
+
stderr=subprocess.DEVNULL,
|
|
211
|
+
)
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
# Log to session activity
|
|
216
|
+
try:
|
|
217
|
+
with open(_ACTIVITY_LOG, "a") as f:
|
|
218
|
+
f.write(f"{now}|{basename}\n")
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
# Suggest richer observe to Claude (with semantic context)
|
|
223
|
+
print(f"[SLM-AUTO] File changed: {basename}"
|
|
224
|
+
" — Call mcp__superlocalmemory__observe with a 1-line"
|
|
225
|
+
" summary of what was changed and why.")
|
|
226
|
+
|
|
227
|
+
# --- Periodic recall reminder (every 15 min) ---
|
|
228
|
+
recall_lock = os.path.join(_TMP, "slm-recall-reminder")
|
|
229
|
+
if _cooldown_elapsed(recall_lock, _RECALL_INTERVAL, now):
|
|
230
|
+
_write_timestamp(recall_lock, now)
|
|
231
|
+
print("[SLM] 15+ min since last context refresh."
|
|
232
|
+
" Call mcp__superlocalmemory__recall with current work topic.")
|
|
233
|
+
|
|
234
|
+
# --- Periodic learn reminder (every 30 min) ---
|
|
235
|
+
learn_lock = os.path.join(_TMP, "slm-learn-reminder")
|
|
236
|
+
if _cooldown_elapsed(learn_lock, _LEARN_INTERVAL, now):
|
|
237
|
+
_write_timestamp(learn_lock, now)
|
|
238
|
+
print("[SLM] Call mcp__superlocalmemory__get_learned_patterns"
|
|
239
|
+
" to adapt to learned preferences.")
|
|
240
|
+
|
|
241
|
+
sys.exit(0)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# 5. STOP — Stop hook (session end)
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def _hook_stop() -> None:
|
|
249
|
+
"""Save rich session summary + trigger auto-consolidation."""
|
|
250
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
251
|
+
project_name = os.path.basename(project_dir)
|
|
252
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M")
|
|
253
|
+
|
|
254
|
+
# --- Git context ---
|
|
255
|
+
git_branch = _run_quiet(["git", "-C", project_dir, "branch", "--show-current"])
|
|
256
|
+
git_diff = _run_quiet(
|
|
257
|
+
["git", "-C", project_dir, "diff", "--stat"],
|
|
258
|
+
postprocess=lambda s: s.strip().rsplit("\n", 1)[-1].strip() if s.strip() else "",
|
|
259
|
+
)
|
|
260
|
+
recent_commits = _run_quiet(
|
|
261
|
+
["git", "-C", project_dir, "log", "--oneline", "-5", "--since=3 hours ago"],
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# --- Files from activity log ---
|
|
265
|
+
modified = ""
|
|
266
|
+
try:
|
|
267
|
+
if os.path.exists(_ACTIVITY_LOG):
|
|
268
|
+
with open(_ACTIVITY_LOG) as f:
|
|
269
|
+
files = sorted({line.split("|", 1)[1].strip()
|
|
270
|
+
for line in f if "|" in line})
|
|
271
|
+
modified = ", ".join(files[:20])
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# --- Build summary ---
|
|
276
|
+
parts = [f"[{project_name}] session ended {timestamp}"]
|
|
277
|
+
if git_branch:
|
|
278
|
+
parts.append(f"branch: {git_branch}")
|
|
279
|
+
if git_diff:
|
|
280
|
+
parts.append(f"uncommitted: {git_diff}")
|
|
281
|
+
if recent_commits:
|
|
282
|
+
commits = "; ".join(recent_commits.strip().split("\n")[:5])
|
|
283
|
+
parts.append(f"recent: {commits}")
|
|
284
|
+
if modified:
|
|
285
|
+
parts.append(f"files: {modified}")
|
|
286
|
+
|
|
287
|
+
summary = " | ".join(parts)
|
|
288
|
+
|
|
289
|
+
# --- Save to SLM ---
|
|
290
|
+
try:
|
|
291
|
+
subprocess.run(
|
|
292
|
+
["slm", "observe", summary],
|
|
293
|
+
capture_output=True, timeout=8,
|
|
294
|
+
)
|
|
295
|
+
except Exception:
|
|
296
|
+
try:
|
|
297
|
+
subprocess.run(
|
|
298
|
+
["slm", "remember", summary],
|
|
299
|
+
capture_output=True, timeout=8,
|
|
300
|
+
)
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
# --- Auto-consolidation (if >24h since last run) ---
|
|
305
|
+
_maybe_consolidate()
|
|
306
|
+
|
|
307
|
+
# --- Clean up session markers ---
|
|
308
|
+
for f in (_MARKER, _START_TIME, _ACTIVITY_LOG):
|
|
309
|
+
try:
|
|
310
|
+
os.remove(f)
|
|
311
|
+
except OSError:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
# Clean rate-limit locks
|
|
315
|
+
for name in os.listdir(_TMP):
|
|
316
|
+
if name.startswith("slm-obs-") or name.startswith("slm-recall-") or name.startswith("slm-learn-"):
|
|
317
|
+
try:
|
|
318
|
+
os.remove(os.path.join(_TMP, name))
|
|
319
|
+
except OSError:
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
sys.exit(0)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
# Helpers (stdlib only)
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
def _safe_hash(s: str) -> str:
|
|
330
|
+
"""Simple string hash for rate-limit lock file names."""
|
|
331
|
+
h = 0
|
|
332
|
+
for c in s:
|
|
333
|
+
h = (h * 31 + ord(c)) & 0xFFFFFFFF
|
|
334
|
+
return format(h, "08x")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _cooldown_elapsed(lock_file: str, interval: int, now: int) -> bool:
|
|
338
|
+
"""Check if enough time has passed since last timestamp in lock_file."""
|
|
339
|
+
try:
|
|
340
|
+
if os.path.exists(lock_file):
|
|
341
|
+
with open(lock_file) as f:
|
|
342
|
+
last = int(f.read().strip())
|
|
343
|
+
return (now - last) >= interval
|
|
344
|
+
except (ValueError, OSError):
|
|
345
|
+
pass
|
|
346
|
+
return True
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _write_timestamp(path: str, ts: int) -> None:
|
|
350
|
+
"""Write a unix timestamp to a file."""
|
|
351
|
+
try:
|
|
352
|
+
with open(path, "w") as f:
|
|
353
|
+
f.write(str(ts))
|
|
354
|
+
except OSError:
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _run_quiet(cmd: list[str], timeout: int = 5, postprocess=None) -> str:
|
|
359
|
+
"""Run a command quietly, return stdout or empty string."""
|
|
360
|
+
try:
|
|
361
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
362
|
+
out = result.stdout.strip()
|
|
363
|
+
if postprocess and out:
|
|
364
|
+
out = postprocess(out)
|
|
365
|
+
return out
|
|
366
|
+
except Exception:
|
|
367
|
+
return ""
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _maybe_consolidate() -> None:
|
|
371
|
+
"""Run cognitive consolidation if last run was >24h ago. Non-blocking."""
|
|
372
|
+
try:
|
|
373
|
+
last_ts = 0
|
|
374
|
+
if os.path.exists(_LAST_CONSOLIDATION):
|
|
375
|
+
with open(_LAST_CONSOLIDATION) as f:
|
|
376
|
+
last_ts = int(f.read().strip())
|
|
377
|
+
|
|
378
|
+
now = int(time.time())
|
|
379
|
+
if (now - last_ts) < 86400: # 24 hours
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
# Update timestamp FIRST to prevent concurrent runs
|
|
383
|
+
os.makedirs(os.path.dirname(_LAST_CONSOLIDATION), exist_ok=True)
|
|
384
|
+
with open(_LAST_CONSOLIDATION, "w") as f:
|
|
385
|
+
f.write(str(now))
|
|
386
|
+
|
|
387
|
+
# Run consolidation in background (don't block session end)
|
|
388
|
+
subprocess.Popen(
|
|
389
|
+
["slm", "consolidate", "--cognitive"],
|
|
390
|
+
stdout=subprocess.DEVNULL,
|
|
391
|
+
stderr=subprocess.DEVNULL,
|
|
392
|
+
)
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
@@ -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)
|