superlocalmemory 3.4.63 → 3.4.64
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
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.4.64] - 2026-05-31 — Fix recall/trace endpoint (Recall Lab search)
|
|
9
|
+
|
|
10
|
+
The dashboard "Search memories" button (Recall Lab) calls `POST /api/v3/recall/trace`,
|
|
11
|
+
NOT `POST /api/search`. v3.4.63 fixed the wrong endpoint. This is the real fix.
|
|
12
|
+
|
|
13
|
+
### Root Cause
|
|
14
|
+
`recall_trace()` called `WorkerPool.shared().recall()` — subprocess worker pool
|
|
15
|
+
that blocks the ASGI event loop and crashes (`Worker died`) after ~17s. The
|
|
16
|
+
15s global fetch timeout in core.js fired first, aborting with "signal is aborted
|
|
17
|
+
without reason".
|
|
18
|
+
|
|
19
|
+
### Fix
|
|
20
|
+
Same pattern as v3.4.63: `run_in_executor` + daemon engine + `fast=True`.
|
|
21
|
+
Synthesis removed (was using the crashed subprocess anyway).
|
|
22
|
+
|
|
23
|
+
### Result
|
|
24
|
+
recall/trace: 7.2s cold, 1.1s warm. Zero browser aborts. No more "Worker died".
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `server/routes/v3_api.py`: `recall_trace` uses `run_in_executor` + daemon engine
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
8
31
|
## [3.4.63] - 2026-05-31 — Dashboard search: fix async blocking + fast mode
|
|
9
32
|
|
|
10
33
|
Fixes "signal is aborted without reason" in dashboard search (second root cause,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.64",
|
|
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
|
@@ -28,7 +28,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
28
28
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
29
29
|
# ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
__version__ = "3.4.
|
|
31
|
+
__version__ = "3.4.64"
|
|
32
32
|
|
|
33
33
|
_REQUIRED_VERSIONS = {
|
|
34
34
|
"sentence_transformers": "5.3.0",
|
|
@@ -514,45 +514,60 @@ async def set_provider(request: Request):
|
|
|
514
514
|
|
|
515
515
|
@router.post("/recall/trace")
|
|
516
516
|
async def recall_trace(request: Request):
|
|
517
|
-
"""Recall with per-channel score breakdown.
|
|
517
|
+
"""Recall with per-channel score breakdown.
|
|
518
|
+
|
|
519
|
+
v3.4.64: Replaced WorkerPool.shared() (subprocess, blocks event loop,
|
|
520
|
+
worker crashes after ~15s) with daemon engine via run_in_executor.
|
|
521
|
+
Same fix as POST /api/search in v3.4.63.
|
|
522
|
+
"""
|
|
523
|
+
import asyncio
|
|
524
|
+
import time as _time
|
|
518
525
|
try:
|
|
519
526
|
body = await request.json()
|
|
520
527
|
query = body.get("query", "")
|
|
521
528
|
limit = body.get("limit", 10)
|
|
522
529
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if
|
|
528
|
-
return JSONResponse(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
530
|
+
# Use daemon engine — already loaded, shares warm page cache.
|
|
531
|
+
# run_in_executor keeps event loop alive so browser doesn't abort.
|
|
532
|
+
from .helpers import get_engine_lazy
|
|
533
|
+
engine = get_engine_lazy(request.app.state)
|
|
534
|
+
if engine is None:
|
|
535
|
+
return JSONResponse({"error": "Engine not initialised"}, status_code=503)
|
|
536
|
+
|
|
537
|
+
loop = asyncio.get_event_loop()
|
|
538
|
+
t0 = _time.monotonic()
|
|
539
|
+
response = await loop.run_in_executor(
|
|
540
|
+
None,
|
|
541
|
+
lambda: engine.recall(query, limit=limit, fast=True),
|
|
542
|
+
)
|
|
543
|
+
elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
|
|
544
|
+
|
|
545
|
+
results = []
|
|
546
|
+
for r in response.results[:limit]:
|
|
547
|
+
results.append({
|
|
548
|
+
"fact_id": r.fact.fact_id,
|
|
549
|
+
"memory_id": getattr(r.fact, "memory_id", ""),
|
|
550
|
+
"content": r.fact.content[:300],
|
|
551
|
+
"score": round(r.score, 4),
|
|
552
|
+
"confidence": round(getattr(r, "confidence", 0.0), 4),
|
|
553
|
+
"channel_scores": getattr(r, "channel_scores", {}),
|
|
554
|
+
"created_at": getattr(r.fact, "created_at", ""),
|
|
555
|
+
})
|
|
541
556
|
|
|
542
557
|
# Record learning signals (non-blocking, non-critical)
|
|
543
558
|
try:
|
|
544
|
-
_record_learning_signals(query,
|
|
559
|
+
_record_learning_signals(query, results)
|
|
545
560
|
except Exception as _sig_exc:
|
|
546
561
|
import logging as _log
|
|
547
562
|
_log.getLogger(__name__).warning("Learning signal error: %s", _sig_exc)
|
|
548
563
|
|
|
549
564
|
return {
|
|
550
565
|
"query": query,
|
|
551
|
-
"query_type":
|
|
552
|
-
"result_count":
|
|
553
|
-
"retrieval_time_ms":
|
|
554
|
-
"results":
|
|
555
|
-
"synthesis":
|
|
566
|
+
"query_type": getattr(response, "query_type", "semantic"),
|
|
567
|
+
"result_count": len(results),
|
|
568
|
+
"retrieval_time_ms": elapsed_ms,
|
|
569
|
+
"results": results,
|
|
570
|
+
"synthesis": "",
|
|
556
571
|
}
|
|
557
572
|
except Exception as e:
|
|
558
573
|
return JSONResponse({"error": str(e)}, status_code=500)
|