superlocalmemory 3.8.5 → 3.8.7
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 +47 -0
- 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 +9 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +139 -404
- package/src/superlocalmemory/core/backend_orchestrator.py +7 -1
- package/src/superlocalmemory/core/component_registry.py +4 -2
- package/src/superlocalmemory/core/embeddings.py +33 -6
- package/src/superlocalmemory/core/engine.py +94 -49
- package/src/superlocalmemory/core/engine_ingestion.py +150 -63
- package/src/superlocalmemory/core/ingestion_command.py +133 -21
- package/src/superlocalmemory/core/mutations.py +32 -10
- package/src/superlocalmemory/core/recall_pipeline.py +111 -77
- package/src/superlocalmemory/core/remember_admission.py +152 -0
- package/src/superlocalmemory/core/remember_runtime.py +712 -0
- package/src/superlocalmemory/graph/cozo_backend.py +5 -5
- package/src/superlocalmemory/learning/bandit.py +50 -1
- package/src/superlocalmemory/learning/source_quality.py +38 -35
- package/src/superlocalmemory/mcp/_daemon_proxy.py +38 -15
- package/src/superlocalmemory/mcp/tools_active.py +4 -41
- package/src/superlocalmemory/mcp/tools_core.py +26 -87
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -10
- package/src/superlocalmemory/optimize/proxy/capture.py +196 -8
- package/src/superlocalmemory/retrieval/engine.py +8 -3
- package/src/superlocalmemory/retrieval/reranker.py +35 -10
- package/src/superlocalmemory/server/loopback.py +7 -13
- package/src/superlocalmemory/server/profile_runtime.py +14 -0
- package/src/superlocalmemory/server/routes/abstraction.py +2 -4
- package/src/superlocalmemory/server/routes/agents.py +3 -5
- package/src/superlocalmemory/server/routes/behavioral.py +5 -13
- package/src/superlocalmemory/server/routes/brain.py +6 -9
- package/src/superlocalmemory/server/routes/entity.py +3 -7
- package/src/superlocalmemory/server/routes/evolution.py +3 -5
- package/src/superlocalmemory/server/routes/helpers.py +44 -23
- package/src/superlocalmemory/server/routes/insights.py +2 -4
- package/src/superlocalmemory/server/routes/learning.py +2 -5
- package/src/superlocalmemory/server/routes/lifecycle.py +2 -4
- package/src/superlocalmemory/server/routes/memories.py +122 -100
- package/src/superlocalmemory/server/routes/tiers.py +3 -22
- package/src/superlocalmemory/server/routes/timeline.py +2 -4
- package/src/superlocalmemory/server/routes/v3_api.py +18 -16
- package/src/superlocalmemory/server/unified_daemon.py +200 -109
- package/src/superlocalmemory/storage/admission_codec.py +119 -0
- package/src/superlocalmemory/storage/admission_journal.py +728 -0
- package/src/superlocalmemory/storage/database.py +59 -0
- package/src/superlocalmemory/storage/deferred_writes.py +67 -11
- package/src/superlocalmemory/storage/memory_write.py +8 -12
- package/src/superlocalmemory/storage/migration_runner.py +37 -0
- package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +188 -0
- package/src/superlocalmemory/storage/read_connection.py +115 -0
- package/src/superlocalmemory/storage/write_coordinator.py +756 -0
- package/src/superlocalmemory/ui/index.html +1 -1
- package/src/superlocalmemory/ui/js/auto-settings.js +14 -1
- package/src/superlocalmemory/ui/js/od-settings.js +9 -3
|
@@ -75,12 +75,12 @@ class _CozoResult:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
class _CozoClientAdapter:
|
|
78
|
-
"""Bridge PyCozo
|
|
78
|
+
"""Bridge legacy PyCozo responses and the store-compatible client surface.
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
SLM graph stores were established with PyCozo 0.3.0. That client returns
|
|
81
|
+
dictionaries and exposes ``import_relations`` rather than ``put``; later
|
|
82
|
+
clients return dataframe-like values and add ``put``. SLM only needs
|
|
83
|
+
relation upserts and row results, so normalize both forms here.
|
|
84
84
|
"""
|
|
85
85
|
|
|
86
86
|
def __init__(self, client: Any) -> None:
|
|
@@ -29,7 +29,6 @@ import os
|
|
|
29
29
|
import secrets
|
|
30
30
|
import sqlite3
|
|
31
31
|
import threading
|
|
32
|
-
import time
|
|
33
32
|
from dataclasses import dataclass, field
|
|
34
33
|
from datetime import datetime, timedelta, timezone
|
|
35
34
|
from pathlib import Path
|
|
@@ -262,6 +261,33 @@ class ContextualBandit:
|
|
|
262
261
|
play_id=play_id,
|
|
263
262
|
)
|
|
264
263
|
|
|
264
|
+
def choose_readonly(self, context: dict[str, Any]) -> BanditChoice:
|
|
265
|
+
"""Sample an arm from a read-only snapshot without recording a play.
|
|
266
|
+
|
|
267
|
+
Recall uses this method so the established bandit weighting and
|
|
268
|
+
ensemble quality path remain available without turning a query into a
|
|
269
|
+
``bandit_plays`` write. It deliberately bypasses the writer-oriented
|
|
270
|
+
thread-local connection factory: that factory configures WAL mode and
|
|
271
|
+
is not a physical read-only guarantee.
|
|
272
|
+
"""
|
|
273
|
+
stratum = compute_stratum(context)
|
|
274
|
+
try:
|
|
275
|
+
posteriors = self._load_stratum_posteriors_readonly(stratum)
|
|
276
|
+
except sqlite3.Error as exc:
|
|
277
|
+
logger.warning(
|
|
278
|
+
"bandit.choose_readonly: posterior load failed stratum=%s: %s",
|
|
279
|
+
stratum,
|
|
280
|
+
exc,
|
|
281
|
+
)
|
|
282
|
+
posteriors = {}
|
|
283
|
+
arm_id = self._sample_best(posteriors)
|
|
284
|
+
return BanditChoice(
|
|
285
|
+
stratum=stratum,
|
|
286
|
+
arm_id=arm_id,
|
|
287
|
+
weights=dict(self._catalog[arm_id]),
|
|
288
|
+
play_id=None,
|
|
289
|
+
)
|
|
290
|
+
|
|
265
291
|
def _sample_best(
|
|
266
292
|
self,
|
|
267
293
|
posteriors: dict[str, tuple[float, float]],
|
|
@@ -438,6 +464,29 @@ class ContextualBandit:
|
|
|
438
464
|
for r in rows
|
|
439
465
|
}
|
|
440
466
|
|
|
467
|
+
def _load_stratum_posteriors_readonly(
|
|
468
|
+
self,
|
|
469
|
+
stratum: str,
|
|
470
|
+
) -> dict[str, tuple[float, float]]:
|
|
471
|
+
"""Read posteriors through SQLite's URI read-only boundary."""
|
|
472
|
+
uri = f"{self._db_path.resolve().as_uri()}?mode=ro"
|
|
473
|
+
conn = sqlite3.connect(uri, uri=True, timeout=0.25)
|
|
474
|
+
try:
|
|
475
|
+
conn.row_factory = sqlite3.Row
|
|
476
|
+
conn.execute("PRAGMA query_only=ON")
|
|
477
|
+
conn.execute("PRAGMA busy_timeout=250")
|
|
478
|
+
rows = conn.execute(
|
|
479
|
+
"SELECT arm_id, alpha, beta FROM bandit_arms "
|
|
480
|
+
"WHERE profile_id = ? AND stratum = ?",
|
|
481
|
+
(self._profile, stratum),
|
|
482
|
+
).fetchall()
|
|
483
|
+
finally:
|
|
484
|
+
conn.close()
|
|
485
|
+
return {
|
|
486
|
+
row["arm_id"]: (float(row["alpha"]), float(row["beta"]))
|
|
487
|
+
for row in rows
|
|
488
|
+
}
|
|
489
|
+
|
|
441
490
|
def _insert_play(
|
|
442
491
|
self,
|
|
443
492
|
query_id: str,
|
|
@@ -81,6 +81,7 @@ CREATE TABLE IF NOT EXISTS source_quality_repair_state (
|
|
|
81
81
|
_MAX_FACTS_PER_OUTCOME = 100
|
|
82
82
|
_MAX_SOURCES_PER_OUTCOME = 100
|
|
83
83
|
_PROVENANCE_QUERY_CHUNK = 500
|
|
84
|
+
_SCHEMA_INIT_LOCK = threading.RLock()
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
class SourceQualityRepairUnavailable(RuntimeError):
|
|
@@ -114,45 +115,47 @@ class SourceQualityScorer:
|
|
|
114
115
|
# ------------------------------------------------------------------
|
|
115
116
|
|
|
116
117
|
def _ensure_schema(self) -> None:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
118
|
+
# Serialize scorer instances in this process before asking SQLite for
|
|
119
|
+
# its cross-process BEGIN IMMEDIATE lease. This prevents two startup
|
|
120
|
+
# threads from racing the journal-mode/schema bootstrap while SQLite's
|
|
121
|
+
# busy timeout protects the equivalent multi-process boundary.
|
|
122
|
+
with _SCHEMA_INIT_LOCK:
|
|
123
|
+
conn = self._connect()
|
|
124
|
+
try:
|
|
125
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
126
|
+
conn.execute(_CREATE_TABLE)
|
|
127
|
+
conn.execute(_CREATE_UNIQUE)
|
|
128
|
+
conn.execute(_CREATE_OBSERVATIONS)
|
|
129
|
+
conn.execute(_CREATE_REPAIR_STATE)
|
|
130
|
+
repair_columns = {
|
|
131
|
+
str(row["name"])
|
|
132
|
+
for row in conn.execute(
|
|
133
|
+
"PRAGMA table_info(source_quality_repair_state)"
|
|
134
|
+
).fetchall()
|
|
135
|
+
}
|
|
136
|
+
if "last_settled_at" not in repair_columns:
|
|
137
|
+
conn.execute(
|
|
138
|
+
"ALTER TABLE source_quality_repair_state "
|
|
139
|
+
"ADD COLUMN last_settled_at TEXT NOT NULL DEFAULT ''"
|
|
140
|
+
)
|
|
141
|
+
if "last_outcome_id" not in repair_columns:
|
|
142
|
+
conn.execute(
|
|
143
|
+
"ALTER TABLE source_quality_repair_state "
|
|
144
|
+
"ADD COLUMN last_outcome_id TEXT NOT NULL DEFAULT ''"
|
|
145
|
+
)
|
|
146
|
+
conn.commit()
|
|
147
|
+
except Exception:
|
|
148
|
+
conn.rollback()
|
|
149
|
+
raise
|
|
150
|
+
finally:
|
|
151
|
+
conn.close()
|
|
151
152
|
|
|
152
153
|
def _connect(self) -> sqlite3.Connection:
|
|
153
154
|
conn = sqlite3.connect(str(self._db_path), timeout=10)
|
|
155
|
+
# Install the busy handler before journal negotiation. On a fresh
|
|
156
|
+
# database, PRAGMA journal_mode itself may contend with another scorer.
|
|
157
|
+
conn.execute("PRAGMA busy_timeout=10000")
|
|
154
158
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
155
|
-
conn.execute("PRAGMA busy_timeout=5000")
|
|
156
159
|
conn.row_factory = sqlite3.Row
|
|
157
160
|
return conn
|
|
158
161
|
|
|
@@ -34,9 +34,26 @@ class DaemonPoolProxy:
|
|
|
34
34
|
envelopes — the adapter is responsible for surfacing those.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
def __init__(
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
port: int | None,
|
|
40
|
+
*,
|
|
41
|
+
timeout_s: float = 30.0,
|
|
42
|
+
unavailable: bool = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
# v3.4.59: 8s→30s — dense graph recall can exceed the old timeout.
|
|
38
45
|
self._port = port
|
|
39
46
|
self._timeout = timeout_s
|
|
47
|
+
self._unavailable = unavailable
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _unavailable_response() -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"ok": False,
|
|
53
|
+
"code": "DAEMON_UNAVAILABLE",
|
|
54
|
+
"retryable": True,
|
|
55
|
+
"error": "DAEMON_UNAVAILABLE: owned daemon is unavailable; retry later.",
|
|
56
|
+
}
|
|
40
57
|
|
|
41
58
|
def recall(
|
|
42
59
|
self, query: str, limit: int = 10, session_id: str = "",
|
|
@@ -45,6 +62,8 @@ class DaemonPoolProxy:
|
|
|
45
62
|
include_shared: bool | None = None,
|
|
46
63
|
window: str | None = None,
|
|
47
64
|
) -> dict[str, Any]:
|
|
65
|
+
if self._unavailable:
|
|
66
|
+
return self._unavailable_response()
|
|
48
67
|
_params: dict[str, Any] = {
|
|
49
68
|
"q": query,
|
|
50
69
|
"limit": limit,
|
|
@@ -75,15 +94,17 @@ class DaemonPoolProxy:
|
|
|
75
94
|
)
|
|
76
95
|
except Exception as exc:
|
|
77
96
|
logger.warning("daemon /recall failed: %s", exc)
|
|
78
|
-
return
|
|
97
|
+
return self._unavailable_response()
|
|
79
98
|
if not isinstance(data, dict):
|
|
80
|
-
return
|
|
99
|
+
return self._unavailable_response()
|
|
81
100
|
data.setdefault("ok", True)
|
|
82
101
|
return data
|
|
83
102
|
|
|
84
103
|
def store(
|
|
85
104
|
self, content: str, metadata: dict | None = None,
|
|
86
105
|
) -> dict[str, Any]:
|
|
106
|
+
if self._unavailable:
|
|
107
|
+
return self._unavailable_response()
|
|
87
108
|
body = {
|
|
88
109
|
"content": content,
|
|
89
110
|
"tags": (metadata or {}).get("tags", ""),
|
|
@@ -101,9 +122,9 @@ class DaemonPoolProxy:
|
|
|
101
122
|
data = daemon_request("POST", "/remember", body)
|
|
102
123
|
except Exception as exc:
|
|
103
124
|
logger.warning("daemon /remember failed: %s", exc)
|
|
104
|
-
return
|
|
125
|
+
return self._unavailable_response()
|
|
105
126
|
if not isinstance(data, dict):
|
|
106
|
-
return
|
|
127
|
+
return self._unavailable_response()
|
|
107
128
|
data.setdefault("ok", True)
|
|
108
129
|
return data
|
|
109
130
|
|
|
@@ -111,17 +132,19 @@ class DaemonPoolProxy:
|
|
|
111
132
|
def choose_pool() -> Any:
|
|
112
133
|
"""Return the best available pool for this MCP process.
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/ first-launch scenarios working.
|
|
135
|
+
The daemon is the sole canonical writer. A bounded daemon auto-start is
|
|
136
|
+
attempted for first use; if it cannot become healthy, return a facade that
|
|
137
|
+
reports a retryable ``DAEMON_UNAVAILABLE`` envelope. Never construct a
|
|
138
|
+
process-local ``WorkerPool`` from an MCP client.
|
|
119
139
|
"""
|
|
120
140
|
try:
|
|
121
|
-
from superlocalmemory.cli.daemon import
|
|
122
|
-
|
|
141
|
+
from superlocalmemory.cli.daemon import (
|
|
142
|
+
_get_port,
|
|
143
|
+
ensure_daemon,
|
|
144
|
+
is_daemon_running,
|
|
145
|
+
)
|
|
146
|
+
if is_daemon_running() or ensure_daemon():
|
|
123
147
|
return DaemonPoolProxy(port=_get_port())
|
|
124
148
|
except Exception as exc:
|
|
125
|
-
logger.warning("daemon probe
|
|
126
|
-
|
|
127
|
-
return WorkerPool.shared()
|
|
149
|
+
logger.warning("daemon probe or bounded start failed: %s", exc)
|
|
150
|
+
return DaemonPoolProxy(port=None, unavailable=True)
|
|
@@ -19,12 +19,12 @@ from __future__ import annotations
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import datetime
|
|
21
21
|
import logging
|
|
22
|
-
import sqlite3
|
|
23
22
|
import uuid
|
|
24
23
|
from typing import TYPE_CHECKING, Callable
|
|
25
24
|
|
|
26
|
-
from superlocalmemory.infra.data_root import
|
|
25
|
+
from superlocalmemory.infra.data_root import state_path
|
|
27
26
|
from superlocalmemory.mcp.shared import authorize_mcp_mutation
|
|
27
|
+
from superlocalmemory.storage.read_connection import ReadConnectionFactory
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from superlocalmemory.mcp._pool_adapter import PoolRecallResponse
|
|
@@ -67,7 +67,8 @@ def _sqlite_emergency_recall(
|
|
|
67
67
|
f"AND f.created_at >= datetime('now', '-{int(max_age_days)} days') "
|
|
68
68
|
if max_age_days > 0 else ""
|
|
69
69
|
)
|
|
70
|
-
|
|
70
|
+
memory_db = state_path("memory.db").resolve()
|
|
71
|
+
conn = ReadConnectionFactory(memory_db, timeout_ms=250).open()
|
|
71
72
|
try:
|
|
72
73
|
rows = conn.execute(
|
|
73
74
|
f"""SELECT f.fact_id, f.content, f.memory_id, f.created_at,
|
|
@@ -139,21 +140,6 @@ def _emit_event(event_type: str, payload: dict | None = None,
|
|
|
139
140
|
logger.warning("event emit failed: type=%s err=%s", event_type, exc)
|
|
140
141
|
|
|
141
142
|
|
|
142
|
-
def _register_agent(agent_id: str, profile_id: str) -> bool:
|
|
143
|
-
"""Register an agent in the AgentRegistry (best-effort)."""
|
|
144
|
-
try:
|
|
145
|
-
from superlocalmemory.core.registry import AgentRegistry
|
|
146
|
-
registry_path = canonical_data_root() / "agents.json"
|
|
147
|
-
registry = AgentRegistry(persist_path=registry_path)
|
|
148
|
-
registry.register_agent(agent_id, profile_id)
|
|
149
|
-
return True
|
|
150
|
-
except Exception as exc:
|
|
151
|
-
logger.warning(
|
|
152
|
-
"agent registry write failed: agent=%s err=%s", agent_id, exc,
|
|
153
|
-
)
|
|
154
|
-
return False
|
|
155
|
-
|
|
156
|
-
|
|
157
143
|
def register_active_tools(server, get_engine: Callable) -> None:
|
|
158
144
|
"""Register 3 active memory tools on *server*."""
|
|
159
145
|
|
|
@@ -401,29 +387,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
401
387
|
f"-{uuid.uuid4().hex[:8]}"
|
|
402
388
|
)
|
|
403
389
|
|
|
404
|
-
# Register agent + emit event (v3.4.39: SLM_AGENT_ID env support)
|
|
405
|
-
agent_id = _get_agent_id()
|
|
406
|
-
if hasattr(engine, "_hooks"):
|
|
407
|
-
registration_auth = authorize_mcp_mutation(
|
|
408
|
-
engine,
|
|
409
|
-
"update",
|
|
410
|
-
mutation_source="mcp-agent-registration",
|
|
411
|
-
profile_id=pid,
|
|
412
|
-
)
|
|
413
|
-
if _register_agent(agent_id, pid):
|
|
414
|
-
registration_auth.complete()
|
|
415
|
-
else:
|
|
416
|
-
# A LIGHT client without the policy registry is read-capable,
|
|
417
|
-
# but must not fall back to an unauthorised registry write.
|
|
418
|
-
logger.info(
|
|
419
|
-
"agent registration skipped: policy hooks unavailable"
|
|
420
|
-
)
|
|
421
|
-
_emit_event("agent.connected", {
|
|
422
|
-
"agent_id": agent_id,
|
|
423
|
-
"project_path": project_path,
|
|
424
|
-
"memory_count": len(memories),
|
|
425
|
-
})
|
|
426
|
-
|
|
427
390
|
return {
|
|
428
391
|
"success": True,
|
|
429
392
|
"session_id": session_id,
|
|
@@ -20,7 +20,7 @@ from typing import Callable
|
|
|
20
20
|
from mcp.types import ToolAnnotations
|
|
21
21
|
|
|
22
22
|
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
|
|
23
|
-
from superlocalmemory.infra.data_root import
|
|
23
|
+
from superlocalmemory.infra.data_root import state_path
|
|
24
24
|
from superlocalmemory.mcp.shared import authorize_mcp_mutation
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
@@ -58,67 +58,6 @@ def _emit_event(event_type: str, payload: dict | None = None,
|
|
|
58
58
|
pass
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def _record_recall_hits(
|
|
62
|
-
get_engine: Callable,
|
|
63
|
-
query: str,
|
|
64
|
-
results: list[dict],
|
|
65
|
-
*,
|
|
66
|
-
profile_id: str = "",
|
|
67
|
-
query_id: str = "",
|
|
68
|
-
fact_ids_candidates: list[str] | None = None,
|
|
69
|
-
) -> None:
|
|
70
|
-
"""Record honest shown-state signals (LLD-02 §4.9).
|
|
71
|
-
|
|
72
|
-
v3.4.22: No more fake positives. For every candidate we enqueue a
|
|
73
|
-
``shown`` / ``not_shown`` flip based on whether it was returned in the
|
|
74
|
-
top-K presented to the user. Outcome/reward arrives in v3.4.22 via the
|
|
75
|
-
action-outcomes pipeline.
|
|
76
|
-
|
|
77
|
-
Non-blocking: all work funnels through ``signals.enqueue_shown_flip``
|
|
78
|
-
(module-level queue + background drain). Failures are swallowed —
|
|
79
|
-
signal quality is never load-bearing on recall correctness.
|
|
80
|
-
"""
|
|
81
|
-
try:
|
|
82
|
-
from superlocalmemory.learning.signals import (
|
|
83
|
-
LearningSignals,
|
|
84
|
-
enqueue_shown_flip,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
pid = profile_id
|
|
88
|
-
if not pid:
|
|
89
|
-
pid = get_engine().profile_id
|
|
90
|
-
slm_dir = canonical_data_root()
|
|
91
|
-
|
|
92
|
-
shown_ids = [r.get("fact_id", "") for r in results[:10]
|
|
93
|
-
if r.get("fact_id")]
|
|
94
|
-
candidates = (fact_ids_candidates
|
|
95
|
-
if fact_ids_candidates is not None
|
|
96
|
-
else shown_ids)
|
|
97
|
-
if not candidates:
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
# Shown-flip enqueue per §4.9. No synthetic positives.
|
|
101
|
-
shown_set = set(shown_ids)
|
|
102
|
-
if query_id:
|
|
103
|
-
for fid in candidates:
|
|
104
|
-
enqueue_shown_flip(query_id, fid, shown=(fid in shown_set))
|
|
105
|
-
|
|
106
|
-
# Legacy zero-cost signals — unchanged (co-retrieval + confidence).
|
|
107
|
-
try:
|
|
108
|
-
signals = LearningSignals(slm_dir / "learning.db")
|
|
109
|
-
signals.record_co_retrieval(pid, shown_ids)
|
|
110
|
-
except Exception:
|
|
111
|
-
pass
|
|
112
|
-
try:
|
|
113
|
-
mem_db = str(slm_dir / "memory.db")
|
|
114
|
-
for fid in shown_ids[:5]:
|
|
115
|
-
LearningSignals.boost_confidence(mem_db, fid)
|
|
116
|
-
except Exception:
|
|
117
|
-
pass
|
|
118
|
-
except Exception:
|
|
119
|
-
pass
|
|
120
|
-
|
|
121
|
-
|
|
122
61
|
def register_core_tools(server, get_engine: Callable) -> None:
|
|
123
62
|
"""Register the 13 core MCP tools on *server*."""
|
|
124
63
|
|
|
@@ -212,10 +151,10 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
212
151
|
await _asyncio.sleep(0.05 * (attempt + 1))
|
|
213
152
|
return {
|
|
214
153
|
"success": False,
|
|
154
|
+
"code": "DAEMON_UNAVAILABLE",
|
|
215
155
|
"retryable": True,
|
|
216
156
|
"error": (
|
|
217
|
-
"
|
|
218
|
-
"same remember operation without starting a second writer."
|
|
157
|
+
"DAEMON_UNAVAILABLE: owned daemon is unavailable; retry later."
|
|
219
158
|
),
|
|
220
159
|
}
|
|
221
160
|
except Exception as dexc:
|
|
@@ -241,11 +180,22 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
241
180
|
worker_meta,
|
|
242
181
|
)
|
|
243
182
|
if not isinstance(stored, dict) or not stored.get("ok"):
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
183
|
+
if isinstance(stored, dict) and stored.get("code") == "DAEMON_UNAVAILABLE":
|
|
184
|
+
return {
|
|
185
|
+
"success": False,
|
|
186
|
+
"code": "DAEMON_UNAVAILABLE",
|
|
187
|
+
"retryable": True,
|
|
188
|
+
"error": stored.get(
|
|
189
|
+
"error",
|
|
190
|
+
"DAEMON_UNAVAILABLE: owned daemon is unavailable; retry later.",
|
|
191
|
+
),
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
"success": False,
|
|
195
|
+
"code": "DAEMON_UNAVAILABLE",
|
|
196
|
+
"retryable": True,
|
|
197
|
+
"error": "DAEMON_UNAVAILABLE: owned daemon is unavailable; retry later.",
|
|
198
|
+
}
|
|
249
199
|
fact_ids = list(stored.get("fact_ids") or [])
|
|
250
200
|
materialization_state = str(
|
|
251
201
|
stored.get("materialization_state") or "complete"
|
|
@@ -275,9 +225,14 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
275
225
|
else "Queryable now; canonical enrichment is still running."
|
|
276
226
|
),
|
|
277
227
|
}
|
|
278
|
-
except Exception
|
|
228
|
+
except Exception:
|
|
279
229
|
logger.exception("remember failed")
|
|
280
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
"success": False,
|
|
232
|
+
"code": "DAEMON_UNAVAILABLE",
|
|
233
|
+
"retryable": True,
|
|
234
|
+
"error": "DAEMON_UNAVAILABLE: owned daemon is unavailable; retry later.",
|
|
235
|
+
}
|
|
281
236
|
|
|
282
237
|
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
283
238
|
async def recall(
|
|
@@ -378,22 +333,6 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
378
333
|
window=window or None,
|
|
379
334
|
)
|
|
380
335
|
if result.get("ok"):
|
|
381
|
-
# Record implicit feedback: every returned result is a recall_hit
|
|
382
|
-
try:
|
|
383
|
-
_record_recall_hits(
|
|
384
|
-
get_engine,
|
|
385
|
-
query,
|
|
386
|
-
result.get("results", []),
|
|
387
|
-
profile_id=str(result.get("profile", "")),
|
|
388
|
-
)
|
|
389
|
-
except Exception:
|
|
390
|
-
pass # Feedback is non-critical, never block recall
|
|
391
|
-
_emit_event("memory.recalled", {
|
|
392
|
-
"query": query[:80],
|
|
393
|
-
"result_count": result.get("result_count", 0),
|
|
394
|
-
"query_type": result.get("query_type", "unknown"),
|
|
395
|
-
"agent_id": agent_id,
|
|
396
|
-
}, source_agent=agent_id)
|
|
397
336
|
return {
|
|
398
337
|
"success": True,
|
|
399
338
|
"results": result.get("results", []),
|
|
@@ -16,13 +16,11 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
18
|
import logging
|
|
19
|
-
import sqlite3
|
|
20
|
-
from datetime import datetime, timezone
|
|
21
|
-
from pathlib import Path
|
|
22
19
|
from typing import Callable
|
|
23
20
|
|
|
24
21
|
from mcp.types import ToolAnnotations
|
|
25
22
|
from superlocalmemory.infra.data_root import state_path
|
|
23
|
+
from superlocalmemory.storage.read_connection import ReadConnectionFactory
|
|
26
24
|
|
|
27
25
|
logger = logging.getLogger(__name__)
|
|
28
26
|
|
|
@@ -146,10 +144,8 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
|
|
|
146
144
|
try:
|
|
147
145
|
engine = get_engine()
|
|
148
146
|
profile_id = engine.profile_id if engine else "default"
|
|
149
|
-
db_path =
|
|
150
|
-
|
|
151
|
-
conn = sqlite3.connect(db_path, timeout=10)
|
|
152
|
-
conn.row_factory = sqlite3.Row
|
|
147
|
+
db_path = state_path("memory.db")
|
|
148
|
+
conn = ReadConnectionFactory(db_path).open()
|
|
153
149
|
|
|
154
150
|
# Gather per-skill invocation stats from tool_events
|
|
155
151
|
# Skills are logged as tool_name='Skill' with actual skill name in input_summary
|
|
@@ -274,9 +270,8 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
|
|
|
274
270
|
skill_name: Specific skill name (empty = all skills)
|
|
275
271
|
"""
|
|
276
272
|
try:
|
|
277
|
-
db_path =
|
|
278
|
-
conn =
|
|
279
|
-
conn.row_factory = sqlite3.Row
|
|
273
|
+
db_path = state_path("memory.db")
|
|
274
|
+
conn = ReadConnectionFactory(db_path).open()
|
|
280
275
|
|
|
281
276
|
if skill_name:
|
|
282
277
|
rows = conn.execute(
|