superlocalmemory 3.8.7 → 3.8.8
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/CHANGELOG.md +34 -1
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/core/embeddings.py +73 -11
- package/src/superlocalmemory/core/engine.py +6 -1
- package/src/superlocalmemory/core/ollama_embedder.py +5 -0
- package/src/superlocalmemory/core/recall_gate.py +39 -4
- package/src/superlocalmemory/core/recall_pipeline.py +40 -0
- package/src/superlocalmemory/encoding/scene_builder.py +105 -15
- package/src/superlocalmemory/retrieval/entity_channel.py +201 -56
- package/src/superlocalmemory/retrieval/vector_store.py +238 -123
- package/src/superlocalmemory/server/recall_health.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +104 -16
- package/src/superlocalmemory/storage/embedding_migrator.py +88 -60
|
@@ -17,9 +17,10 @@ from __future__ import annotations
|
|
|
17
17
|
import logging
|
|
18
18
|
import sqlite3
|
|
19
19
|
import threading
|
|
20
|
+
from contextlib import contextmanager
|
|
20
21
|
from dataclasses import dataclass
|
|
21
22
|
from pathlib import Path
|
|
22
|
-
from typing import
|
|
23
|
+
from typing import Generator
|
|
23
24
|
|
|
24
25
|
import numpy as np
|
|
25
26
|
|
|
@@ -31,6 +32,7 @@ logger = logging.getLogger(__name__)
|
|
|
31
32
|
@dataclass(frozen=True) # Rule 10
|
|
32
33
|
class VectorStoreConfig:
|
|
33
34
|
"""Configuration for VectorStore."""
|
|
35
|
+
|
|
34
36
|
dimension: int = 768
|
|
35
37
|
binary_quantization_threshold: int = 100_000 # L4 fix
|
|
36
38
|
model_name: str = "nomic-embed-text-v1.5"
|
|
@@ -78,6 +80,7 @@ class VectorStore:
|
|
|
78
80
|
"""
|
|
79
81
|
try:
|
|
80
82
|
import sqlite_vec # noqa: F401
|
|
83
|
+
|
|
81
84
|
conn = self._connect()
|
|
82
85
|
conn.close()
|
|
83
86
|
return True
|
|
@@ -112,6 +115,26 @@ class VectorStore:
|
|
|
112
115
|
conn.enable_load_extension(False)
|
|
113
116
|
return conn
|
|
114
117
|
|
|
118
|
+
@contextmanager
|
|
119
|
+
def _managed_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
|
120
|
+
"""Close every sqlite-vec connection, rolling back abandoned writes.
|
|
121
|
+
|
|
122
|
+
sqlite-vec can reject a commit after its virtual-table shadow rows have
|
|
123
|
+
already opened a SQLite write transaction. A fail-soft caller must not
|
|
124
|
+
return while that connection still owns the WAL writer lock: the next
|
|
125
|
+
canonical/BM25 write would then wait behind an unreachable transaction.
|
|
126
|
+
"""
|
|
127
|
+
conn = self._connect()
|
|
128
|
+
try:
|
|
129
|
+
yield conn
|
|
130
|
+
finally:
|
|
131
|
+
if conn.in_transaction:
|
|
132
|
+
try:
|
|
133
|
+
conn.rollback()
|
|
134
|
+
except sqlite3.Error:
|
|
135
|
+
pass
|
|
136
|
+
conn.close()
|
|
137
|
+
|
|
115
138
|
# -- Table creation -----------------------------------------------------
|
|
116
139
|
|
|
117
140
|
def _ensure_vec0_table(self) -> None:
|
|
@@ -134,21 +157,18 @@ class VectorStore:
|
|
|
134
157
|
")"
|
|
135
158
|
)
|
|
136
159
|
meta_idx_fact = (
|
|
137
|
-
"CREATE INDEX IF NOT EXISTS idx_embmeta_fact "
|
|
138
|
-
"ON embedding_metadata (fact_id)"
|
|
160
|
+
"CREATE INDEX IF NOT EXISTS idx_embmeta_fact ON embedding_metadata (fact_id)"
|
|
139
161
|
)
|
|
140
162
|
meta_idx_profile = (
|
|
141
|
-
"CREATE INDEX IF NOT EXISTS idx_embmeta_profile "
|
|
142
|
-
"ON embedding_metadata (profile_id)"
|
|
163
|
+
"CREATE INDEX IF NOT EXISTS idx_embmeta_profile ON embedding_metadata (profile_id)"
|
|
143
164
|
)
|
|
144
165
|
try:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
conn.close()
|
|
166
|
+
with self._managed_connection() as conn:
|
|
167
|
+
conn.execute(vec0_ddl)
|
|
168
|
+
conn.execute(meta_ddl)
|
|
169
|
+
conn.execute(meta_idx_fact)
|
|
170
|
+
conn.execute(meta_idx_profile)
|
|
171
|
+
conn.commit()
|
|
152
172
|
except Exception as exc:
|
|
153
173
|
logger.debug("vec0 table creation failed: %s", exc)
|
|
154
174
|
self._available = False
|
|
@@ -180,7 +200,8 @@ class VectorStore:
|
|
|
180
200
|
if len(embedding) != self._config.dimension:
|
|
181
201
|
logger.debug(
|
|
182
202
|
"Dimension mismatch: got %d, expected %d",
|
|
183
|
-
len(embedding),
|
|
203
|
+
len(embedding),
|
|
204
|
+
self._config.dimension,
|
|
184
205
|
)
|
|
185
206
|
return False
|
|
186
207
|
|
|
@@ -196,46 +217,91 @@ class VectorStore:
|
|
|
196
217
|
# with db._lock: vs.upsert()
|
|
197
218
|
# simply re-enters the RLock (same thread — always safe).
|
|
198
219
|
_wl = get_write_lock(self._db_path)
|
|
199
|
-
with _wl:
|
|
220
|
+
with _wl: # OUTER: serialises all memory.db writers
|
|
200
221
|
with self._lock: # INNER: VectorStore per-instance state
|
|
201
222
|
try:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
223
|
+
with self._managed_connection() as conn:
|
|
224
|
+
# Reserve SQLite's cross-process writer before reading
|
|
225
|
+
# either side of the row-id allocator. The Python
|
|
226
|
+
# RLocks above coordinate threads only; BEGIN IMMEDIATE
|
|
227
|
+
# plus the connection's bounded busy_timeout serializes
|
|
228
|
+
# independent MCP/agent processes as well.
|
|
229
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
230
|
+
# Check if fact_id already exists in metadata
|
|
231
|
+
row = conn.execute(
|
|
232
|
+
"SELECT vec_rowid, profile_id "
|
|
233
|
+
"FROM embedding_metadata "
|
|
234
|
+
"WHERE fact_id = ?",
|
|
235
|
+
(fact_id,),
|
|
236
|
+
).fetchone()
|
|
237
|
+
|
|
238
|
+
if row is not None:
|
|
239
|
+
rowid = row["vec_rowid"]
|
|
240
|
+
vector_row = conn.execute(
|
|
241
|
+
"SELECT profile_id FROM fact_embeddings WHERE rowid = ?",
|
|
242
|
+
(rowid,),
|
|
243
|
+
).fetchone()
|
|
244
|
+
pair_matches_profile = (
|
|
245
|
+
vector_row is not None
|
|
246
|
+
and str(row["profile_id"]) == profile_id
|
|
247
|
+
and str(vector_row["profile_id"]) == profile_id
|
|
248
|
+
)
|
|
249
|
+
if pair_matches_profile:
|
|
250
|
+
conn.execute(
|
|
251
|
+
"UPDATE fact_embeddings SET embedding = ? WHERE rowid = ?",
|
|
252
|
+
(vec_bytes, rowid),
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
# Older self-heal code could insert metadata
|
|
256
|
+
# before sqlite-vec, or row-id drift could point
|
|
257
|
+
# metadata at another profile's vector. Neither
|
|
258
|
+
# is a valid projection pair. Remove only the
|
|
259
|
+
# stale pointer and rebuild at a fresh rowid;
|
|
260
|
+
# never overwrite the other profile's payload.
|
|
261
|
+
conn.execute(
|
|
262
|
+
"DELETE FROM embedding_metadata WHERE fact_id = ?",
|
|
263
|
+
(fact_id,),
|
|
264
|
+
)
|
|
265
|
+
row = None
|
|
266
|
+
|
|
267
|
+
if row is None:
|
|
268
|
+
# Allocate from both sides of the projection pair.
|
|
269
|
+
# Mature databases can contain orphaned vec0 rows
|
|
270
|
+
# or metadata rows after older fail-soft releases.
|
|
271
|
+
# sqlite-vec's implicit last_insert_rowid() only
|
|
272
|
+
# considers the virtual table, so it can reuse a
|
|
273
|
+
# rowid that is still owned by embedding_metadata
|
|
274
|
+
# and make every later projection fail UNIQUE.
|
|
275
|
+
rowid = conn.execute(
|
|
276
|
+
"SELECT COALESCE(MAX(candidate), 0) + 1 "
|
|
277
|
+
"FROM ("
|
|
278
|
+
"SELECT MAX(rowid) AS candidate "
|
|
279
|
+
"FROM fact_embeddings "
|
|
280
|
+
"UNION ALL "
|
|
281
|
+
"SELECT MAX(vec_rowid) AS candidate "
|
|
282
|
+
"FROM embedding_metadata"
|
|
283
|
+
")"
|
|
284
|
+
).fetchone()[0]
|
|
285
|
+
conn.execute(
|
|
286
|
+
"INSERT INTO fact_embeddings"
|
|
287
|
+
"(rowid, profile_id, embedding) "
|
|
288
|
+
"VALUES (?, ?, ?)",
|
|
289
|
+
(rowid, profile_id, vec_bytes),
|
|
290
|
+
)
|
|
291
|
+
conn.execute(
|
|
292
|
+
"INSERT INTO embedding_metadata "
|
|
293
|
+
"(vec_rowid, fact_id, profile_id, model_name, dimension) "
|
|
294
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
295
|
+
(
|
|
296
|
+
rowid,
|
|
297
|
+
fact_id,
|
|
298
|
+
profile_id,
|
|
299
|
+
model_name or self._config.model_name,
|
|
300
|
+
self._config.dimension,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
conn.commit()
|
|
239
305
|
return True
|
|
240
306
|
except Exception as exc:
|
|
241
307
|
logger.debug("upsert failed for fact_id=%s: %s", fact_id, exc)
|
|
@@ -261,48 +327,64 @@ class VectorStore:
|
|
|
261
327
|
vec_bytes = self._serialize_f32(query_embedding)
|
|
262
328
|
|
|
263
329
|
try:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
330
|
+
with self._managed_connection() as conn:
|
|
331
|
+
if top_k <= 0:
|
|
332
|
+
return []
|
|
333
|
+
if profile_id is not None:
|
|
334
|
+
sql = (
|
|
335
|
+
"SELECT fe.rowid, fe.distance, em.fact_id "
|
|
336
|
+
"FROM fact_embeddings AS fe "
|
|
337
|
+
"JOIN embedding_metadata AS em "
|
|
338
|
+
"ON em.vec_rowid = fe.rowid "
|
|
339
|
+
"AND em.profile_id = fe.profile_id "
|
|
340
|
+
"WHERE fe.embedding MATCH ? "
|
|
341
|
+
"AND fe.profile_id = ? "
|
|
342
|
+
"AND fe.k = ?"
|
|
343
|
+
)
|
|
344
|
+
base_params: tuple[object, ...] = (vec_bytes, profile_id)
|
|
345
|
+
count_sql = "SELECT COUNT(*) AS c FROM fact_embeddings WHERE profile_id = ?"
|
|
346
|
+
count_params: tuple[object, ...] = (profile_id,)
|
|
347
|
+
else:
|
|
348
|
+
sql = (
|
|
349
|
+
"SELECT fe.rowid, fe.distance, em.fact_id "
|
|
350
|
+
"FROM fact_embeddings AS fe "
|
|
351
|
+
"JOIN embedding_metadata AS em "
|
|
352
|
+
"ON em.vec_rowid = fe.rowid "
|
|
353
|
+
"AND em.profile_id = fe.profile_id "
|
|
354
|
+
"WHERE fe.embedding MATCH ? "
|
|
355
|
+
"AND fe.k = ?"
|
|
356
|
+
)
|
|
357
|
+
base_params = (vec_bytes,)
|
|
358
|
+
count_sql = "SELECT COUNT(*) AS c FROM fact_embeddings"
|
|
359
|
+
count_params = ()
|
|
360
|
+
|
|
361
|
+
# vec0 applies k before the relational join. A legacy orphan
|
|
362
|
+
# can therefore occupy a nearest-neighbour slot and then be
|
|
363
|
+
# discarded by the profile-safe join. Expand only when that
|
|
364
|
+
# happens, doubling until top_k valid pairs are found or the
|
|
365
|
+
# profile's vector population is exhausted.
|
|
366
|
+
search_k = top_k
|
|
276
367
|
rows = conn.execute(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"WHERE embedding MATCH ? "
|
|
280
|
-
"AND k = ?",
|
|
281
|
-
(vec_bytes, top_k),
|
|
368
|
+
sql,
|
|
369
|
+
(*base_params, search_k),
|
|
282
370
|
).fetchall()
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
f"WHERE vec_rowid IN ({placeholders})",
|
|
296
|
-
rowids,
|
|
297
|
-
).fetchall()
|
|
298
|
-
|
|
299
|
-
conn.close()
|
|
371
|
+
if len(rows) < top_k:
|
|
372
|
+
count_row = conn.execute(
|
|
373
|
+
count_sql,
|
|
374
|
+
count_params,
|
|
375
|
+
).fetchone()
|
|
376
|
+
total_vectors = int(count_row["c"]) if count_row else 0
|
|
377
|
+
while len(rows) < top_k and search_k < total_vectors:
|
|
378
|
+
search_k = min(total_vectors, max(search_k + 1, search_k * 2))
|
|
379
|
+
rows = conn.execute(
|
|
380
|
+
sql,
|
|
381
|
+
(*base_params, search_k),
|
|
382
|
+
).fetchall()
|
|
300
383
|
|
|
301
384
|
results: list[tuple[str, float]] = []
|
|
302
|
-
for
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
similarity = max(0.0, 1.0 - dist_map[rid])
|
|
385
|
+
for row in rows[:top_k]:
|
|
386
|
+
fid = str(row["fact_id"])
|
|
387
|
+
similarity = max(0.0, 1.0 - row["distance"])
|
|
306
388
|
results.append((fid, similarity))
|
|
307
389
|
|
|
308
390
|
results.sort(key=lambda x: x[1], reverse=True)
|
|
@@ -322,38 +404,44 @@ class VectorStore:
|
|
|
322
404
|
return False
|
|
323
405
|
|
|
324
406
|
_wl = get_write_lock(self._db_path)
|
|
325
|
-
with _wl:
|
|
407
|
+
with _wl: # OUTER: process-level write serialisation
|
|
326
408
|
with self._lock: # INNER: VectorStore per-instance state
|
|
327
409
|
try:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
410
|
+
with self._managed_connection() as conn:
|
|
411
|
+
row = conn.execute(
|
|
412
|
+
"SELECT vec_rowid, profile_id "
|
|
413
|
+
"FROM embedding_metadata "
|
|
414
|
+
"WHERE fact_id = ?",
|
|
415
|
+
(fact_id,),
|
|
416
|
+
).fetchone()
|
|
334
417
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return False
|
|
418
|
+
if row is None:
|
|
419
|
+
return False
|
|
338
420
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
421
|
+
rowid = row["vec_rowid"]
|
|
422
|
+
vector_row = conn.execute(
|
|
423
|
+
"SELECT profile_id FROM fact_embeddings WHERE rowid = ?",
|
|
424
|
+
(rowid,),
|
|
425
|
+
).fetchone()
|
|
426
|
+
if vector_row is not None and str(vector_row["profile_id"]) == str(
|
|
427
|
+
row["profile_id"]
|
|
428
|
+
):
|
|
429
|
+
conn.execute(
|
|
430
|
+
"DELETE FROM fact_embeddings WHERE rowid = ?",
|
|
431
|
+
(rowid,),
|
|
432
|
+
)
|
|
433
|
+
conn.execute(
|
|
434
|
+
"DELETE FROM embedding_metadata WHERE vec_rowid = ?",
|
|
435
|
+
(rowid,),
|
|
436
|
+
)
|
|
437
|
+
conn.commit()
|
|
350
438
|
return True
|
|
351
439
|
except Exception as exc:
|
|
352
440
|
logger.debug("delete failed for fact_id=%s: %s", fact_id, exc)
|
|
353
441
|
return False
|
|
354
442
|
|
|
355
443
|
def count(self, profile_id: str | None = None) -> int:
|
|
356
|
-
"""Count
|
|
444
|
+
"""Count complete metadata/vector pairs in the store.
|
|
357
445
|
|
|
358
446
|
Returns 0 if unavailable.
|
|
359
447
|
"""
|
|
@@ -361,23 +449,50 @@ class VectorStore:
|
|
|
361
449
|
return 0
|
|
362
450
|
|
|
363
451
|
try:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
452
|
+
with self._managed_connection() as conn:
|
|
453
|
+
if profile_id is not None:
|
|
454
|
+
row = conn.execute(
|
|
455
|
+
"SELECT COUNT(*) AS c "
|
|
456
|
+
"FROM embedding_metadata em "
|
|
457
|
+
"JOIN fact_embeddings fe "
|
|
458
|
+
"ON fe.rowid = em.vec_rowid "
|
|
459
|
+
"AND fe.profile_id = em.profile_id "
|
|
460
|
+
"WHERE em.profile_id = ?",
|
|
461
|
+
(profile_id,),
|
|
462
|
+
).fetchone()
|
|
463
|
+
else:
|
|
464
|
+
row = conn.execute(
|
|
465
|
+
"SELECT COUNT(*) AS c "
|
|
466
|
+
"FROM embedding_metadata em "
|
|
467
|
+
"JOIN fact_embeddings fe "
|
|
468
|
+
"ON fe.rowid = em.vec_rowid "
|
|
469
|
+
"AND fe.profile_id = em.profile_id",
|
|
470
|
+
).fetchone()
|
|
376
471
|
return int(row["c"]) if row else 0
|
|
377
472
|
except Exception as exc:
|
|
378
473
|
logger.debug("count failed: %s", exc)
|
|
379
474
|
return 0
|
|
380
475
|
|
|
476
|
+
def indexed_fact_ids(self, profile_id: str) -> set[str]:
|
|
477
|
+
"""Return fact IDs backed by both metadata and a vec0 payload."""
|
|
478
|
+
if not self._available:
|
|
479
|
+
return set()
|
|
480
|
+
try:
|
|
481
|
+
with self._managed_connection() as conn:
|
|
482
|
+
rows = conn.execute(
|
|
483
|
+
"SELECT em.fact_id "
|
|
484
|
+
"FROM embedding_metadata em "
|
|
485
|
+
"JOIN fact_embeddings fe "
|
|
486
|
+
"ON fe.rowid = em.vec_rowid "
|
|
487
|
+
"AND fe.profile_id = em.profile_id "
|
|
488
|
+
"WHERE em.profile_id = ?",
|
|
489
|
+
(profile_id,),
|
|
490
|
+
).fetchall()
|
|
491
|
+
return {str(row["fact_id"]) for row in rows}
|
|
492
|
+
except Exception as exc:
|
|
493
|
+
logger.debug("indexed_fact_ids failed: %s", exc)
|
|
494
|
+
return set()
|
|
495
|
+
|
|
381
496
|
def rebuild_from_facts(
|
|
382
497
|
self,
|
|
383
498
|
facts: list[tuple[str, str, list[float]]],
|
|
@@ -153,7 +153,9 @@ def run_health_tick(engine, state: RecallHealth, *, probe: str = DEFAULT_PROBE,
|
|
|
153
153
|
# fast=True: a health probe must release its operation lease well
|
|
154
154
|
# within the 5s profile-switch drain window (fast=False is 2-10s and
|
|
155
155
|
# would make every profile switch time out while a tick is in flight).
|
|
156
|
-
|
|
156
|
+
from superlocalmemory.core.recall_gate import background_work
|
|
157
|
+
with background_work():
|
|
158
|
+
resp = engine.recall(probe, limit=3, fast=True)
|
|
157
159
|
except Exception as exc:
|
|
158
160
|
state.healthy = False
|
|
159
161
|
state.consecutive_failures += 1
|