superlocalmemory 3.6.13 → 3.6.15
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/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +28 -0
- package/README.md +189 -740
- package/package.json +12 -5
- package/plugin/.claude-plugin/plugin.json +20 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/CLAUDE.md +44 -0
- package/plugin/_GENERATED.md +6 -0
- package/plugin/agents/slm-memory-advisor.md +44 -0
- package/plugin/agents/slm-optimize-advisor.md +38 -0
- package/plugin/hooks/hooks.json +14 -0
- package/plugin/requirements.txt +1 -0
- package/plugin/scripts/ensure-venv.bat +122 -0
- package/plugin/scripts/ensure-venv.sh +105 -0
- package/plugin/scripts/slm-launch +15 -0
- package/plugin/scripts/slm-launch.bat +17 -0
- package/plugin/settings.json +16 -0
- package/plugin/skills/slm-cache/SKILL.md +140 -0
- package/plugin/skills/slm-compress/SKILL.md +143 -0
- package/plugin/skills/slm-graph/SKILL.md +300 -0
- package/plugin/skills/slm-recall/SKILL.md +204 -0
- package/plugin/skills/slm-remember/SKILL.md +194 -0
- package/plugin/skills/slm-session/SKILL.md +207 -0
- package/plugin/skills/slm-status/SKILL.md +149 -0
- package/plugin-src/.mcp.json +12 -0
- package/plugin-src/agents/slm-memory-advisor.md +44 -0
- package/plugin-src/agents/slm-optimize-advisor.md +38 -0
- package/plugin-src/commands/slm-optimize.md +22 -0
- package/plugin-src/commands/slm-recall.md +16 -0
- package/plugin-src/commands/slm-remember.md +16 -0
- package/plugin-src/commands/slm-status.md +15 -0
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +14 -0
- package/plugin-src/manifest.json +25 -0
- package/plugin-src/requirements.txt +1 -0
- package/plugin-src/rules/AGENTS.md +91 -0
- package/plugin-src/rules/CLAUDE.md.fragment +44 -0
- package/plugin-src/scripts/ensure-venv.bat +122 -0
- package/plugin-src/scripts/ensure-venv.sh +105 -0
- package/plugin-src/scripts/slm-launch +15 -0
- package/plugin-src/scripts/slm-launch.bat +17 -0
- package/plugin-src/settings.json +16 -0
- package/plugin-src/skills/slm-cache/SKILL.md +140 -0
- package/plugin-src/skills/slm-compress/SKILL.md +143 -0
- package/plugin-src/skills/slm-graph/SKILL.md +300 -0
- package/plugin-src/skills/slm-recall/SKILL.md +204 -0
- package/plugin-src/skills/slm-remember/SKILL.md +194 -0
- package/plugin-src/skills/slm-session/SKILL.md +207 -0
- package/plugin-src/skills/slm-status/SKILL.md +149 -0
- package/pyproject.toml +6 -2
- package/scripts/__tests__/build-plugin.test.mjs +613 -0
- package/scripts/_savings_math.py +270 -0
- package/scripts/build-plugin.js +742 -0
- package/scripts/dogfood_savings.py +490 -0
- package/scripts/install-skills.ps1 +4 -334
- package/scripts/install-skills.sh +4 -435
- package/scripts/postinstall-interactive.js +0 -27
- package/scripts/postinstall.js +21 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +115 -0
- package/src/superlocalmemory/cli/commands.py +439 -41
- package/src/superlocalmemory/cli/main.py +92 -4
- package/src/superlocalmemory/cli/setup_wizard.py +47 -6
- package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
- package/src/superlocalmemory/core/config.py +194 -9
- package/src/superlocalmemory/core/embeddings.py +10 -5
- package/src/superlocalmemory/core/engine.py +76 -5
- package/src/superlocalmemory/core/fact_consolidator.py +20 -3
- package/src/superlocalmemory/core/platform_utils.py +8 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/recall_worker.py +7 -0
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/core/worker_pool.py +14 -2
- package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
- package/src/superlocalmemory/hooks/portable_kit.py +506 -0
- package/src/superlocalmemory/hooks/session_registry.py +8 -4
- package/src/superlocalmemory/infra/cloud_backup.py +99 -23
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
- package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
- package/src/superlocalmemory/mcp/server.py +75 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
- package/src/superlocalmemory/mcp/tools_core.py +37 -4
- package/src/superlocalmemory/mcp/tools_v3.py +6 -1
- package/src/superlocalmemory/mcp/tools_v33.py +8 -4
- package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
- package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
- package/src/superlocalmemory/optimize/cache/manager.py +92 -6
- package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
- package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
- package/src/superlocalmemory/optimize/compress/router.py +46 -13
- package/src/superlocalmemory/optimize/config/schema.py +6 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
- package/src/superlocalmemory/optimize/proxy/server.py +11 -0
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
- package/src/superlocalmemory/optimize/storage/db.py +30 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
- package/src/superlocalmemory/retrieval/engine.py +36 -3
- package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
- package/src/superlocalmemory/server/recall_serializer.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +156 -16
- package/src/superlocalmemory/storage/database.py +215 -43
- package/src/superlocalmemory/storage/migration_runner.py +17 -1
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
- package/src/superlocalmemory/storage/models.py +10 -0
- package/src/superlocalmemory/storage/schema.py +15 -10
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
- package/src/superlocalmemory/ui/index.html +2 -2
- package/src/superlocalmemory/ui/js/core.js +98 -0
- package/src/superlocalmemory/ui/js/dashboard.js +8 -1
- package/src/superlocalmemory/ui/js/ide-status.js +16 -3
- package/src/superlocalmemory/ui/js/math-health.js +15 -3
- package/src/superlocalmemory/ui/js/optimize.js +18 -2
- package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
- package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/ide/skills/slm-build-graph/SKILL.md +0 -423
- package/ide/skills/slm-list-recent/SKILL.md +0 -348
- package/ide/skills/slm-recall/SKILL.md +0 -326
- package/ide/skills/slm-remember/SKILL.md +0 -194
- package/ide/skills/slm-show-patterns/SKILL.md +0 -224
- package/ide/skills/slm-status/SKILL.md +0 -363
- package/ide/skills/slm-switch-profile/SKILL.md +0 -442
- package/skills/slm-build-graph/SKILL.md +0 -423
- package/skills/slm-list-recent/SKILL.md +0 -348
- package/skills/slm-optimize/README.md +0 -55
- package/skills/slm-optimize/SKILL.md +0 -139
- package/skills/slm-recall/SKILL.md +0 -343
- package/skills/slm-remember/SKILL.md +0 -194
- package/skills/slm-show-patterns/SKILL.md +0 -224
- package/skills/slm-status/SKILL.md +0 -363
- package/skills/slm-switch-profile/SKILL.md +0 -442
- package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
- package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
|
@@ -24,6 +24,8 @@ _os.environ.setdefault('TORCH_DEVICE', 'cpu')
|
|
|
24
24
|
import argparse
|
|
25
25
|
import sys
|
|
26
26
|
|
|
27
|
+
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
|
|
28
|
+
|
|
27
29
|
_HELP_EPILOG = """\
|
|
28
30
|
operating modes:
|
|
29
31
|
Mode A Local Guardian — Zero cloud, zero LLM. All processing stays on
|
|
@@ -76,6 +78,17 @@ def main() -> None:
|
|
|
76
78
|
handle_hook(sys.argv[2])
|
|
77
79
|
return
|
|
78
80
|
|
|
81
|
+
# WP-07: lazy first-run init — runs after hook/mcp fast-paths so stdout
|
|
82
|
+
# is never polluted on those paths (CRIT-3, MCP JSON-RPC purity).
|
|
83
|
+
# Guarded: any failure must not crash the CLI (AC4).
|
|
84
|
+
_is_mcp_cmd = len(sys.argv) >= 2 and sys.argv[1] == "mcp"
|
|
85
|
+
if not _is_mcp_cmd:
|
|
86
|
+
try:
|
|
87
|
+
from superlocalmemory.cli._lazy_init import _ensure_initialized
|
|
88
|
+
_ensure_initialized()
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
79
92
|
from superlocalmemory.cli.json_output import _get_version
|
|
80
93
|
_ver = _get_version()
|
|
81
94
|
|
|
@@ -101,8 +114,9 @@ def main() -> None:
|
|
|
101
114
|
from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
|
|
102
115
|
migrate_if_safe as _migrate_if_safe,
|
|
103
116
|
)
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
# WP-07: route through slm_home() so all 3 env aliases are honoured.
|
|
118
|
+
from superlocalmemory.cli._lazy_init import slm_home as _slm_home
|
|
119
|
+
_data = _slm_home()
|
|
106
120
|
_res = _migrate_if_safe(_data)
|
|
107
121
|
if _res.get("status") == "deferred":
|
|
108
122
|
print(
|
|
@@ -140,6 +154,11 @@ def main() -> None:
|
|
|
140
154
|
"--gate", action="store_true",
|
|
141
155
|
help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
|
|
142
156
|
)
|
|
157
|
+
# WP-07: non-interactive auto setup (pip post-install, CI, scripts).
|
|
158
|
+
init_p.add_argument(
|
|
159
|
+
"--auto", action="store_true",
|
|
160
|
+
help="Non-interactive setup: mode A + hooks (no TTY required, for CI/scripts)",
|
|
161
|
+
)
|
|
143
162
|
|
|
144
163
|
setup_p = sub.add_parser("setup", help="Interactive first-time setup wizard")
|
|
145
164
|
setup_p.add_argument(
|
|
@@ -159,11 +178,32 @@ def main() -> None:
|
|
|
159
178
|
)
|
|
160
179
|
|
|
161
180
|
connect_p = sub.add_parser("connect", help="Auto-configure IDE integrations (17+ IDEs)")
|
|
162
|
-
connect_p.add_argument("ide", nargs="?", help="Specific IDE to configure")
|
|
181
|
+
connect_p.add_argument("ide", nargs="?", help="Specific IDE to configure (e.g. cursor, codex, continue)")
|
|
163
182
|
connect_p.add_argument(
|
|
164
183
|
"--list", action="store_true", help="List all supported IDEs",
|
|
165
184
|
)
|
|
166
185
|
connect_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
186
|
+
# WP-08 CRIT-1: declare missing flags so cmd_connect can read them without getattr fallback
|
|
187
|
+
connect_p.add_argument(
|
|
188
|
+
"--here", action="store_true", default=False,
|
|
189
|
+
help="Write config relative to current working directory (project scope)",
|
|
190
|
+
)
|
|
191
|
+
connect_p.add_argument(
|
|
192
|
+
"--cross-platform", action="store_true", dest="cross_platform", default=False,
|
|
193
|
+
help="Use LLD-05 cross-platform adapter orchestrator",
|
|
194
|
+
)
|
|
195
|
+
connect_p.add_argument(
|
|
196
|
+
"--disable", metavar="ADAPTER", default=None,
|
|
197
|
+
help="Disable a specific cross-platform adapter by name",
|
|
198
|
+
)
|
|
199
|
+
connect_p.add_argument(
|
|
200
|
+
"--profile", metavar="PROFILE", default=None,
|
|
201
|
+
help="Inject SLM_MCP_PROFILE env var into the MCP server block (WP-01)",
|
|
202
|
+
)
|
|
203
|
+
connect_p.add_argument(
|
|
204
|
+
"--dry-run", action="store_true", dest="dry_run", default=False,
|
|
205
|
+
help="Show what would be written without making changes",
|
|
206
|
+
)
|
|
167
207
|
|
|
168
208
|
migrate_p = sub.add_parser("migrate", help="Migrate data from V2 to V3 schema")
|
|
169
209
|
migrate_p.add_argument(
|
|
@@ -195,12 +235,24 @@ def main() -> None:
|
|
|
195
235
|
"--sync", dest="sync_mode", action="store_true",
|
|
196
236
|
help="Wait for completion (default: async background processing)",
|
|
197
237
|
)
|
|
238
|
+
remember_p.add_argument(
|
|
239
|
+
"--scope", default=None, choices=("personal", "shared", "global"),
|
|
240
|
+
help="Memory scope: personal, shared, or global. Unset uses the "
|
|
241
|
+
"configured default_scope (personal). Shared memory is opt-in.",
|
|
242
|
+
)
|
|
243
|
+
remember_p.add_argument(
|
|
244
|
+
"--shared-with", default=None,
|
|
245
|
+
help="Comma-separated profile IDs for shared scope",
|
|
246
|
+
)
|
|
198
247
|
|
|
199
248
|
# v3.6.12 (parity-3): `search` is an alias of `recall` so the CLI has the
|
|
200
249
|
# same search verb the MCP exposes (handlers dict maps both to cmd_recall).
|
|
201
250
|
recall_p = sub.add_parser("recall", aliases=["search"], help="Semantic search with 4-channel retrieval")
|
|
202
251
|
recall_p.add_argument("query", help="Search query")
|
|
203
|
-
recall_p.add_argument(
|
|
252
|
+
recall_p.add_argument(
|
|
253
|
+
"--limit", type=int, default=CANONICAL_RECALL_LIMIT,
|
|
254
|
+
help=f"Max results (default {CANONICAL_RECALL_LIMIT})",
|
|
255
|
+
)
|
|
204
256
|
recall_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
205
257
|
recall_p.add_argument(
|
|
206
258
|
"--fast", action="store_true",
|
|
@@ -208,6 +260,26 @@ def main() -> None:
|
|
|
208
260
|
"Other 4 channels (semantic, lexical, temporal, structural) still run. "
|
|
209
261
|
"Use when you need recall before a tool call (e.g. before WebSearch).",
|
|
210
262
|
)
|
|
263
|
+
# v3.6.15: shared memory is opt-in. Unset (None) → resolve the configured
|
|
264
|
+
# default (recall_include_global/shared, both False by default). Explicit
|
|
265
|
+
# flags override per-call. default=None on BOTH members of each pair so the
|
|
266
|
+
# store_false's implicit default=True can't sneak back in.
|
|
267
|
+
recall_p.add_argument(
|
|
268
|
+
"--include-global", dest="include_global", action="store_true", default=None,
|
|
269
|
+
help="Include global-scope facts in retrieval (opt-in; default off)",
|
|
270
|
+
)
|
|
271
|
+
recall_p.add_argument(
|
|
272
|
+
"--no-global", dest="include_global", action="store_false", default=None,
|
|
273
|
+
help="Exclude global-scope facts from retrieval",
|
|
274
|
+
)
|
|
275
|
+
recall_p.add_argument(
|
|
276
|
+
"--include-shared", dest="include_shared", action="store_true", default=None,
|
|
277
|
+
help="Include facts shared with this profile (opt-in; default off)",
|
|
278
|
+
)
|
|
279
|
+
recall_p.add_argument(
|
|
280
|
+
"--no-shared", dest="include_shared", action="store_false", default=None,
|
|
281
|
+
help="Exclude shared-scope facts from retrieval",
|
|
282
|
+
)
|
|
211
283
|
|
|
212
284
|
forget_p = sub.add_parser("forget", help="Delete memories matching a query (fuzzy)")
|
|
213
285
|
forget_p.add_argument("query", help="Query to match for deletion")
|
|
@@ -337,6 +409,22 @@ def main() -> None:
|
|
|
337
409
|
)
|
|
338
410
|
ctx_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
339
411
|
|
|
412
|
+
# #49: local session open/close (for hooks; no model roundtrip)
|
|
413
|
+
session_p = sub.add_parser(
|
|
414
|
+
"session", help="Open/close a session locally (for hooks; no model roundtrip)"
|
|
415
|
+
)
|
|
416
|
+
session_sub = session_p.add_subparsers(dest="session_command", title="session actions")
|
|
417
|
+
sopen_p = session_sub.add_parser("open", help="Warm session context")
|
|
418
|
+
sopen_p.add_argument("--project-path", default="", help="Project path to derive the warm query")
|
|
419
|
+
sopen_p.add_argument("--query", default="", help="Explicit warm query")
|
|
420
|
+
sopen_p.add_argument("--max-results", type=int, default=10, help="Max memories to warm (default 10)")
|
|
421
|
+
sclose_p = session_sub.add_parser(
|
|
422
|
+
"close", help="Close session, create temporal summaries"
|
|
423
|
+
)
|
|
424
|
+
sclose_p.add_argument(
|
|
425
|
+
"--session-id", default="", help="Session to close (default: most recent)"
|
|
426
|
+
)
|
|
427
|
+
|
|
340
428
|
obs_p = sub.add_parser("observe", help="Auto-capture content (pipe or argument)")
|
|
341
429
|
obs_p.add_argument("content", nargs="?", default="", help="Content to evaluate")
|
|
342
430
|
|
|
@@ -28,7 +28,17 @@ from pathlib import Path
|
|
|
28
28
|
# Constants
|
|
29
29
|
# ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# WP-07: resolve via slm_home() so all 3 env aliases are honoured.
|
|
32
|
+
# Fallback keeps stdlib-only path if the import fails during early bootstrap.
|
|
33
|
+
def _resolve_slm_home() -> Path:
|
|
34
|
+
try:
|
|
35
|
+
from superlocalmemory.cli._lazy_init import slm_home
|
|
36
|
+
return slm_home()
|
|
37
|
+
except Exception:
|
|
38
|
+
return Path(os.environ.get("SL_MEMORY_PATH", "") or Path.home() / ".superlocalmemory")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_SLM_HOME = _resolve_slm_home()
|
|
32
42
|
_SETUP_MARKER = _SLM_HOME / ".setup-complete"
|
|
33
43
|
_EMBED_MODEL = "nomic-ai/nomic-embed-text-v1.5"
|
|
34
44
|
_RERANKER_MODEL = "cross-encoder/ms-marco-MiniLM-L-12-v2"
|
|
@@ -369,6 +379,33 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
369
379
|
mode_map = {"a": Mode.A, "b": Mode.B, "c": Mode.C}
|
|
370
380
|
config = SLMConfig.for_mode(mode_map[choice])
|
|
371
381
|
|
|
382
|
+
# -- Multi-scope (shared memory) opt-in — v3.6.15 --
|
|
383
|
+
# OFF by default: your memories stay private to this profile (3.6.14 behaviour).
|
|
384
|
+
# Enabling only turns ON recall VISIBILITY of other profiles' shared/global facts;
|
|
385
|
+
# your own writes still default to 'personal' (mark a memory shared/global per call
|
|
386
|
+
# with --scope). Existing users can flip this later in the `scope` section of
|
|
387
|
+
# mode_a/b/c.json. See docs/shared-memory.md.
|
|
388
|
+
print()
|
|
389
|
+
print(" Shared memory lets other local profiles' 'shared'/'global' memories")
|
|
390
|
+
print(" appear in your recall. It is OFF by default — your memories stay private.")
|
|
391
|
+
if interactive:
|
|
392
|
+
sm_choice = _prompt(
|
|
393
|
+
" See shared/global memories from other profiles by default? [y/N] (default: N): ",
|
|
394
|
+
"n",
|
|
395
|
+
).lower()
|
|
396
|
+
else:
|
|
397
|
+
sm_choice = "n"
|
|
398
|
+
if sm_choice in ("y", "yes"):
|
|
399
|
+
from superlocalmemory.core.config import ScopeConfig
|
|
400
|
+
config.scope = ScopeConfig(
|
|
401
|
+
default_scope="personal",
|
|
402
|
+
recall_include_global=True,
|
|
403
|
+
recall_include_shared=True,
|
|
404
|
+
)
|
|
405
|
+
print(" ✓ Shared-memory recall ENABLED (your own writes still default to personal).")
|
|
406
|
+
else:
|
|
407
|
+
print(" ✓ Shared memory OFF (default) — enable later in mode_*.json if needed.")
|
|
408
|
+
|
|
372
409
|
if choice == "b":
|
|
373
410
|
print()
|
|
374
411
|
if shutil.which("ollama"):
|
|
@@ -703,14 +740,18 @@ def check_first_use(command: str) -> None:
|
|
|
703
740
|
if is_setup_complete():
|
|
704
741
|
return
|
|
705
742
|
|
|
706
|
-
# Non-interactive: use defaults silently, don't block the command
|
|
743
|
+
# Non-interactive: use defaults silently, don't block the command.
|
|
744
|
+
# CRIT-1: only save config when it does NOT already exist — lazy-init may
|
|
745
|
+
# have already written a valid config.json; overwriting it here would clobber
|
|
746
|
+
# any lazy-init content (e.g. a pre-existing mode-A skeleton).
|
|
707
747
|
if not is_interactive():
|
|
708
|
-
# Just create config with defaults and mark complete
|
|
709
748
|
try:
|
|
710
|
-
from superlocalmemory.core.config import SLMConfig
|
|
749
|
+
from superlocalmemory.core.config import SLMConfig, DEFAULT_BASE_DIR
|
|
711
750
|
from superlocalmemory.storage.models import Mode
|
|
712
|
-
|
|
713
|
-
|
|
751
|
+
config_path = DEFAULT_BASE_DIR / "config.json"
|
|
752
|
+
if not config_path.exists():
|
|
753
|
+
cfg = SLMConfig.for_mode(Mode.A)
|
|
754
|
+
cfg.save(mode_change=True)
|
|
714
755
|
_mark_complete()
|
|
715
756
|
except Exception:
|
|
716
757
|
pass
|
|
@@ -342,15 +342,15 @@ class BackendOrchestrator:
|
|
|
342
342
|
count: int = 0, error: str = "") -> None:
|
|
343
343
|
self._backend_cache[name] = status
|
|
344
344
|
try:
|
|
345
|
-
|
|
345
|
+
# #47 fix: DatabaseManager has no `.conn`; execute() commits itself.
|
|
346
|
+
self._db.execute(
|
|
346
347
|
"INSERT OR REPLACE INTO backend_status "
|
|
347
348
|
"(backend_name, status, record_count, error_message, last_sync_at) "
|
|
348
349
|
"VALUES (?, ?, ?, ?, datetime('now'))",
|
|
349
350
|
(name, status, count, error),
|
|
350
351
|
)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
pass
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
logger.debug("backend_status update failed for %s: %s", name, exc)
|
|
354
354
|
|
|
355
355
|
# ------------------------------------------------------------------
|
|
356
356
|
# Internal: Schema
|
|
@@ -361,10 +361,14 @@ class BackendOrchestrator:
|
|
|
361
361
|
from superlocalmemory.storage.schema_v345 import (
|
|
362
362
|
apply_migration, schema_version_applied,
|
|
363
363
|
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
# #47 fix: use raw_connection() — DatabaseManager has no `.conn`,
|
|
365
|
+
# so the old code raised AttributeError that was silently swallowed,
|
|
366
|
+
# leaving the v3.4.5 migration (access_count_30d) permanently unapplied.
|
|
367
|
+
with self._db.raw_connection() as conn:
|
|
368
|
+
if not schema_version_applied(conn):
|
|
369
|
+
result = apply_migration(conn)
|
|
370
|
+
if result.get("errors"):
|
|
371
|
+
logger.warning("Schema v3.4.5 had errors: %s", result["errors"])
|
|
368
372
|
except ImportError:
|
|
369
373
|
logger.debug("schema_v345 not found — skipping")
|
|
370
374
|
except Exception as exc:
|
|
@@ -21,6 +21,16 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
from superlocalmemory.storage.models import Mode
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Canonical limits (WP-02 — single source of truth across all surfaces)
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
#: Default number of results returned by recall across MCP, CLI, daemon, and
|
|
29
|
+
#: engine. All surfaces bind their ``limit`` defaults to this constant so a
|
|
30
|
+
#: single change is sufficient to keep the full stack in sync.
|
|
31
|
+
CANONICAL_RECALL_LIMIT: int = 20
|
|
32
|
+
|
|
33
|
+
|
|
24
34
|
# ---------------------------------------------------------------------------
|
|
25
35
|
# Default Paths
|
|
26
36
|
# ---------------------------------------------------------------------------
|
|
@@ -119,6 +129,79 @@ class ChannelWeights:
|
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Scope Weights
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class ScopeWeights:
|
|
138
|
+
"""RRF fusion weights for multi-scope retrieval.
|
|
139
|
+
|
|
140
|
+
Personal scope has highest weight (most relevant to current profile).
|
|
141
|
+
Shared scope has medium weight (team/group memories).
|
|
142
|
+
Global scope has lowest weight (public/common knowledge).
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
personal: float = 1.0
|
|
146
|
+
shared: float = 0.7
|
|
147
|
+
global_: float = 0.5 # trailing underscore avoids Python keyword
|
|
148
|
+
|
|
149
|
+
def __post_init__(self) -> None:
|
|
150
|
+
for name in ("personal", "shared", "global_"):
|
|
151
|
+
val = getattr(self, name)
|
|
152
|
+
if val < 0:
|
|
153
|
+
raise ValueError(f"ScopeWeights values must be non-negative, got {name}={val}")
|
|
154
|
+
|
|
155
|
+
def as_dict(self) -> dict[str, float]:
|
|
156
|
+
return {"personal": self.personal, "shared": self.shared, "global": self.global_}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Scope Config (multi-scope memory behaviour defaults)
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
_VALID_SCOPES = ("personal", "shared", "global")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class ScopeConfig:
|
|
168
|
+
"""User-facing defaults for multi-scope (shared) memory.
|
|
169
|
+
|
|
170
|
+
SHARED MEMORY IS OPT-IN — NOT a default feature (v3.6.15 product decision).
|
|
171
|
+
The defaults below make a fresh / unconfigured install behave EXACTLY like
|
|
172
|
+
3.6.14: every write is ``personal`` and recall returns only this profile's
|
|
173
|
+
own facts. Another profile's ``global``/``shared`` facts never leak into
|
|
174
|
+
recall until the user explicitly turns sharing on.
|
|
175
|
+
|
|
176
|
+
- ``default_scope='personal'`` → writes stay private by default;
|
|
177
|
+
- ``recall_include_global=False`` → don't surface other profiles' global;
|
|
178
|
+
- ``recall_include_shared=False`` → don't surface facts shared *to* me.
|
|
179
|
+
|
|
180
|
+
Turning it on is a deliberate act, done either per-call (``--scope`` /
|
|
181
|
+
``--include-global`` / MCP args) or persistently by editing config.json /
|
|
182
|
+
mode_a|b|c.json (the installer can also write the choice). The CLI/MCP
|
|
183
|
+
boundary passes ``None`` ("not specified") so the engine resolves these
|
|
184
|
+
config values as the effective default.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
default_scope: str = "personal" # scope assigned to new memories
|
|
188
|
+
recall_include_global: bool = False # surface scope='global' facts in recall
|
|
189
|
+
recall_include_shared: bool = False # surface scope='shared' facts in recall
|
|
190
|
+
|
|
191
|
+
def __post_init__(self) -> None:
|
|
192
|
+
if self.default_scope not in _VALID_SCOPES:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"default_scope must be one of {_VALID_SCOPES}, got {self.default_scope!r}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def as_dict(self) -> dict:
|
|
198
|
+
return {
|
|
199
|
+
"default_scope": self.default_scope,
|
|
200
|
+
"recall_include_global": self.recall_include_global,
|
|
201
|
+
"recall_include_shared": self.recall_include_shared,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
122
205
|
# ---------------------------------------------------------------------------
|
|
123
206
|
# Encoding Config
|
|
124
207
|
# ---------------------------------------------------------------------------
|
|
@@ -692,6 +775,8 @@ class SLMConfig:
|
|
|
692
775
|
embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
|
|
693
776
|
llm: LLMConfig = field(default_factory=LLMConfig)
|
|
694
777
|
channel_weights: ChannelWeights = field(default_factory=ChannelWeights)
|
|
778
|
+
scope_weights: ScopeWeights = field(default_factory=ScopeWeights)
|
|
779
|
+
scope: ScopeConfig = field(default_factory=ScopeConfig)
|
|
695
780
|
encoding: EncodingConfig = field(default_factory=EncodingConfig)
|
|
696
781
|
retrieval: RetrievalConfig = field(default_factory=RetrievalConfig)
|
|
697
782
|
math: MathConfig = field(default_factory=MathConfig)
|
|
@@ -739,17 +824,32 @@ class SLMConfig:
|
|
|
739
824
|
@classmethod
|
|
740
825
|
def load(cls, config_path: Path | None = None) -> SLMConfig:
|
|
741
826
|
"""Load config from JSON file. Returns default Mode A if file doesn't exist."""
|
|
742
|
-
|
|
827
|
+
# WP-07: resolve base dir through slm_home() at call time so all 3 env
|
|
828
|
+
# aliases (SLM_DATA_DIR → SL_MEMORY_PATH → SLM_HOME) are honoured.
|
|
829
|
+
try:
|
|
830
|
+
from superlocalmemory.cli._lazy_init import slm_home as _slm_home
|
|
831
|
+
_runtime_base = _slm_home()
|
|
832
|
+
except Exception:
|
|
833
|
+
_runtime_base = DEFAULT_BASE_DIR
|
|
834
|
+
path = config_path or (_runtime_base / "config.json")
|
|
743
835
|
if not path.exists():
|
|
744
836
|
return cls.for_mode(Mode.A)
|
|
745
837
|
import json
|
|
746
|
-
|
|
838
|
+
try:
|
|
839
|
+
data = json.loads(path.read_text())
|
|
840
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
841
|
+
# An already-corrupt/truncated config.json must NOT brick every `slm`
|
|
842
|
+
# call — degrade to the Mode-A default and warn rather than raise.
|
|
843
|
+
logger.warning(
|
|
844
|
+
"config.json unreadable/corrupt (%s) — using Mode A default", exc
|
|
845
|
+
)
|
|
846
|
+
return cls.for_mode(Mode.A)
|
|
747
847
|
mode = Mode(data.get("mode", "a"))
|
|
748
848
|
llm_data = data.get("llm", {})
|
|
749
849
|
emb_data = data.get("embedding", {})
|
|
750
850
|
# V3.5.9: read base_dir before constructing config so for_mode() builds
|
|
751
851
|
# db_path from the user's directory, not DEFAULT_BASE_DIR.
|
|
752
|
-
raw_base_dir = Path(data.get("base_dir", str(
|
|
852
|
+
raw_base_dir = Path(data.get("base_dir", str(_runtime_base)))
|
|
753
853
|
config = cls.for_mode(
|
|
754
854
|
mode,
|
|
755
855
|
llm_provider=llm_data.get("provider", ""),
|
|
@@ -835,6 +935,36 @@ class SLMConfig:
|
|
|
835
935
|
prestage_max_response_bytes=int(inj.get("prestage_max_response_bytes", 64 * 1024)),
|
|
836
936
|
)
|
|
837
937
|
|
|
938
|
+
# Multi-scope memory: scope weights
|
|
939
|
+
sw = data.get("scope_weights", {})
|
|
940
|
+
if sw:
|
|
941
|
+
# v3.6.15: a malformed value (e.g. negative weight) must NOT brick
|
|
942
|
+
# every `slm` command via an uncaught ValueError out of load().
|
|
943
|
+
# Fall back to defaults and warn — the config is recoverable.
|
|
944
|
+
try:
|
|
945
|
+
config.scope_weights = ScopeWeights(**{
|
|
946
|
+
k: v for k, v in sw.items()
|
|
947
|
+
if k in ScopeWeights.__dataclass_fields__
|
|
948
|
+
})
|
|
949
|
+
except (ValueError, TypeError) as exc:
|
|
950
|
+
logger.warning(
|
|
951
|
+
"Ignoring invalid scope_weights in config (%s) — using defaults", exc
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
# Multi-scope memory: behaviour defaults (default scope + recall visibility)
|
|
955
|
+
sc = data.get("scope", {})
|
|
956
|
+
if sc:
|
|
957
|
+
# Same guard: a typo'd default_scope must not crash the whole CLI.
|
|
958
|
+
try:
|
|
959
|
+
config.scope = ScopeConfig(**{
|
|
960
|
+
k: v for k, v in sc.items()
|
|
961
|
+
if k in ScopeConfig.__dataclass_fields__
|
|
962
|
+
})
|
|
963
|
+
except (ValueError, TypeError) as exc:
|
|
964
|
+
logger.warning(
|
|
965
|
+
"Ignoring invalid scope config (%s) — using shared-off defaults", exc
|
|
966
|
+
)
|
|
967
|
+
|
|
838
968
|
return config
|
|
839
969
|
|
|
840
970
|
def save(
|
|
@@ -927,12 +1057,27 @@ class SLMConfig:
|
|
|
927
1057
|
"prestage_max_response_bytes": self.injection.prestage_max_response_bytes,
|
|
928
1058
|
}
|
|
929
1059
|
|
|
1060
|
+
# Multi-scope memory: scope weights
|
|
1061
|
+
data["scope_weights"] = {
|
|
1062
|
+
"personal": self.scope_weights.personal,
|
|
1063
|
+
"shared": self.scope_weights.shared,
|
|
1064
|
+
"global_": self.scope_weights.global_,
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
# Multi-scope memory: behaviour defaults
|
|
1068
|
+
data["scope"] = self.scope.as_dict()
|
|
1069
|
+
|
|
930
1070
|
# Preserve existing V3.3 config sections that aren't in for_mode()
|
|
931
1071
|
for key in ("forgetting", "quantization", "sagq", "embedding_signature", "auto_invoke"):
|
|
932
1072
|
if key in existing:
|
|
933
1073
|
data[key] = existing[key]
|
|
934
1074
|
|
|
935
|
-
|
|
1075
|
+
# Atomic write: a crash mid-write must NOT leave a truncated/corrupt
|
|
1076
|
+
# config.json (which would make every subsequent `slm` call fail to load).
|
|
1077
|
+
import os as _os
|
|
1078
|
+
_tmp = path.with_suffix(path.suffix + ".tmp")
|
|
1079
|
+
_tmp.write_text(json.dumps(data, indent=2))
|
|
1080
|
+
_os.replace(_tmp, path)
|
|
936
1081
|
|
|
937
1082
|
@staticmethod
|
|
938
1083
|
def provider_presets() -> dict[str, dict[str, str]]:
|
|
@@ -987,7 +1132,15 @@ class SLMConfig:
|
|
|
987
1132
|
embedding_dimension: int = 0,
|
|
988
1133
|
) -> SLMConfig:
|
|
989
1134
|
"""Create config with mode-appropriate defaults."""
|
|
990
|
-
|
|
1135
|
+
# WP-07: resolve base dir via slm_home() at call time when not explicit.
|
|
1136
|
+
if base_dir is None:
|
|
1137
|
+
try:
|
|
1138
|
+
from superlocalmemory.cli._lazy_init import slm_home as _slm_home
|
|
1139
|
+
_base = _slm_home()
|
|
1140
|
+
except Exception:
|
|
1141
|
+
_base = DEFAULT_BASE_DIR
|
|
1142
|
+
else:
|
|
1143
|
+
_base = base_dir
|
|
991
1144
|
|
|
992
1145
|
if mode == Mode.A:
|
|
993
1146
|
# V3.4.24: If user chose "openai" provider, honour their custom
|
|
@@ -1130,7 +1283,15 @@ class SLMConfig:
|
|
|
1130
1283
|
|
|
1131
1284
|
Returns ``\"b\"`` (the default) if the file doesn't exist.
|
|
1132
1285
|
"""
|
|
1133
|
-
|
|
1286
|
+
# WP-07: resolve via slm_home() at call time when base_dir not explicit.
|
|
1287
|
+
if base_dir is None:
|
|
1288
|
+
try:
|
|
1289
|
+
from superlocalmemory.cli._lazy_init import slm_home as _slm_home
|
|
1290
|
+
_base = _slm_home()
|
|
1291
|
+
except Exception:
|
|
1292
|
+
_base = DEFAULT_BASE_DIR
|
|
1293
|
+
else:
|
|
1294
|
+
_base = base_dir
|
|
1134
1295
|
_f = _base / CURRENT_MODE_FILE
|
|
1135
1296
|
try:
|
|
1136
1297
|
return _f.read_text(encoding="utf-8").strip().lower() or "b"
|
|
@@ -1140,7 +1301,15 @@ class SLMConfig:
|
|
|
1140
1301
|
@staticmethod
|
|
1141
1302
|
def write_current_mode(mode: str, base_dir: Path | None = None) -> None:
|
|
1142
1303
|
"""Write the current mode letter to ``current_mode``."""
|
|
1143
|
-
|
|
1304
|
+
# WP-07: resolve via slm_home() at call time when base_dir not explicit.
|
|
1305
|
+
if base_dir is None:
|
|
1306
|
+
try:
|
|
1307
|
+
from superlocalmemory.cli._lazy_init import slm_home as _slm_home
|
|
1308
|
+
_base = _slm_home()
|
|
1309
|
+
except Exception:
|
|
1310
|
+
_base = DEFAULT_BASE_DIR
|
|
1311
|
+
else:
|
|
1312
|
+
_base = base_dir
|
|
1144
1313
|
_base.mkdir(parents=True, exist_ok=True)
|
|
1145
1314
|
(_base / CURRENT_MODE_FILE).write_text(
|
|
1146
1315
|
mode.lower().strip(), encoding="utf-8",
|
|
@@ -1175,7 +1344,15 @@ class SLMConfig:
|
|
|
1175
1344
|
import dataclasses
|
|
1176
1345
|
|
|
1177
1346
|
from superlocalmemory.storage.models import Mode as _M
|
|
1178
|
-
|
|
1347
|
+
# WP-07: resolve via slm_home() at call time when base_dir not explicit.
|
|
1348
|
+
if base_dir is None:
|
|
1349
|
+
try:
|
|
1350
|
+
from superlocalmemory.cli._lazy_init import slm_home as _slm_home
|
|
1351
|
+
_base = _slm_home()
|
|
1352
|
+
except Exception:
|
|
1353
|
+
_base = DEFAULT_BASE_DIR
|
|
1354
|
+
else:
|
|
1355
|
+
_base = base_dir
|
|
1179
1356
|
_base.mkdir(parents=True, exist_ok=True)
|
|
1180
1357
|
|
|
1181
1358
|
old_config = cls.load(_base / "config.json")
|
|
@@ -1242,7 +1419,15 @@ class SLMConfig:
|
|
|
1242
1419
|
Called on daemon boot. Idempotent — if ``current_mode`` already
|
|
1243
1420
|
exists, this is a no-op. Returns True if migration was performed.
|
|
1244
1421
|
"""
|
|
1245
|
-
|
|
1422
|
+
# WP-07: resolve via slm_home() at call time when base_dir not explicit.
|
|
1423
|
+
if base_dir is None:
|
|
1424
|
+
try:
|
|
1425
|
+
from superlocalmemory.cli._lazy_init import slm_home as _slm_home
|
|
1426
|
+
_base = _slm_home()
|
|
1427
|
+
except Exception:
|
|
1428
|
+
_base = DEFAULT_BASE_DIR
|
|
1429
|
+
else:
|
|
1430
|
+
_base = base_dir
|
|
1246
1431
|
_current = _base / CURRENT_MODE_FILE
|
|
1247
1432
|
if _current.exists():
|
|
1248
1433
|
return False # already migrated
|
|
@@ -601,8 +601,13 @@ class EmbeddingService:
|
|
|
601
601
|
def _cloud_embed_batch(
|
|
602
602
|
self, texts: list[str], *, max_retries: int = 3,
|
|
603
603
|
) -> list[list[float]]:
|
|
604
|
-
"""Encode via Azure OpenAI embedding API with retry.
|
|
605
|
-
|
|
604
|
+
"""Encode via Azure OpenAI embedding API with retry.
|
|
605
|
+
|
|
606
|
+
V3.6.14: Reuses self._get_http_client() (shared persistent connection)
|
|
607
|
+
instead of creating a fresh httpx.Client per call/retry. resp.json()
|
|
608
|
+
is now called inside the try block while the response object is still
|
|
609
|
+
in scope, eliminating reliance on httpx body-buffering after close.
|
|
610
|
+
"""
|
|
606
611
|
url = (
|
|
607
612
|
f"{self._config.api_endpoint.rstrip('/')}/openai/deployments/"
|
|
608
613
|
f"{self._config.deployment_name}/embeddings"
|
|
@@ -613,12 +618,12 @@ class EmbeddingService:
|
|
|
613
618
|
"api-key": self._config.api_key,
|
|
614
619
|
}
|
|
615
620
|
body = {"input": texts, "model": self._config.deployment_name}
|
|
621
|
+
client = self._get_http_client()
|
|
616
622
|
last_error: Exception | None = None
|
|
617
623
|
for attempt in range(max_retries):
|
|
618
624
|
try:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
resp.raise_for_status()
|
|
625
|
+
resp = client.post(url, headers=headers, json=body)
|
|
626
|
+
resp.raise_for_status()
|
|
622
627
|
data = resp.json()
|
|
623
628
|
results = []
|
|
624
629
|
for item in sorted(data["data"], key=lambda d: d["index"]):
|