superlocalmemory 3.8.2 → 3.8.3

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +1 -1
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/server/routes/memories.py +72 -23
  34. package/src/superlocalmemory/server/unified_daemon.py +83 -8
  35. package/src/superlocalmemory/ui/js/core.js +6 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ 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.8.3] - 2026-07-24 — Recall stays responsive under heavy load
9
+
10
+ ### Fixed
11
+
12
+ - **Recall no longer hangs when the system is busy.** Search and recall now
13
+ complete within a generous time budget even during background maintenance or
14
+ when many agents are querying at once. If a query can't finish in that window,
15
+ SLM returns keyword-matched results and marks them clearly instead of leaving
16
+ the request to time out. This applies everywhere recall runs — the dashboard
17
+ search, the CLI, and connected assistants — so results are consistent across
18
+ every surface.
19
+ - **Dashboard search waits long enough for a real answer.** The dashboard now
20
+ allows recall the full server-side budget before giving up, so heavy-load
21
+ queries return results rather than an aborted-request error.
22
+
8
23
  ## [3.8.2] - 2026-07-24 — Self-healing upgrades & faster, consistent recall
9
24
 
10
25
  ### Added
package/README.md CHANGED
@@ -5,10 +5,10 @@
5
5
  </picture>
6
6
  </p>
7
7
 
8
- <h1 align="center">SuperLocalMemory V3.8.2</h1>
8
+ <h1 align="center">SuperLocalMemory V3.8.3</h1>
9
9
  <p align="center"><strong>Enterprise-grade, local-first memory for AI agents and teams.</strong><br/>
10
10
  <em>A persistent, auditable long-term brain for your agents that runs on your own infrastructure — with multi-workspace isolation, role-based access, and GDPR + EU AI Act governance controls built in.</em></p>
11
- <p align="center"><code>v3.8.2</code> — one control plane: auditable retrieval · multi-scope memory (personal / shared / global) · Cache · Compress · trusted-peer Mesh · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
11
+ <p align="center"><code>v3.8.3</code> — one control plane: auditable retrieval · multi-scope memory (personal / shared / global) · Cache · Compress · trusted-peer Mesh · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
12
12
  Proxy: <code>slm wrap claude</code> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; Skill: zero-config</p>
13
13
  <p align="center"><strong>3 public research preprints</strong> (arXiv + Zenodo archives) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.8.2",
3
+ "version": "3.8.3",
4
4
  "description": "Local-first agent memory with MCP and an agent-native CLI. Documented clients include Claude Code, Cursor, and Windsurf.",
5
5
  "keywords": [
6
6
  "ai-memory",
@@ -15,5 +15,5 @@
15
15
  "mcpServers": "./.mcp.json",
16
16
  "name": "superlocalmemory",
17
17
  "repository": "https://github.com/qualixar/superlocalmemory",
18
- "version": "3.8.2"
18
+ "version": "3.8.3"
19
19
  }
package/plugin/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v3.8.2 -->
1
+ <!-- BEGIN SuperLocalMemory v3.8.3 -->
2
2
 
3
3
  ## SuperLocalMemory (SLM) — Agent Rules
4
4
 
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
39
39
  ### Subagents
40
40
  slm-memory-advisor (memory decisions, session hygiene, scope/profile guidance) · slm-optimize-advisor (context compression + KV cache) · slm-governance-advisor (scope/roles/compliance/GDPR)
41
41
 
42
- <!-- END SuperLocalMemory v3.8.2 -->
42
+ <!-- END SuperLocalMemory v3.8.3 -->
43
43
 
44
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -77,4 +77,4 @@ slm-scope · slm-governance · slm-profile · slm-remember · slm-recall
77
77
  # What NOT to do
78
78
  Never session_init twice; never forget without dry-run preview; never store secrets; never bypass role checks; never claim an erasure succeeded without verifying via recall.
79
79
 
80
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
80
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -68,4 +68,4 @@ assessment. The gate is the authority.
68
68
 
69
69
  ---
70
70
 
71
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
71
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
46
46
  # What NOT to do
47
47
  Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit; never bypass scope or governance restrictions.
48
48
 
49
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
49
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
41
41
  # What NOT to do
42
42
  Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %; never carry ccr_ids across profile switches.
43
43
 
44
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -1 +1 @@
1
- superlocalmemory==3.8.2
1
+ superlocalmemory==3.8.3
@@ -145,4 +145,4 @@ These subcommands control daemon-level cache settings. They do not read or write
145
145
 
146
146
  ---
147
147
 
148
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -147,4 +147,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
147
147
 
148
148
  ---
149
149
 
150
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -245,4 +245,4 @@ Before running any destructive operation (`forget`, `compact_memories`):
245
245
 
246
246
  ---
247
247
 
248
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
248
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -311,4 +311,4 @@ profile. See `slm-profile` for the full profile switching workflow.
311
311
 
312
312
  ---
313
313
 
314
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
314
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -96,4 +96,4 @@ paused, name the approval needed; when errored, quote the short detail.
96
96
 
97
97
  ---
98
98
 
99
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
99
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -279,4 +279,4 @@ mesh availability.
279
279
 
280
280
  ---
281
281
 
282
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
282
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -145,4 +145,4 @@ Name them differently in your MCP config (e.g. `superlocalmemory-personal` and
145
145
 
146
146
  ---
147
147
 
148
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
148
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -236,4 +236,4 @@ before recalling, then switch back. See `slm-profile` for workspace switching.
236
236
 
237
237
  ---
238
238
 
239
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
239
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -238,4 +238,4 @@ different workspace, use `switch_profile` first. See `slm-profile`.
238
238
 
239
239
  ---
240
240
 
241
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
241
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -173,4 +173,4 @@ to review the impact. See `slm-remember` for the full deletion discipline.
173
173
 
174
174
  ---
175
175
 
176
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
176
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -227,4 +227,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
227
227
 
228
228
  ---
229
229
 
230
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
230
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -163,4 +163,4 @@ multi-profile setup. To switch the active profile, see `slm-profile`.
163
163
 
164
164
  ---
165
165
 
166
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -128,4 +128,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
128
128
  - **slm-optimize-advisor** — context compression and KV cache
129
129
  - **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
130
130
 
131
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
131
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -145,4 +145,4 @@ These subcommands control daemon-level cache settings. They do not read or write
145
145
 
146
146
  ---
147
147
 
148
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -147,4 +147,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
147
147
 
148
148
  ---
149
149
 
150
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -311,4 +311,4 @@ profile. See `slm-profile` for the full profile switching workflow.
311
311
 
312
312
  ---
313
313
 
314
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
314
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
@@ -236,4 +236,4 @@ before recalling, then switch back. See `slm-profile` for workspace switching.
236
236
 
237
237
  ---
238
238
 
239
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
239
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -238,4 +238,4 @@ different workspace, use `switch_profile` first. See `slm-profile`.
238
238
 
239
239
  ---
240
240
 
241
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
241
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -227,4 +227,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
227
227
 
228
228
  ---
229
229
 
230
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
230
+ *SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later*
@@ -163,4 +163,4 @@ multi-profile setup. To switch the active profile, see `slm-profile`.
163
163
 
164
164
  ---
165
165
 
166
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v3.8.3 · Qualixar · AGPL-3.0-or-later
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.8.2"
3
+ version = "3.8.3"
4
4
  description = "Local-first agent memory with auditable hybrid retrieval"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
32
32
  os.environ["OMP_NUM_THREADS"] = "2"
33
33
  # ---------------------------------------------------------------------------
34
34
 
35
- __version__ = "3.8.2"
35
+ __version__ = "3.8.3"
36
36
 
37
37
  _REQUIRED_VERSIONS = {
38
38
  "sentence_transformers": "5.3.0",
@@ -19,6 +19,25 @@ from .helpers import (
19
19
  logger = logging.getLogger("superlocalmemory.routes.memories")
20
20
  router = APIRouter()
21
21
 
22
+ # v3.8.3: GENEROUS latency budget for recall. SLM's value is quality recall
23
+ # under heavy multi-agent load, so semantic recall is given ample time to
24
+ # finish — the keyword fallback is a LAST-RESORT safety net for a genuine hang
25
+ # (e.g. a wedged embedder), NOT an aggressive speed cutoff. Only if recall
26
+ # exceeds this budget do we serve the fast keyword search so the caller ALWAYS
27
+ # gets a result instead of hanging forever. Tune with SLM_SEARCH_RECALL_TIMEOUT_S.
28
+ # The dashboard's fetch timeout is set ABOVE this so the browser waits for the
29
+ # quality result rather than aborting early.
30
+ _DEFAULT_RECALL_BUDGET_S = 25.0
31
+
32
+
33
+ def _search_recall_timeout_s() -> float:
34
+ import os
35
+ try:
36
+ v = float(os.environ.get("SLM_SEARCH_RECALL_TIMEOUT_S", ""))
37
+ return v if v > 0 else _DEFAULT_RECALL_BUDGET_S
38
+ except (TypeError, ValueError):
39
+ return _DEFAULT_RECALL_BUDGET_S
40
+
22
41
 
23
42
  def _internal_error(detail: str = "Internal server error") -> HTTPException:
24
43
  """SEC-H-02: log the full traceback server-side; return a generic message.
@@ -526,35 +545,65 @@ async def search_memories(request: Request, body: SearchRequest):
526
545
  _window = getattr(body, "window", None) or ""
527
546
  if not _window and getattr(body, "date_from", None) and getattr(body, "date_to", None):
528
547
  _window = f"{body.date_from}..{body.date_to}"
529
- response = await loop.run_in_executor(
548
+ # v3.8.3: bound the synchronous recall. Under a concurrent
549
+ # maintenance pass or a busy embedder it can run tens of seconds
550
+ # and the browser aborts the fetch. If it exceeds the budget we
551
+ # fall through to the fast keyword search below, so the dashboard
552
+ # ALWAYS returns instead of failing with an abort.
553
+ _recall_future = loop.run_in_executor(
530
554
  None,
531
555
  lambda: engine.recall(
532
556
  body.query, limit=body.limit, fast=True,
533
557
  window=_window or None,
534
558
  ),
535
559
  )
536
- elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
537
- from superlocalmemory.server.recall_serializer import (
538
- recall_response_metadata,
539
- serialize_recall_response,
540
- )
541
- results, no_confident_match = serialize_recall_response(
542
- response,
543
- limit=body.limit,
544
- per_fact_max=300,
545
- total_max=max(300, body.limit * 300),
546
- )
547
- return {
548
- "query": body.query,
549
- "results": results,
550
- "total": len(results),
551
- "query_type": getattr(response, "query_type", "semantic"),
552
- "retrieval_time_ms": elapsed_ms,
553
- "no_confident_match": no_confident_match,
554
- **recall_response_metadata(response),
555
- }
556
-
557
- # Fallback: direct DB text search (engine not yet initialised)
560
+ # A run_in_executor thread cannot be cancelled, and wait_for() on it
561
+ # blocks until the thread finishes (defeating the timeout). So poll
562
+ # the future without blocking the event loop and give up at the
563
+ # deadline — the orphaned recall completes in the background and its
564
+ # result is discarded. This is what bounds dashboard-search latency.
565
+ _budget = _search_recall_timeout_s()
566
+ _deadline = loop.time() + _budget
567
+ while not _recall_future.done() and loop.time() < _deadline:
568
+ await asyncio.sleep(0.05)
569
+ if _recall_future.done():
570
+ response = _recall_future.result()
571
+ else:
572
+ # Ensure the orphaned future's eventual result/exception is
573
+ # retrieved so asyncio doesn't log "never retrieved".
574
+ _recall_future.add_done_callback(
575
+ lambda f: (f.cancelled() or f.exception())
576
+ )
577
+ logger.warning(
578
+ "search_memories: semantic recall exceeded %.0fs budget for "
579
+ "%r — serving keyword fallback",
580
+ _budget, (body.query or "")[:80],
581
+ )
582
+ response = None
583
+ if response is not None:
584
+ elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
585
+ from superlocalmemory.server.recall_serializer import (
586
+ recall_response_metadata,
587
+ serialize_recall_response,
588
+ )
589
+ results, no_confident_match = serialize_recall_response(
590
+ response,
591
+ limit=body.limit,
592
+ per_fact_max=300,
593
+ total_max=max(300, body.limit * 300),
594
+ )
595
+ return {
596
+ "query": body.query,
597
+ "results": results,
598
+ "total": len(results),
599
+ "query_type": getattr(response, "query_type", "semantic"),
600
+ "retrieval_time_ms": elapsed_ms,
601
+ "no_confident_match": no_confident_match,
602
+ **recall_response_metadata(response),
603
+ }
604
+ # recall timed out — fall through to the fast keyword search.
605
+
606
+ # Fallback: direct DB text search (engine not ready OR recall over budget)
558
607
  conn = get_db_connection()
559
608
  conn.row_factory = dict_factory
560
609
  cursor = conn.cursor()
@@ -453,6 +453,61 @@ def _emit_event(
453
453
  import asyncio as _asyncio
454
454
  _recall_semaphore = _asyncio.Semaphore(3)
455
455
 
456
+
457
+ def _recall_budget_s() -> float:
458
+ """Generous latency budget for a recall before the keyword fallback (v3.8.3).
459
+
460
+ SLM's value is quality recall under heavy multi-agent load, so semantic
461
+ recall is given ample time; the keyword fallback is a LAST-RESORT safety
462
+ net for a genuine hang (e.g. a wedged embedder), not a speed cutoff. Tune
463
+ with SLM_SEARCH_RECALL_TIMEOUT_S (shared with the dashboard search route).
464
+ """
465
+ import os
466
+ try:
467
+ v = float(os.environ.get("SLM_SEARCH_RECALL_TIMEOUT_S", ""))
468
+ return v if v > 0 else 25.0
469
+ except (TypeError, ValueError):
470
+ return 25.0
471
+
472
+
473
+ def _recall_keyword_fallback(engine, query: str, limit: int) -> dict:
474
+ """Fast profile-scoped keyword (LIKE) fallback for /recall.
475
+
476
+ Used only when semantic recall exceeds its budget, so CLI/MCP callers get
477
+ a bounded response instead of hanging. Mirrors the dashboard /api/search
478
+ fallback shape (retrieval_mode=degraded_lexical).
479
+ """
480
+ results = []
481
+ try:
482
+ rows = engine._db.execute(
483
+ "SELECT fact_id, content, confidence FROM atomic_facts "
484
+ "WHERE profile_id = ? AND content LIKE ? "
485
+ "ORDER BY confidence DESC LIMIT ?",
486
+ (engine.profile_id, f"%{query}%", limit),
487
+ )
488
+ for pos, r in enumerate(rows, start=1):
489
+ d = dict(r)
490
+ results.append({
491
+ "fact_id": d.get("fact_id"),
492
+ "content": (d.get("content") or "")[:2400],
493
+ "score": None, "relevance_score": None, "ranking_score": None,
494
+ "confidence": d.get("confidence"),
495
+ "rank_position": pos,
496
+ })
497
+ except Exception as exc:
498
+ logger.warning("recall keyword fallback failed (non-fatal): %s", exc)
499
+ return {
500
+ "ok": True,
501
+ "query": query,
502
+ "query_type": "text_search",
503
+ "retrieval_mode": "degraded_lexical",
504
+ "degraded_reason": "recall_budget_exceeded",
505
+ "result_count": len(results),
506
+ "results": results,
507
+ "count": len(results),
508
+ "no_confident_match": True,
509
+ }
510
+
456
511
  # v3.4.52: Embedding model warm state. Set to True by the async pre-warm
457
512
  # thread once Ollama has loaded the embedding model. /health reports this
458
513
  # so MCP clients can wait for warm state before issuing recall calls.
@@ -3013,15 +3068,35 @@ def _register_daemon_routes(application: FastAPI) -> None:
3013
3068
  if not fast:
3014
3069
  await _recall_semaphore.acquire()
3015
3070
  try:
3016
- response = await asyncio.to_thread(
3017
- engine.recall,
3018
- search_query, limit=limit, session_id=effective_sid,
3019
- agent_id=recall_actor,
3020
- fast=fast,
3021
- include_global=include_global,
3022
- include_shared=include_shared,
3023
- window=window or None,
3071
+ # v3.8.3: bound the recall so CLI/MCP callers never hang on a
3072
+ # wedged embedder. Poll the executor future (which cannot be
3073
+ # cancelled) without blocking the loop, and give quality recall a
3074
+ # GENEROUS budget; only if it is exceeded do we serve the fast
3075
+ # keyword fallback. The orphaned recall finishes in the background.
3076
+ loop = asyncio.get_running_loop()
3077
+ _rf = loop.run_in_executor(
3078
+ None,
3079
+ lambda: engine.recall(
3080
+ search_query, limit=limit, session_id=effective_sid,
3081
+ agent_id=recall_actor,
3082
+ fast=fast,
3083
+ include_global=include_global,
3084
+ include_shared=include_shared,
3085
+ window=window or None,
3086
+ ),
3024
3087
  )
3088
+ _budget = _recall_budget_s()
3089
+ _deadline = loop.time() + _budget
3090
+ while not _rf.done() and loop.time() < _deadline:
3091
+ await asyncio.sleep(0.05)
3092
+ if not _rf.done():
3093
+ _rf.add_done_callback(lambda f: (f.cancelled() or f.exception()))
3094
+ logger.warning(
3095
+ "recall: semantic recall exceeded %.0fs budget for %r — "
3096
+ "serving keyword fallback", _budget, (search_query or "")[:80],
3097
+ )
3098
+ return _recall_keyword_fallback(engine, search_query, limit)
3099
+ response = _rf.result()
3025
3100
  # v3.4.26: return the same field shape as recall_worker so
3026
3101
  # MCP processes proxying through the daemon get recall_trace-
3027
3102
  # compatible data without a second round trip.
@@ -13,7 +13,12 @@
13
13
  // surfaces as a normal rejection and the UI can show a clear error.
14
14
  // ============================================================================
15
15
 
16
- window.SLM_FETCH_TIMEOUT_MS = 15000;
16
+ // v3.8.3: 30s (> the server's 25s recall budget) so the browser waits for a
17
+ // quality recall under heavy multi-agent load instead of aborting it early
18
+ // ("signal is aborted without reason"). The server self-bounds recall and
19
+ // falls back to keyword within its budget, so this only ever waits longer for
20
+ // a genuinely stuck request. Per-call init.timeoutMs still overrides.
21
+ window.SLM_FETCH_TIMEOUT_MS = 30000;
17
22
  window.SLM_INSTALL_TOKEN_KEY = 'slm_install_token';
18
23
 
19
24
  // B2 (3.7.9): the install token is kept in a private closure, never in