superlocalmemory 3.8.10 → 3.8.12

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 (54) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +7 -3
  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/cli/commands.py +28 -6
  34. package/src/superlocalmemory/cli/daemon.py +219 -10
  35. package/src/superlocalmemory/cli/setup_wizard.py +45 -1
  36. package/src/superlocalmemory/core/component_registry.py +25 -0
  37. package/src/superlocalmemory/core/config.py +35 -1
  38. package/src/superlocalmemory/core/engine_wiring.py +81 -5
  39. package/src/superlocalmemory/core/recall_pipeline.py +25 -4
  40. package/src/superlocalmemory/core/reranker_worker.py +78 -17
  41. package/src/superlocalmemory/infra/daemon_identity.py +16 -0
  42. package/src/superlocalmemory/infra/process_identity.py +180 -0
  43. package/src/superlocalmemory/learning/feedback.py +328 -27
  44. package/src/superlocalmemory/learning/legacy_migration.py +45 -4
  45. package/src/superlocalmemory/learning/pattern_miner.py +31 -11
  46. package/src/superlocalmemory/mcp/_daemon_proxy.py +23 -1
  47. package/src/superlocalmemory/mcp/tools_active.py +179 -17
  48. package/src/superlocalmemory/mcp/tools_core.py +6 -5
  49. package/src/superlocalmemory/retrieval/remote_reranker.py +636 -0
  50. package/src/superlocalmemory/retrieval/reranker.py +52 -5
  51. package/src/superlocalmemory/server/unified_daemon.py +4 -0
  52. package/src/superlocalmemory/storage/migration_runner.py +9 -0
  53. package/src/superlocalmemory/storage/migrations/M033_learning_feedback_channel.py +77 -0
  54. package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
@@ -84,6 +84,21 @@ _WARMUP_MAX_ATTEMPTS = int(os.environ.get("SLM_RERANKER_WARMUP_ATTEMPTS", "5"))
84
84
  _WARMUP_RETRY_BACKOFF_S = float(os.environ.get("SLM_RERANKER_WARMUP_BACKOFF", "3"))
85
85
 
86
86
 
87
+ # Substrings that mark a load failure as a configuration problem rather than a
88
+ # transient one. Retrying these can never succeed, so the warmup aborts on the
89
+ # first occurrence instead of spending _WARMUP_MAX_ATTEMPTS × backoff on them.
90
+ _PERMANENT_LOAD_ERROR_MARKERS = (
91
+ "unknown backend",
92
+ "sentence-transformers is not installed",
93
+ )
94
+
95
+
96
+ def _is_permanent_load_error(error: str) -> bool:
97
+ """True when a worker load error cannot be fixed by retrying."""
98
+ lowered = (error or "").lower()
99
+ return any(m in lowered for m in _PERMANENT_LOAD_ERROR_MARKERS)
100
+
101
+
87
102
  class CrossEncoderReranker:
88
103
  """Rerank candidate facts using a local cross-encoder model.
89
104
 
@@ -201,11 +216,43 @@ class CrossEncoderReranker:
201
216
  resp.get("warmup_inference", False),
202
217
  )
203
218
  return
204
- logger.warning(
205
- "Reranker warmup attempt %d/%d did not confirm "
206
- "ready (timeout=%ds); retrying",
207
- attempt, _WARMUP_MAX_ATTEMPTS, _WARMUP_LOAD_TIMEOUT,
208
- )
219
+ # v3.8.11 (issue #103): this used to report
220
+ # "(timeout=90s)" for EVERY failure, including loads
221
+ # that failed instantly. A user whose retries were 3s
222
+ # apart was told each one timed out after 90s, and the
223
+ # worker's actual error was never printed at all.
224
+ # Distinguish the two cases and surface the real cause.
225
+ if resp is None:
226
+ logger.warning(
227
+ "Reranker warmup attempt %d/%d: no response "
228
+ "from worker within %ds; retrying",
229
+ attempt, _WARMUP_MAX_ATTEMPTS,
230
+ _WARMUP_LOAD_TIMEOUT,
231
+ )
232
+ else:
233
+ load_error = (
234
+ resp.get("error")
235
+ or "worker reported not-ready without an error"
236
+ )
237
+ # A misconfiguration cannot fix itself. Retrying a
238
+ # bad backend name or a missing dependency four
239
+ # more times burns ~7.5 minutes of daemon startup
240
+ # to reach the same answer (issue #103). Fail fast
241
+ # and say exactly what to change.
242
+ if _is_permanent_load_error(load_error):
243
+ logger.error(
244
+ "Reranker disabled — configuration error: "
245
+ "%s. Not retrying. Fix the config or set "
246
+ "retrieval.use_cross_encoder=false; recall "
247
+ "continues with fusion scores.",
248
+ load_error,
249
+ )
250
+ return
251
+ logger.warning(
252
+ "Reranker warmup attempt %d/%d failed: %s; "
253
+ "retrying",
254
+ attempt, _WARMUP_MAX_ATTEMPTS, load_error,
255
+ )
209
256
 
210
257
  if attempt < _WARMUP_MAX_ATTEMPTS and not self._model_loaded:
211
258
  if self._shutdown_event.wait(
@@ -534,6 +534,10 @@ def _recall_keyword_fallback(engine, query: str, limit: int) -> dict:
534
534
  "results": results,
535
535
  "count": len(results),
536
536
  "no_confident_match": True,
537
+ # PR #101: every other recall path returns this key, so clients format
538
+ # it unconditionally. Omitting it here made the degraded path — the one
539
+ # that fires when recall is ALREADY struggling — crash the CLI.
540
+ "retrieval_time_ms": 0,
537
541
  }
538
542
 
539
543
  # v3.4.52: Embedding model warm state. Set to True by the async pre-warm
@@ -128,6 +128,9 @@ from superlocalmemory.storage.migrations import (
128
128
  from superlocalmemory.storage.migrations import (
129
129
  M032_write_coordinator_admission as _M032,
130
130
  )
131
+ from superlocalmemory.storage.migrations import (
132
+ M033_learning_feedback_channel as _M033,
133
+ )
131
134
 
132
135
  # Map migration name → module (used for the optional ``verify(conn)`` hook
133
136
  # that lets the runner detect "already applied" state when an idempotent
@@ -164,6 +167,7 @@ _MODULES = {
164
167
  _M030.NAME: _M030,
165
168
  _M031.NAME: _M031,
166
169
  _M032.NAME: _M032,
170
+ _M033.NAME: _M033,
167
171
  }
168
172
 
169
173
  logger = logging.getLogger(__name__)
@@ -220,6 +224,11 @@ MIGRATIONS: list[Migration] = [
220
224
  # observations for ShadowTest persistence across daemon restart.
221
225
  Migration(name=_M012.NAME, db_target="learning", ddl=_M012.DDL,
222
226
  dependencies=(_M003.NAME,)),
227
+ # M033 adds learning_feedback.channel, which pattern_miner has always
228
+ # queried but which no schema ever defined. Its DDL creates the table
229
+ # when absent, so it needs no dependency beyond the migration log.
230
+ Migration(name=_M033.NAME, db_target="learning", ddl=_M033.DDL,
231
+ dependencies=(_M003.NAME,)),
223
232
  Migration(name=_M004.NAME, db_target="memory", ddl=_M004.DDL),
224
233
  # M007 creates pending_outcomes (memory.db, LLD-00 §1.2).
225
234
  Migration(name=_M007.NAME, db_target="memory", ddl=_M007.DDL),
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.8.11
4
+
5
+ """M033 — add the ``channel`` column to ``learning_feedback``.
6
+
7
+ ``pattern_miner._mine_channel_and_coretrieval`` has always executed::
8
+
9
+ SELECT channel, COUNT(*) AS cnt, AVG(signal_value) AS avg_signal
10
+ FROM learning_feedback GROUP BY channel
11
+
12
+ but ``channel`` was never defined on the table. Every database therefore
13
+ raised ``sqlite3.OperationalError: no such column: channel``. That error was
14
+ caught by the miner's outer ``except Exception`` and logged at DEBUG, so the
15
+ failure was invisible — and because the channel query runs FIRST inside that
16
+ try block, it also aborted the co-retrieval mining below it. One missing
17
+ column silently disabled two pattern types (issue #102, "Patterns learned
18
+ remains 0" despite a restored backup: restoring rows cannot fix a schema gap).
19
+
20
+ Additive only — ``ALTER TABLE ADD COLUMN`` with a default. No data loss and no
21
+ type changes; existing rows get ``'unknown'``, which groups cleanly rather than
22
+ being dropped by the miner's ``GROUP BY``.
23
+
24
+ ``learning_feedback`` is bootstrapped at runtime by
25
+ ``learning.feedback.FeedbackCollector._ensure_schema`` rather than by a
26
+ migration, so the DDL below CREATEs it (without ``channel``) when absent
27
+ before the ALTER. That keeps all three states correct:
28
+
29
+ - table missing -> CREATE, then ALTER adds ``channel``
30
+ - table present, no column -> CREATE is a no-op, ALTER adds ``channel``
31
+ - table already migrated -> ``verify()`` returns True, runner skips entirely
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import sqlite3
37
+
38
+ NAME = "M033_learning_feedback_channel"
39
+ DB_TARGET = "learning"
40
+
41
+
42
+ def verify(conn: sqlite3.Connection) -> bool:
43
+ """Return True if ``learning_feedback.channel`` already exists."""
44
+ try:
45
+ cols = {
46
+ row[1] for row in
47
+ conn.execute("PRAGMA table_info(learning_feedback)").fetchall()
48
+ }
49
+ except sqlite3.Error:
50
+ return False
51
+ # An empty set means the table does not exist yet — not migrated.
52
+ return bool(cols) and "channel" in cols
53
+
54
+
55
+ DDL = """
56
+ BEGIN IMMEDIATE;
57
+
58
+ CREATE TABLE IF NOT EXISTS learning_feedback (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ profile_id TEXT NOT NULL,
61
+ fact_id TEXT NOT NULL,
62
+ signal_type TEXT NOT NULL,
63
+ signal_value REAL NOT NULL,
64
+ query_hash TEXT,
65
+ created_at TEXT NOT NULL,
66
+ metadata TEXT
67
+ );
68
+
69
+ ALTER TABLE learning_feedback ADD COLUMN channel TEXT DEFAULT 'unknown';
70
+
71
+ CREATE INDEX IF NOT EXISTS idx_feedback_profile
72
+ ON learning_feedback (profile_id, created_at DESC);
73
+ CREATE INDEX IF NOT EXISTS idx_feedback_channel
74
+ ON learning_feedback (profile_id, channel);
75
+
76
+ COMMIT;
77
+ """
@@ -28,6 +28,7 @@ from . import (
28
28
  M020_model_state_integrity,
29
29
  M029_behavioral_history_indexes,
30
30
  M030_entity_explorer_indexes,
31
+ M033_learning_feedback_channel,
31
32
  )
32
33
 
33
34
  # ---------------------------------------------------------------------------
@@ -81,6 +82,7 @@ __all__ = (
81
82
  "M020_model_state_integrity",
82
83
  "M029_behavioral_history_indexes",
83
84
  "M030_entity_explorer_indexes",
85
+ "M033_learning_feedback_channel",
84
86
  # Legacy re-exports (backward compat):
85
87
  "CURRENT_SCHEMA_VERSION",
86
88
  "get_schema_version",