superlocalmemory 3.4.25 → 3.4.31

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 (55) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +8 -1
  3. package/package.json +1 -1
  4. package/pyproject.toml +3 -1
  5. package/src/superlocalmemory/__init__.py +1 -1
  6. package/src/superlocalmemory/cli/daemon.py +90 -16
  7. package/src/superlocalmemory/cli/doctor_cmd.py +152 -0
  8. package/src/superlocalmemory/cli/main.py +28 -0
  9. package/src/superlocalmemory/cli/pending_store.py +55 -3
  10. package/src/superlocalmemory/cli/post_install.py +15 -0
  11. package/src/superlocalmemory/cli/setup_wizard.py +20 -0
  12. package/src/superlocalmemory/cli/version_banner.py +183 -0
  13. package/src/superlocalmemory/cli/wizard_v3426_options.py +129 -0
  14. package/src/superlocalmemory/core/clock_monitor.py +45 -0
  15. package/src/superlocalmemory/core/db_pool.py +80 -0
  16. package/src/superlocalmemory/core/engine.py +75 -30
  17. package/src/superlocalmemory/core/engine_capabilities.py +24 -0
  18. package/src/superlocalmemory/core/engine_lock.py +75 -0
  19. package/src/superlocalmemory/core/error_catalog.py +113 -0
  20. package/src/superlocalmemory/core/error_envelope.py +60 -0
  21. package/src/superlocalmemory/core/file_lock.py +92 -0
  22. package/src/superlocalmemory/core/loop_watchdog.py +56 -0
  23. package/src/superlocalmemory/core/maintenance_scheduler.py +8 -0
  24. package/src/superlocalmemory/core/priority_queue.py +61 -0
  25. package/src/superlocalmemory/core/queue_dispatcher.py +73 -0
  26. package/src/superlocalmemory/core/rate_limit.py +151 -0
  27. package/src/superlocalmemory/core/recall_queue.py +370 -0
  28. package/src/superlocalmemory/core/recall_worker.py +10 -0
  29. package/src/superlocalmemory/core/safe_fs.py +108 -0
  30. package/src/superlocalmemory/hooks/auto_capture.py +34 -12
  31. package/src/superlocalmemory/hooks/auto_recall.py +36 -9
  32. package/src/superlocalmemory/learning/signals.py +7 -1
  33. package/src/superlocalmemory/mcp/_daemon_proxy.py +107 -0
  34. package/src/superlocalmemory/mcp/_pool_adapter.py +121 -0
  35. package/src/superlocalmemory/mcp/resources.py +8 -5
  36. package/src/superlocalmemory/mcp/server.py +38 -9
  37. package/src/superlocalmemory/mcp/tools_active.py +21 -9
  38. package/src/superlocalmemory/mcp/tools_core.py +13 -9
  39. package/src/superlocalmemory/mcp/tools_evolution.py +4 -2
  40. package/src/superlocalmemory/mcp/tools_learning.py +5 -3
  41. package/src/superlocalmemory/mcp/tools_mesh.py +5 -3
  42. package/src/superlocalmemory/mcp/tools_v3.py +18 -22
  43. package/src/superlocalmemory/mcp/tools_v33.py +65 -2
  44. package/src/superlocalmemory/migrations/__init__.py +5 -0
  45. package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +144 -0
  46. package/src/superlocalmemory/server/routes/data_io.py +21 -2
  47. package/src/superlocalmemory/server/routes/memories.py +91 -0
  48. package/src/superlocalmemory/server/routes/stats.py +16 -2
  49. package/src/superlocalmemory/server/unified_daemon.py +128 -12
  50. package/src/superlocalmemory/ui/index.html +35 -25
  51. package/src/superlocalmemory/ui/js/core.js +20 -4
  52. package/src/superlocalmemory/ui/js/fact-detail.js +62 -73
  53. package/src/superlocalmemory/ui/js/memories.js +34 -2
  54. package/src/superlocalmemory/ui/js/modal.js +41 -2
  55. package/src/superlocalmemory/ui/js/search.js +27 -0
@@ -0,0 +1,24 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """MemoryEngine capability levels.
6
+
7
+ LIGHT: SQLite + profile only. Suitable for clients that must stay small in
8
+ memory (e.g. multi-IDE MCP processes) and route heavy work elsewhere.
9
+
10
+ FULL: DB layer + embedder + retrieval engine + LLM. Matches the historical
11
+ v3.4.25 engine surface exactly and remains the default for backward compat.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import enum
16
+
17
+
18
+ class Capabilities(enum.Enum):
19
+ LIGHT = "light"
20
+ FULL = "full"
21
+
22
+
23
+ class CapabilityError(RuntimeError):
24
+ """Raised when a LIGHT-mode engine is asked for a FULL-mode operation."""
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Reader-writer lock used by the shared worker engine.
6
+
7
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ from contextlib import contextmanager
14
+ from typing import Iterator
15
+
16
+
17
+ class EngineRWLock:
18
+ """Writer-priority reader-writer lock.
19
+
20
+ Usage:
21
+ lock = EngineRWLock()
22
+ with lock.read():
23
+ ...
24
+ with lock.write():
25
+ ...
26
+
27
+ Not reentrant.
28
+ """
29
+
30
+ __slots__ = ("_cond", "_readers", "_writers_waiting", "_writer_active")
31
+
32
+ def __init__(self) -> None:
33
+ self._cond = threading.Condition()
34
+ self._readers: int = 0
35
+ self._writers_waiting: int = 0
36
+ self._writer_active: bool = False
37
+
38
+ @contextmanager
39
+ def read(self) -> Iterator[None]:
40
+ with self._cond:
41
+ while self._writer_active or self._writers_waiting > 0:
42
+ self._cond.wait()
43
+ self._readers += 1
44
+ try:
45
+ yield
46
+ finally:
47
+ with self._cond:
48
+ self._readers -= 1
49
+ if self._readers == 0:
50
+ self._cond.notify_all()
51
+
52
+ @contextmanager
53
+ def write(self) -> Iterator[None]:
54
+ with self._cond:
55
+ self._writers_waiting += 1
56
+ try:
57
+ while self._readers > 0 or self._writer_active:
58
+ self._cond.wait()
59
+ self._writer_active = True
60
+ finally:
61
+ self._writers_waiting -= 1
62
+ try:
63
+ yield
64
+ finally:
65
+ with self._cond:
66
+ self._writer_active = False
67
+ self._cond.notify_all()
68
+
69
+ def stats(self) -> dict[str, int | bool]:
70
+ with self._cond:
71
+ return {
72
+ "readers": self._readers,
73
+ "writers_waiting": self._writers_waiting,
74
+ "writer_active": self._writer_active,
75
+ }
@@ -0,0 +1,113 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """User-facing strings for queue error codes."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TypedDict
10
+
11
+ from superlocalmemory.core.error_envelope import ErrorCode
12
+
13
+
14
+ class ErrorEntry(TypedDict):
15
+ code: str
16
+ title: str
17
+ cli_message: str
18
+ recovery: list[str]
19
+ exit_code: int
20
+
21
+
22
+ CATALOG: dict[str, ErrorEntry] = {
23
+ ErrorCode.RATE_LIMITED.value: {
24
+ "code": "RATE_LIMITED",
25
+ "title": "Rate limited",
26
+ "cli_message": "Too many recalls in the last second.",
27
+ "recovery": [
28
+ "Wait a moment and try again.",
29
+ "For batch work, export SLM_RATE_LIMIT_PER_AGENT=50 and restart the daemon.",
30
+ ],
31
+ "exit_code": 3,
32
+ },
33
+ ErrorCode.QUEUE_FULL.value: {
34
+ "code": "QUEUE_FULL",
35
+ "title": "Queue is full",
36
+ "cli_message": "The request queue cannot accept more work right now.",
37
+ "recovery": [
38
+ "Retry with backoff.",
39
+ "Run: slm queue status",
40
+ ],
41
+ "exit_code": 5,
42
+ },
43
+ ErrorCode.TIMEOUT.value: {
44
+ "code": "TIMEOUT",
45
+ "title": "Timed out",
46
+ "cli_message": "The recall did not finish in the allotted time.",
47
+ "recovery": [
48
+ "Retry.",
49
+ "Run: slm doctor",
50
+ ],
51
+ "exit_code": 2,
52
+ },
53
+ ErrorCode.CANCELLED.value: {
54
+ "code": "CANCELLED",
55
+ "title": "Cancelled",
56
+ "cli_message": "The request was cancelled before it completed.",
57
+ "recovery": ["Re-issue the request if needed."],
58
+ "exit_code": 6,
59
+ },
60
+ ErrorCode.DEAD_LETTER.value: {
61
+ "code": "DEAD_LETTER",
62
+ "title": "Request failed after retries",
63
+ "cli_message": (
64
+ "The request could not complete after the maximum number of "
65
+ "attempts. Your query is preserved in the dead-letter queue."
66
+ ),
67
+ "recovery": [
68
+ "Inspect: slm queue dlq",
69
+ "Run: slm doctor",
70
+ ],
71
+ "exit_code": 4,
72
+ },
73
+ ErrorCode.DAEMON_DOWN.value: {
74
+ "code": "DAEMON_DOWN",
75
+ "title": "Daemon unreachable",
76
+ "cli_message": "Cannot reach the SLM daemon.",
77
+ "recovery": [
78
+ "Start the daemon: slm daemon start",
79
+ "Check status: slm daemon status",
80
+ ],
81
+ "exit_code": 7,
82
+ },
83
+ ErrorCode.INTERNAL.value: {
84
+ "code": "INTERNAL",
85
+ "title": "Internal error",
86
+ "cli_message": "An unexpected internal error occurred.",
87
+ "recovery": [
88
+ "Run: slm doctor",
89
+ "If the issue persists, file an issue with the log excerpt.",
90
+ ],
91
+ "exit_code": 8,
92
+ },
93
+ }
94
+
95
+
96
+ def lookup(code: str | ErrorCode) -> ErrorEntry:
97
+ key = code.value if isinstance(code, ErrorCode) else code
98
+ if key not in CATALOG:
99
+ return CATALOG[ErrorCode.INTERNAL.value]
100
+ return CATALOG[key]
101
+
102
+
103
+ def format_cli(code: str | ErrorCode, detail: str | None = None) -> str:
104
+ entry = lookup(code)
105
+ lines = [f"\u2717 {entry['title']}"]
106
+ lines.append(f" {entry['cli_message']}")
107
+ if detail:
108
+ lines.append(f" Detail: {detail}")
109
+ if entry["recovery"]:
110
+ lines.append(" Try:")
111
+ for step in entry["recovery"]:
112
+ lines.append(f" - {step}")
113
+ return "\n".join(lines)
@@ -0,0 +1,60 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Uniform error envelope for queue-backed callers.
6
+
7
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from enum import Enum
13
+ from typing import Any
14
+
15
+
16
+ class ErrorCode(str, Enum):
17
+ RATE_LIMITED = "RATE_LIMITED"
18
+ QUEUE_FULL = "QUEUE_FULL"
19
+ TIMEOUT = "TIMEOUT"
20
+ CANCELLED = "CANCELLED"
21
+ DEAD_LETTER = "DEAD_LETTER"
22
+ DAEMON_DOWN = "DAEMON_DOWN"
23
+ INTERNAL = "INTERNAL"
24
+
25
+
26
+ _HTTP_STATUS: dict[ErrorCode, int] = {
27
+ ErrorCode.RATE_LIMITED: 429,
28
+ ErrorCode.QUEUE_FULL: 503,
29
+ ErrorCode.TIMEOUT: 504,
30
+ ErrorCode.DEAD_LETTER: 504,
31
+ ErrorCode.CANCELLED: 499,
32
+ ErrorCode.DAEMON_DOWN: 502,
33
+ ErrorCode.INTERNAL: 500,
34
+ }
35
+
36
+
37
+ def http_status_for(code: ErrorCode) -> int:
38
+ return _HTTP_STATUS[code]
39
+
40
+
41
+ def make_error_envelope(
42
+ code: ErrorCode,
43
+ message: str,
44
+ *,
45
+ request_id: str | None = None,
46
+ retry_after_ms: int | None = None,
47
+ **extras: Any,
48
+ ) -> dict[str, Any]:
49
+ """Build a JSON-serialisable error envelope."""
50
+ env: dict[str, Any] = {
51
+ "ok": False,
52
+ "error_code": code.value,
53
+ "error": message,
54
+ "request_id": request_id,
55
+ "retry_after_ms": retry_after_ms,
56
+ }
57
+ for k, v in extras.items():
58
+ if k not in env:
59
+ env[k] = v
60
+ return env
@@ -0,0 +1,92 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Cross-platform exclusive file lock via portalocker.
6
+
7
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import threading
14
+ from contextlib import contextmanager
15
+ from pathlib import Path
16
+ from typing import Iterator
17
+
18
+ try:
19
+ import portalocker
20
+ _HAS_PORTALOCKER = True
21
+ except ImportError: # pragma: no cover
22
+ portalocker = None # type: ignore[assignment]
23
+ _HAS_PORTALOCKER = False
24
+
25
+
26
+ class LockHeldError(RuntimeError):
27
+ """Raised when the lock cannot be acquired (held by another holder)."""
28
+
29
+
30
+ class _FallbackLock:
31
+ """Thread-local mutex used when portalocker is unavailable."""
32
+
33
+ def __init__(self) -> None:
34
+ self._lock = threading.Lock()
35
+
36
+ def acquire(self, timeout: float) -> bool:
37
+ return self._lock.acquire(timeout=timeout)
38
+
39
+ def release(self) -> None:
40
+ self._lock.release()
41
+
42
+
43
+ _fallback_registry: dict[str, _FallbackLock] = {}
44
+ _fallback_registry_lock = threading.Lock()
45
+
46
+ # Track fds held by this process so a double-acquire in the same process
47
+ # is rejected immediately (portalocker on POSIX is permissive on same fd).
48
+ _held_in_process: set[str] = set()
49
+ _held_lock = threading.Lock()
50
+
51
+
52
+ @contextmanager
53
+ def exclusive_lock(path: Path, timeout_s: float = 0.0) -> Iterator[int]:
54
+ """Acquire an exclusive file lock. Raises LockHeldError on contention."""
55
+ path_str = str(path)
56
+ with _held_lock:
57
+ if path_str in _held_in_process:
58
+ raise LockHeldError(f"{path} already locked by this process")
59
+ _held_in_process.add(path_str)
60
+
61
+ try:
62
+ if _HAS_PORTALOCKER:
63
+ fd = os.open(str(path), os.O_RDWR | os.O_CREAT, 0o600)
64
+ try:
65
+ try:
66
+ portalocker.lock(
67
+ fd, portalocker.LOCK_EX | portalocker.LOCK_NB,
68
+ )
69
+ except portalocker.LockException as exc:
70
+ os.close(fd)
71
+ raise LockHeldError(f"{path} is locked") from exc
72
+ try:
73
+ yield fd
74
+ finally:
75
+ try:
76
+ portalocker.unlock(fd)
77
+ finally:
78
+ os.close(fd)
79
+ except LockHeldError:
80
+ raise
81
+ else: # pragma: no cover — fallback path
82
+ with _fallback_registry_lock:
83
+ lk = _fallback_registry.setdefault(path_str, _FallbackLock())
84
+ if not lk.acquire(timeout=timeout_s):
85
+ raise LockHeldError(f"{path} is locked (fallback)")
86
+ try:
87
+ yield -1
88
+ finally:
89
+ lk.release()
90
+ finally:
91
+ with _held_lock:
92
+ _held_in_process.discard(path_str)
@@ -0,0 +1,56 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Tick-based watchdog that fires once when a cooperative loop goes silent.
6
+
7
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ import time
14
+ from typing import Callable, Optional
15
+
16
+
17
+ class LoopWatchdog:
18
+ def __init__(
19
+ self,
20
+ stale_threshold_s: float,
21
+ on_stale: Optional[Callable[[float], None]] = None,
22
+ ) -> None:
23
+ self._threshold = stale_threshold_s
24
+ self._on_stale = on_stale
25
+ self._last_tick = time.monotonic()
26
+ self._fired = False
27
+ self._lock = threading.Lock()
28
+
29
+ def tick(self) -> None:
30
+ with self._lock:
31
+ self._last_tick = time.monotonic()
32
+ self._fired = False
33
+
34
+ def age_s(self) -> float:
35
+ with self._lock:
36
+ return time.monotonic() - self._last_tick
37
+
38
+ def is_stale(self) -> bool:
39
+ return self.age_s() >= self._threshold
40
+
41
+ def check(self) -> bool:
42
+ """Fire callback once if stale; return True if just fired."""
43
+ with self._lock:
44
+ age = time.monotonic() - self._last_tick
45
+ if age < self._threshold or self._fired:
46
+ return False
47
+ self._fired = True
48
+ cb = self._on_stale
49
+ if cb is not None:
50
+ cb(age)
51
+ return True
52
+
53
+ def run_forever(self, stop: threading.Event, interval_s: float = 1.0) -> None:
54
+ while not stop.is_set():
55
+ self.check()
56
+ stop.wait(timeout=interval_s)
@@ -116,6 +116,14 @@ class MaintenanceScheduler:
116
116
  except Exception as exc:
117
117
  logger.debug("Auto-backup check skipped: %s", exc)
118
118
 
119
+ try:
120
+ from superlocalmemory.cli.pending_store import cleanup_stale
121
+ stats = cleanup_stale()
122
+ if stats["total"] > 0:
123
+ logger.info("Pending cleanup: %s", stats)
124
+ except Exception as exc:
125
+ logger.debug("Pending cleanup skipped: %s", exc)
126
+
119
127
  self._schedule_next()
120
128
 
121
129
  def _sync_cloud_destinations(self, manager: object) -> None:
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Weighted fair scheduler for high/low job lanes.
6
+
7
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ from typing import Literal
14
+
15
+ Lane = Literal["high", "low"]
16
+
17
+
18
+ class WFQScheduler:
19
+ """Deficit-round-robin-ish scheduler over two lanes.
20
+
21
+ Tracks served counts and picks the lane whose ratio is furthest
22
+ below its target share. Approximates the target ratio over any
23
+ rolling window without requiring a time source.
24
+ """
25
+
26
+ def __init__(self, high_weight: int = 70, low_weight: int = 30) -> None:
27
+ if high_weight <= 0 or low_weight <= 0:
28
+ raise ValueError("weights must be positive")
29
+ self.high_weight = high_weight
30
+ self.low_weight = low_weight
31
+ self._served = {"high": 0, "low": 0}
32
+ self._lock = threading.Lock()
33
+
34
+ def pick_lane(self, *, has_high: bool, has_low: bool) -> Lane | None:
35
+ if not has_high and not has_low:
36
+ return None
37
+ if has_high and not has_low:
38
+ return "high"
39
+ if has_low and not has_high:
40
+ return "low"
41
+ with self._lock:
42
+ total = self._served["high"] + self._served["low"]
43
+ if total == 0:
44
+ return "high"
45
+ total_weight = self.high_weight + self.low_weight
46
+ high_target = self.high_weight / total_weight
47
+ low_target = self.low_weight / total_weight
48
+ high_ratio = self._served["high"] / total
49
+ low_ratio = self._served["low"] / total
50
+ # Pick whichever lane is further below its target
51
+ high_deficit = high_target - high_ratio
52
+ low_deficit = low_target - low_ratio
53
+ return "high" if high_deficit >= low_deficit else "low"
54
+
55
+ def record_served(self, lane: Lane) -> None:
56
+ with self._lock:
57
+ self._served[lane] += 1
58
+
59
+ def snapshot(self) -> dict[str, int]:
60
+ with self._lock:
61
+ return dict(self._served)
@@ -0,0 +1,73 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Queue-backed dispatcher coordinator.
6
+
7
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from superlocalmemory.core import rate_limit as rl
17
+ from superlocalmemory.core import recall_queue as rq
18
+ from superlocalmemory.core.engine_lock import EngineRWLock
19
+
20
+
21
+ class QueueDispatcher:
22
+ """Coordinates rate-limit check, enqueue, and poll for callers."""
23
+
24
+ def __init__(
25
+ self,
26
+ *,
27
+ db_path: Path | str,
28
+ global_rps: float = 100.0,
29
+ per_pid_rps: float = 30.0,
30
+ per_agent_rps: float = 10.0,
31
+ ) -> None:
32
+ self.queue = rq.RecallQueue(db_path=db_path)
33
+ self.engine_lock = EngineRWLock()
34
+ self._rate = rl.LayeredRateLimiter(
35
+ global_rps=global_rps,
36
+ per_pid_rps=per_pid_rps,
37
+ per_agent_rps=per_agent_rps,
38
+ )
39
+ # Module handles kept for test introspection.
40
+ self.rl = rl
41
+ self.rq = rq
42
+
43
+ def _check_rate(self, *, pid: int, agent_id: str | None) -> None:
44
+ self._rate.check_and_consume(pid=pid, agent_id=agent_id)
45
+
46
+ def dispatch(
47
+ self,
48
+ *,
49
+ query: str,
50
+ limit_n: int,
51
+ mode: str,
52
+ agent_id: str,
53
+ session_id: str,
54
+ tenant_id: str = "",
55
+ namespace: str = "",
56
+ priority: str = "high",
57
+ stall_timeout_s: float = 25.0,
58
+ timeout_s: float = 30.0,
59
+ ) -> dict[str, Any]:
60
+ self._check_rate(pid=os.getpid(), agent_id=agent_id)
61
+ rid = self.queue.enqueue(
62
+ query=query, limit_n=limit_n, mode=mode,
63
+ agent_id=agent_id, session_id=session_id,
64
+ tenant_id=tenant_id, namespace=namespace,
65
+ priority=priority, stall_timeout_s=stall_timeout_s,
66
+ )
67
+ try:
68
+ return self.queue.poll_result(rid, timeout_s=timeout_s)
69
+ finally:
70
+ self.queue.unsubscribe(rid)
71
+
72
+ def close(self) -> None:
73
+ self.queue.close()