superlocalmemory 3.4.62 → 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,58 @@ 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
+
31
+ ## [3.4.63] - 2026-05-31 — Dashboard search: fix async blocking + fast mode
32
+
33
+ Fixes "signal is aborted without reason" in dashboard search (second root cause,
34
+ different from v3.4.61's WorkerPool fix).
35
+
36
+ ### Root Cause
37
+ `engine.recall()` is a synchronous blocking Python call (~2-10s). Calling it
38
+ directly inside an `async` FastAPI route blocks the ASGI event loop for the
39
+ full duration. Chrome detects a stalled HTTP connection (no response headers
40
+ being sent) and fires `controller.abort()` with no reason — producing the
41
+ "signal is aborted without reason" browser error, regardless of fetch timeout.
42
+
43
+ ### Fix
44
+ 1. `await loop.run_in_executor(None, lambda: engine.recall(...))` — offloads
45
+ the blocking call to a thread pool. Event loop stays alive to send HTTP
46
+ keepalive frames, preventing Chrome from aborting the connection.
47
+ 2. `fast=True` — skips spreading_activation + Hopfield channels, reducing
48
+ recall from 9.5s to <2s. These channels add precision for MCP/session_init
49
+ but are unnecessary for dashboard search results.
50
+
51
+ ### Result
52
+ Dashboard search: 919ms cold, 1.7s warm. Zero browser aborts.
53
+ Works immediately after `slm restart` — no wait needed.
54
+
55
+ ### Changed
56
+ - `server/routes/memories.py`: `search_memories` uses `run_in_executor` + `fast=True`
57
+
58
+ ---
59
+
8
60
  ## [3.4.62] - 2026-05-31 — Recall engine pre-warm on startup
9
61
 
10
62
  Adds a `recall-warmup` background thread that fires one full 6-channel recall
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.62",
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.62"
3
+ version = "3.4.64"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -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.62"
31
+ __version__ = "3.4.64"
32
32
 
33
33
  _REQUIRED_VERSIONS = {
34
34
  "sentence_transformers": "5.3.0",
@@ -412,12 +412,23 @@ async def search_memories(request: Request, body: SearchRequest):
412
412
  from superlocalmemory.core.recall_gate import begin_recall, end_recall
413
413
  begin_recall()
414
414
  try:
415
- # Use the daemon engine directly — already loaded, shares warm cache
415
+ # Use the daemon engine directly — already loaded, shares warm cache.
416
+ # v3.4.63: engine.recall() is synchronous/blocking (~2-10s). Calling it
417
+ # directly in an async route blocks the ASGI event loop — Chrome detects
418
+ # a stalled connection and aborts with "signal is aborted without reason"
419
+ # before the response arrives. Fix: run in a thread-pool executor so the
420
+ # event loop stays alive to send keepalive frames. Also fast=True skips
421
+ # spreading_activation + Hopfield (saves ~7s on cold graph traversal).
422
+ import asyncio
423
+ import time as _time
416
424
  engine = _get_engine(request)
417
425
  if engine is not None:
418
- import time as _time
426
+ loop = asyncio.get_event_loop()
419
427
  t0 = _time.monotonic()
420
- response = engine.recall(body.query, limit=body.limit)
428
+ response = await loop.run_in_executor(
429
+ None,
430
+ lambda: engine.recall(body.query, limit=body.limit, fast=True),
431
+ )
421
432
  elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
422
433
  results = []
423
434
  for r in response.results[: body.limit]:
@@ -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
- from superlocalmemory.core.worker_pool import WorkerPool
524
- pool = WorkerPool.shared()
525
- result = pool.recall(query, limit=limit)
526
-
527
- if not result.get("ok"):
528
- return JSONResponse(
529
- {"error": result.get("error", "Recall failed")},
530
- status_code=503,
531
- )
532
-
533
- # Optional: synthesize answer from results (Mode B/C only)
534
- synthesis = ""
535
- if body.get("synthesize") and result.get("results"):
536
- try:
537
- syn_result = pool.synthesize(query, result["results"][:5])
538
- synthesis = syn_result.get("synthesis", "") if syn_result.get("ok") else ""
539
- except Exception:
540
- pass
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, result.get("results", []))
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": result.get("query_type", "unknown"),
552
- "result_count": result.get("result_count", 0),
553
- "retrieval_time_ms": result.get("retrieval_time_ms", 0),
554
- "results": result.get("results", []),
555
- "synthesis": 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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.4.62
3
+ Version: 3.4.64
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: AGPL-3.0-or-later