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.
Files changed (147) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +189 -740
  4. package/package.json +12 -5
  5. package/plugin/.claude-plugin/plugin.json +20 -0
  6. package/plugin/.mcp.json +12 -0
  7. package/plugin/CLAUDE.md +44 -0
  8. package/plugin/_GENERATED.md +6 -0
  9. package/plugin/agents/slm-memory-advisor.md +44 -0
  10. package/plugin/agents/slm-optimize-advisor.md +38 -0
  11. package/plugin/hooks/hooks.json +14 -0
  12. package/plugin/requirements.txt +1 -0
  13. package/plugin/scripts/ensure-venv.bat +122 -0
  14. package/plugin/scripts/ensure-venv.sh +105 -0
  15. package/plugin/scripts/slm-launch +15 -0
  16. package/plugin/scripts/slm-launch.bat +17 -0
  17. package/plugin/settings.json +16 -0
  18. package/plugin/skills/slm-cache/SKILL.md +140 -0
  19. package/plugin/skills/slm-compress/SKILL.md +143 -0
  20. package/plugin/skills/slm-graph/SKILL.md +300 -0
  21. package/plugin/skills/slm-recall/SKILL.md +204 -0
  22. package/plugin/skills/slm-remember/SKILL.md +194 -0
  23. package/plugin/skills/slm-session/SKILL.md +207 -0
  24. package/plugin/skills/slm-status/SKILL.md +149 -0
  25. package/plugin-src/.mcp.json +12 -0
  26. package/plugin-src/agents/slm-memory-advisor.md +44 -0
  27. package/plugin-src/agents/slm-optimize-advisor.md +38 -0
  28. package/plugin-src/commands/slm-optimize.md +22 -0
  29. package/plugin-src/commands/slm-recall.md +16 -0
  30. package/plugin-src/commands/slm-remember.md +16 -0
  31. package/plugin-src/commands/slm-status.md +15 -0
  32. package/plugin-src/hooks/.gitkeep +0 -0
  33. package/plugin-src/hooks/hooks.json +14 -0
  34. package/plugin-src/manifest.json +25 -0
  35. package/plugin-src/requirements.txt +1 -0
  36. package/plugin-src/rules/AGENTS.md +91 -0
  37. package/plugin-src/rules/CLAUDE.md.fragment +44 -0
  38. package/plugin-src/scripts/ensure-venv.bat +122 -0
  39. package/plugin-src/scripts/ensure-venv.sh +105 -0
  40. package/plugin-src/scripts/slm-launch +15 -0
  41. package/plugin-src/scripts/slm-launch.bat +17 -0
  42. package/plugin-src/settings.json +16 -0
  43. package/plugin-src/skills/slm-cache/SKILL.md +140 -0
  44. package/plugin-src/skills/slm-compress/SKILL.md +143 -0
  45. package/plugin-src/skills/slm-graph/SKILL.md +300 -0
  46. package/plugin-src/skills/slm-recall/SKILL.md +204 -0
  47. package/plugin-src/skills/slm-remember/SKILL.md +194 -0
  48. package/plugin-src/skills/slm-session/SKILL.md +207 -0
  49. package/plugin-src/skills/slm-status/SKILL.md +149 -0
  50. package/pyproject.toml +6 -2
  51. package/scripts/__tests__/build-plugin.test.mjs +613 -0
  52. package/scripts/_savings_math.py +270 -0
  53. package/scripts/build-plugin.js +742 -0
  54. package/scripts/dogfood_savings.py +490 -0
  55. package/scripts/install-skills.ps1 +4 -334
  56. package/scripts/install-skills.sh +4 -435
  57. package/scripts/postinstall-interactive.js +0 -27
  58. package/scripts/postinstall.js +21 -2
  59. package/src/superlocalmemory/__init__.py +1 -1
  60. package/src/superlocalmemory/cli/_lazy_init.py +115 -0
  61. package/src/superlocalmemory/cli/commands.py +439 -41
  62. package/src/superlocalmemory/cli/main.py +92 -4
  63. package/src/superlocalmemory/cli/setup_wizard.py +47 -6
  64. package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
  65. package/src/superlocalmemory/core/config.py +194 -9
  66. package/src/superlocalmemory/core/embeddings.py +10 -5
  67. package/src/superlocalmemory/core/engine.py +76 -5
  68. package/src/superlocalmemory/core/fact_consolidator.py +20 -3
  69. package/src/superlocalmemory/core/platform_utils.py +8 -0
  70. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  71. package/src/superlocalmemory/core/recall_worker.py +7 -0
  72. package/src/superlocalmemory/core/store_pipeline.py +23 -1
  73. package/src/superlocalmemory/core/worker_pool.py +14 -2
  74. package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
  75. package/src/superlocalmemory/hooks/portable_kit.py +506 -0
  76. package/src/superlocalmemory/hooks/session_registry.py +8 -4
  77. package/src/superlocalmemory/infra/cloud_backup.py +99 -23
  78. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
  79. package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
  80. package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
  81. package/src/superlocalmemory/mcp/server.py +75 -4
  82. package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
  83. package/src/superlocalmemory/mcp/tools_core.py +37 -4
  84. package/src/superlocalmemory/mcp/tools_v3.py +6 -1
  85. package/src/superlocalmemory/mcp/tools_v33.py +8 -4
  86. package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
  87. package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
  88. package/src/superlocalmemory/optimize/cache/manager.py +92 -6
  89. package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
  90. package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
  91. package/src/superlocalmemory/optimize/compress/router.py +46 -13
  92. package/src/superlocalmemory/optimize/config/schema.py +6 -0
  93. package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
  94. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
  95. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
  96. package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
  97. package/src/superlocalmemory/optimize/proxy/server.py +11 -0
  98. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
  99. package/src/superlocalmemory/optimize/storage/db.py +30 -0
  100. package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
  101. package/src/superlocalmemory/retrieval/engine.py +36 -3
  102. package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
  103. package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
  104. package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
  105. package/src/superlocalmemory/server/recall_serializer.py +3 -1
  106. package/src/superlocalmemory/server/unified_daemon.py +156 -16
  107. package/src/superlocalmemory/storage/database.py +215 -43
  108. package/src/superlocalmemory/storage/migration_runner.py +17 -1
  109. package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
  110. package/src/superlocalmemory/storage/models.py +10 -0
  111. package/src/superlocalmemory/storage/schema.py +15 -10
  112. package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
  113. package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
  114. package/src/superlocalmemory/ui/index.html +2 -2
  115. package/src/superlocalmemory/ui/js/core.js +98 -0
  116. package/src/superlocalmemory/ui/js/dashboard.js +8 -1
  117. package/src/superlocalmemory/ui/js/ide-status.js +16 -3
  118. package/src/superlocalmemory/ui/js/math-health.js +15 -3
  119. package/src/superlocalmemory/ui/js/optimize.js +18 -2
  120. package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
  121. package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
  122. package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
  123. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  124. package/ide/skills/slm-build-graph/SKILL.md +0 -423
  125. package/ide/skills/slm-list-recent/SKILL.md +0 -348
  126. package/ide/skills/slm-recall/SKILL.md +0 -326
  127. package/ide/skills/slm-remember/SKILL.md +0 -194
  128. package/ide/skills/slm-show-patterns/SKILL.md +0 -224
  129. package/ide/skills/slm-status/SKILL.md +0 -363
  130. package/ide/skills/slm-switch-profile/SKILL.md +0 -442
  131. package/skills/slm-build-graph/SKILL.md +0 -423
  132. package/skills/slm-list-recent/SKILL.md +0 -348
  133. package/skills/slm-optimize/README.md +0 -55
  134. package/skills/slm-optimize/SKILL.md +0 -139
  135. package/skills/slm-recall/SKILL.md +0 -343
  136. package/skills/slm-remember/SKILL.md +0 -194
  137. package/skills/slm-show-patterns/SKILL.md +0 -224
  138. package/skills/slm-status/SKILL.md +0 -363
  139. package/skills/slm-switch-profile/SKILL.md +0 -442
  140. package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
  141. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
  142. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
  143. package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
  144. package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
  145. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
  146. package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
  147. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  import logging
21
21
  from typing import Any
22
22
 
23
- from superlocalmemory.core.config import SLMConfig
23
+ from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT, SLMConfig
24
24
  from superlocalmemory.core.engine_capabilities import Capabilities, CapabilityError
25
25
  from superlocalmemory.core.modes import get_capabilities
26
26
  from superlocalmemory.storage.models import (
@@ -164,6 +164,25 @@ class MemoryEngine:
164
164
  except Exception as exc:
165
165
  logger.warning("V3.4.6 schema migration failed: %s", exc)
166
166
 
167
+ # v3.6.15: apply ALL pending migrations — including DEFERRED ones like
168
+ # M016 (scope/shared_with columns) — for DIRECT-engine usage: `slm
169
+ # remember --sync`, the Python API, and LangChain/CrewAI integrations.
170
+ # Previously only the daemon lifespan ran apply_deferred, so an existing
171
+ # pre-3.6.15 database used WITHOUT the daemon hit
172
+ # "table memories has no column named scope" on the first scoped write.
173
+ # Idempotent (skips applied migrations) + non-fatal; mirrors the daemon.
174
+ try:
175
+ from superlocalmemory.storage.migration_runner import (
176
+ apply_all, apply_deferred,
177
+ )
178
+ _base = self._config.base_dir
179
+ _learning_db = _base / "learning.db"
180
+ _memory_db = self._db.db_path
181
+ apply_all(_learning_db, _memory_db)
182
+ apply_deferred(_learning_db, _memory_db)
183
+ except Exception as exc:
184
+ logger.warning("v3.6.15 deferred migration apply failed: %s", exc)
185
+
167
186
  # V3.4.7: Apply "Learning Brain" schema (tool_events, behavioral_assertions)
168
187
  try:
169
188
  from superlocalmemory.storage.schema_v347 import apply_v347_schema
@@ -332,7 +351,25 @@ class MemoryEngine:
332
351
  logger.info("Processing %d pending memories from async store", len(pending))
333
352
  for item in pending:
334
353
  try:
335
- self.store(item["content"])
354
+ # v3.6.15 multi-scope: the pending row carries a metadata JSON
355
+ # blob that may hold scope/shared_with (written by the async
356
+ # /remember path). Replay them so a queued ``--scope global``
357
+ # write lands as global, not silently downgraded to personal.
358
+ import json as _json
359
+ meta = item.get("metadata")
360
+ if isinstance(meta, str):
361
+ try:
362
+ meta = _json.loads(meta) if meta else {}
363
+ except (ValueError, TypeError):
364
+ meta = {}
365
+ if not isinstance(meta, dict):
366
+ meta = {}
367
+ _scope = meta.get("scope") or "personal"
368
+ _shared = meta.get("shared_with")
369
+ self.store(
370
+ item["content"], metadata=meta or None,
371
+ scope=_scope, shared_with=_shared,
372
+ )
336
373
  mark_done(item["id"], base_dir)
337
374
  except Exception as exc:
338
375
  logger.warning("Pending memory %d failed: %s", item["id"], exc)
@@ -348,8 +385,15 @@ class MemoryEngine:
348
385
  speaker: str = "",
349
386
  role: str = "user",
350
387
  metadata: dict[str, Any] | None = None,
388
+ *,
389
+ scope: str = "personal",
390
+ shared_with: list[str] | None = None,
351
391
  ) -> list[str]:
352
- """Store content and extract structured facts. Returns fact_ids."""
392
+ """Store content and extract structured facts. Returns fact_ids.
393
+
394
+ Multi-scope: ``scope`` sets the visibility (personal/shared/global).
395
+ ``shared_with`` is a list of profile_ids for shared scope.
396
+ """
353
397
  self._require_full("store")
354
398
  self._ensure_init()
355
399
 
@@ -358,6 +402,7 @@ class MemoryEngine:
358
402
  content, self._profile_id,
359
403
  session_id=session_id, session_date=session_date,
360
404
  speaker=speaker, role=role, metadata=metadata,
405
+ scope=scope, shared_with=shared_with,
361
406
  config=self._config, db=self._db,
362
407
  embedder=self._embedder,
363
408
  fact_extractor=self._fact_extractor,
@@ -397,7 +442,10 @@ class MemoryEngine:
397
442
  vector_store=self._vector_store,
398
443
  )
399
444
 
400
- def store_fast(self, content: str, metadata: dict[str, Any] | None = None) -> list[str]:
445
+ def store_fast(
446
+ self, content: str, metadata: dict[str, Any] | None = None,
447
+ *, scope: str = "personal", shared_with: list[str] | None = None,
448
+ ) -> list[str]:
401
449
  """v3.5.5 WRITE-THROUGH: synchronous verbatim insert for IMMEDIATE recall.
402
450
 
403
451
  Full ``store()`` blocks 30-180s on LLM fact-extraction + Ollama embedding
@@ -455,6 +503,7 @@ class MemoryEngine:
455
503
  session_date=now[:10],
456
504
  session_id=(metadata or {}).get("session_id", ""),
457
505
  metadata=metadata or {},
506
+ scope=scope, shared_with=shared_with,
458
507
  )
459
508
  self._db.store_memory(record)
460
509
  # Lightweight regex entities (matches store_pipeline verbatim path) so
@@ -485,6 +534,7 @@ class MemoryEngine:
485
534
  observation_date=now[:10], confidence=0.7, importance=0.5,
486
535
  embedding=emb, fisher_mean=fmean, fisher_variance=fvar,
487
536
  created_at=now,
537
+ scope=scope, shared_with=shared_with,
488
538
  )
489
539
  self._db.store_fact(fact) # FTS5 trigger → immediately BM25-recallable
490
540
  # Upsert to vector store so the semantic channel finds it now.
@@ -507,10 +557,13 @@ class MemoryEngine:
507
557
 
508
558
  def recall(
509
559
  self, query: str, profile_id: str | None = None,
510
- mode: Mode | None = None, limit: int = 20,
560
+ mode: Mode | None = None, limit: int = CANONICAL_RECALL_LIMIT,
511
561
  agent_id: str = "unknown",
512
562
  session_id: str | None = None,
513
563
  fast: bool = False,
564
+ *,
565
+ include_global: bool | None = None,
566
+ include_shared: bool | None = None,
514
567
  ) -> RecallResponse:
515
568
  """Recall relevant facts for a query.
516
569
 
@@ -526,10 +579,26 @@ class MemoryEngine:
526
579
  neighbor-cache fix; fast=True is slower than fast=False and reduces
527
580
  recall quality. The parameter is accepted for backward compatibility
528
581
  but is silently treated as False.
582
+
583
+ Multi-scope: ``include_global`` / ``include_shared`` control which
584
+ scopes participate in retrieval. ``None`` (the default) means "use the
585
+ configured ScopeConfig default", which ships OFF — shared memory is
586
+ opt-in (v3.6.15). Personal facts are ALWAYS returned regardless, so a
587
+ config of False reproduces 3.6.14 pure-isolation behaviour exactly.
588
+ This is the single policy chokepoint: every recall path (CLI, MCP,
589
+ daemon HTTP, in-process adapter) flows through here, so a caller that
590
+ forgets to thread the flag still gets the safe configured default.
529
591
  """
530
592
  self._require_full("recall")
531
593
  self._ensure_init()
532
594
 
595
+ # Resolve None → configured ScopeConfig default (shared-off by default).
596
+ _scope_cfg = getattr(self._config, "scope", None)
597
+ if include_global is None:
598
+ include_global = bool(getattr(_scope_cfg, "recall_include_global", False))
599
+ if include_shared is None:
600
+ include_shared = bool(getattr(_scope_cfg, "recall_include_shared", False))
601
+
533
602
  if fast:
534
603
  logger.warning(
535
604
  "fast=True is deprecated (v3.6.9): SpreadingActivation now "
@@ -552,6 +621,8 @@ class MemoryEngine:
552
621
  access_log=self._access_log,
553
622
  auto_linker=self._auto_linker,
554
623
  fast=fast,
624
+ include_global=include_global,
625
+ include_shared=include_shared,
555
626
  )
556
627
 
557
628
  # S9-DASH-02: enqueue for pending_outcomes. Non-blocking; errors
@@ -190,7 +190,8 @@ def _consolidate_cluster(
190
190
  # Load fact contents including canonical_entities_json
191
191
  placeholders = ",".join("?" * len(fact_ids))
192
192
  facts = c.execute(
193
- f"SELECT fact_id, content, confidence, created_at, canonical_entities_json "
193
+ f"SELECT fact_id, content, confidence, created_at, canonical_entities_json, "
194
+ f"scope, shared_with "
194
195
  f"FROM atomic_facts "
195
196
  f"WHERE fact_id IN ({placeholders}) ORDER BY created_at",
196
197
  fact_ids,
@@ -215,6 +216,21 @@ def _consolidate_cluster(
215
216
  now = datetime.now(timezone.utc).isoformat()
216
217
  avg_confidence = sum(f["confidence"] or 0.5 for f in facts) / len(facts)
217
218
 
219
+ # v3.6.15 multi-scope: a summary must never be MORE visible than its
220
+ # sources, or it would leak a private fact into a shared/global summary.
221
+ # Preserve scope only when the whole cluster agrees; any mix (or shared
222
+ # facts with differing targets) falls back to 'personal' — the most
223
+ # restrictive scope. All-personal clusters (the common case) are
224
+ # unchanged. shared_with is preserved only for a uniform shared cluster.
225
+ _src_scopes = {(f["scope"] or "personal") for f in facts}
226
+ _src_shared = {f["shared_with"] for f in facts}
227
+ if _src_scopes == {"global"}:
228
+ _sum_scope, _sum_shared = "global", None
229
+ elif _src_scopes == {"shared"} and len(_src_shared) == 1:
230
+ _sum_scope, _sum_shared = "shared", facts[0]["shared_with"]
231
+ else:
232
+ _sum_scope, _sum_shared = "personal", None
233
+
218
234
  # Collect entities from ALL source facts (already in the SELECT)
219
235
  all_entities = set()
220
236
  raw_entities = set()
@@ -253,13 +269,14 @@ def _consolidate_cluster(
253
269
  (fact_id, memory_id, profile_id, content, fact_type,
254
270
  entities_json, canonical_entities_json,
255
271
  confidence, importance, evidence_count, access_count,
256
- created_at, lifecycle)
257
- VALUES (?, '', ?, ?, 'semantic', ?, ?, ?, 0.8, ?, 0, ?, 'active')
272
+ created_at, lifecycle, scope, shared_with)
273
+ VALUES (?, '', ?, ?, 'semantic', ?, ?, ?, 0.8, ?, 0, ?, 'active', ?, ?)
258
274
  """, (
259
275
  new_fact_id, profile_id, summary,
260
276
  json.dumps(list(all_entities)),
261
277
  json.dumps(list(all_entities)),
262
278
  round(avg_confidence, 3), len(facts), now,
279
+ _sum_scope, _sum_shared,
263
280
  ))
264
281
 
265
282
  # Record the consolidation
@@ -64,6 +64,10 @@ def is_pid_alive(pid: int) -> bool:
64
64
  try:
65
65
  os.kill(pid, 0)
66
66
  return True
67
+ except ProcessLookupError:
68
+ return False # ESRCH — no such process
69
+ except PermissionError:
70
+ return True # EPERM — process EXISTS, we just can't signal it
67
71
  except OSError:
68
72
  return False
69
73
  try:
@@ -73,6 +77,10 @@ def is_pid_alive(pid: int) -> bool:
73
77
  try:
74
78
  os.kill(pid, 0)
75
79
  return True
80
+ except ProcessLookupError:
81
+ return False # ESRCH — no such process
82
+ except PermissionError:
83
+ return True # EPERM — process EXISTS, we just can't signal it
76
84
  except OSError:
77
85
  return False
78
86
 
@@ -587,9 +587,14 @@ def run_recall(
587
587
  access_log: Any = None,
588
588
  auto_linker: Any = None,
589
589
  fast: bool = False,
590
+ include_global: bool = False,
591
+ include_shared: bool = False,
590
592
  ) -> RecallResponse:
591
593
  """Recall relevant facts for a query.
592
594
 
595
+ Multi-scope: ``include_global`` / ``include_shared`` control which
596
+ scopes participate in retrieval (passed through to retrieval engine).
597
+
593
598
  Pipeline: retrieval -> agentic sufficiency (if configured) -> post-recall updates.
594
599
 
595
600
  V3.4.40: ``fast=True`` adds spreading_activation to the per-recall
@@ -623,6 +628,8 @@ def run_recall(
623
628
  response = retrieval_engine.recall(
624
629
  query, profile_id, m, limit,
625
630
  extra_disabled_channels=extra_disabled,
631
+ include_global=include_global,
632
+ include_shared=include_shared,
626
633
  )
627
634
  _mark("retrieval(chan+rerank)")
628
635
 
@@ -61,10 +61,15 @@ def _get_engine():
61
61
 
62
62
  def _handle_recall(
63
63
  query: str, limit: int, session_id: str = "", fast: bool = False,
64
+ include_global: bool | None = None, include_shared: bool | None = None,
64
65
  ) -> dict:
65
66
  engine = _get_engine()
67
+ # v3.6.15 multi-scope: None flags let engine.recall resolve the configured
68
+ # default (shared-off). The subprocess loads its own SLMConfig, so the
69
+ # resolution is identical to the in-process / daemon paths.
66
70
  response = engine.recall(
67
71
  query, limit=limit, session_id=session_id or None, fast=bool(fast),
72
+ include_global=include_global, include_shared=include_shared,
68
73
  )
69
74
 
70
75
  # Batch-fetch original memory text for all results
@@ -285,6 +290,8 @@ def _worker_main() -> None:
285
290
  result = _handle_recall(
286
291
  req.get("query", ""), req.get("limit", 10),
287
292
  req.get("session_id", ""), bool(req.get("fast", False)),
293
+ include_global=req.get("include_global"),
294
+ include_shared=req.get("include_shared"),
288
295
  )
289
296
  _respond(result)
290
297
  elif cmd == "store":
@@ -99,6 +99,17 @@ def enrich_fact(
99
99
  langevin_position=langevin_pos,
100
100
  emotional_valence=emotion.valence, emotional_arousal=emotion.arousal,
101
101
  signal_type=signal, created_at=fact.created_at,
102
+ pinned=getattr(fact, 'pinned', False),
103
+ # v3.6.15 multi-scope: scope is a per-MEMORY property — every fact
104
+ # derived from a memory inherits the memory's scope. The record is
105
+ # authoritative; fact-extractor output never carries scope, so reading
106
+ # it off the fact (as before) silently downgraded extracted facts to
107
+ # 'personal' and broke `--scope global` on the common extraction path.
108
+ scope=(getattr(record, 'scope', None)
109
+ or getattr(fact, 'scope', None) or 'personal'),
110
+ shared_with=(getattr(record, 'shared_with', None)
111
+ if getattr(record, 'scope', None) in ('shared', 'global')
112
+ else getattr(fact, 'shared_with', None)),
102
113
  )
103
114
 
104
115
 
@@ -146,6 +157,8 @@ def run_store(
146
157
  role: str = "user",
147
158
  metadata: dict[str, Any] | None = None,
148
159
  *,
160
+ scope: str = "personal",
161
+ shared_with: list[str] | None = None,
149
162
  config: SLMConfig,
150
163
  db: DatabaseManager,
151
164
  embedder: Any,
@@ -169,7 +182,11 @@ def run_store(
169
182
  context_generator: Any = None,
170
183
  consolidation_engine: Any = None,
171
184
  ) -> list[str]:
172
- """Store content and extract structured facts. Returns fact_ids."""
185
+ """Store content and extract structured facts. Returns fact_ids.
186
+
187
+ Multi-scope: ``scope`` sets visibility (personal/shared/global).
188
+ ``shared_with`` is a list of profile_ids for shared scope.
189
+ """
173
190
  # Pre-operation hooks (trust gate, ABAC, rate limiter)
174
191
  hook_ctx = {
175
192
  "operation": "store",
@@ -203,6 +220,7 @@ def run_store(
203
220
  profile_id=profile_id, content=content,
204
221
  session_id=session_id, speaker=speaker, role=role,
205
222
  session_date=parsed_date, metadata=metadata or {},
223
+ scope=scope, shared_with=shared_with,
206
224
  )
207
225
  db.store_memory(record)
208
226
 
@@ -259,6 +277,8 @@ def run_store(
259
277
  observation_date=parsed_date,
260
278
  confidence=0.9,
261
279
  importance=0.5,
280
+ scope=scope,
281
+ shared_with=shared_with,
262
282
  )
263
283
  # Avoid duplicate if extraction already produced the exact same text
264
284
  extracted_texts = {f.content.strip().lower() for f in facts}
@@ -280,6 +300,8 @@ def run_store(
280
300
  observation_date=parsed_date,
281
301
  confidence=0.7,
282
302
  importance=0.3,
303
+ scope=scope,
304
+ shared_with=shared_with,
283
305
  )]
284
306
 
285
307
  if not facts:
@@ -68,6 +68,8 @@ class WorkerPool:
68
68
  def recall(
69
69
  self, query: str, limit: int = 10, session_id: str = "",
70
70
  fast: bool = False,
71
+ include_global: bool | None = None,
72
+ include_shared: bool | None = None,
71
73
  ) -> dict:
72
74
  """Run recall in worker subprocess. Returns result dict.
73
75
 
@@ -75,11 +77,21 @@ class WorkerPool:
75
77
  so the outcome-queue gets a pending_outcomes row for this
76
78
  recall. Without it, hook-based signals have no outcome to
77
79
  attach to.
80
+
81
+ v3.6.15 multi-scope: ``include_global``/``include_shared`` are forwarded
82
+ to the worker (and on to ``engine.recall``). ``None`` is sent verbatim
83
+ so the worker-side engine resolves the configured default — shared
84
+ memory is opt-in.
78
85
  """
79
- return self._send({
86
+ msg = {
80
87
  "cmd": "recall", "query": query, "limit": limit,
81
88
  "session_id": session_id or "", "fast": bool(fast),
82
- })
89
+ }
90
+ if include_global is not None:
91
+ msg["include_global"] = bool(include_global)
92
+ if include_shared is not None:
93
+ msg["include_shared"] = bool(include_shared)
94
+ return self._send(msg)
83
95
 
84
96
  def store(self, content: str, metadata: dict | None = None) -> dict:
85
97
  """Run store in worker subprocess. Returns result dict."""
@@ -266,7 +266,12 @@ def _remove_slm_hooks(settings: dict) -> dict:
266
266
 
267
267
 
268
268
  def _read_settings() -> dict:
269
- """Read Claude Code settings.json, return empty dict if missing."""
269
+ """Read Claude Code settings.json, return empty dict if missing.
270
+
271
+ Raises:
272
+ json.JSONDecodeError: propagated as-is when the file exists but is
273
+ malformed, so callers can distinguish 'missing' from 'corrupt'.
274
+ """
270
275
  if CLAUDE_SETTINGS.exists():
271
276
  return json.loads(CLAUDE_SETTINGS.read_text())
272
277
  return {}
@@ -356,6 +361,8 @@ def check_status() -> dict:
356
361
 
357
362
  hook_types_found: list[str] = []
358
363
  has_gate = False
364
+ parse_error: str | None = None
365
+
359
366
  try:
360
367
  settings = _read_settings()
361
368
  for hook_type, entries in settings.get("hooks", {}).items():
@@ -373,12 +380,26 @@ def check_status() -> dict:
373
380
  break
374
381
  if has_gate:
375
382
  break
383
+ except json.JSONDecodeError as exc:
384
+ # File exists but is malformed — report indeterminate state so callers
385
+ # do not conflate 'corrupt' with 'not installed'. Claude Code reads the
386
+ # file independently; hooks may still be firing even though we cannot
387
+ # parse the file here (split-brain scenario).
388
+ parse_error = f"JSON parse error in settings.json: {exc}"
389
+ logger.warning("SLM: %s", parse_error)
376
390
  except Exception:
377
391
  pass
378
392
 
379
- installed = len(hook_types_found) >= 3
393
+ # Three-valued installed:
394
+ # True — enough SLM hook types found (≥3)
395
+ # False — file missing or parseable but no SLM hooks
396
+ # None — file exists but is corrupt (indeterminate)
397
+ if parse_error is not None:
398
+ installed: bool | None = None
399
+ else:
400
+ installed = len(hook_types_found) >= 3
380
401
 
381
- return {
402
+ result: dict = {
382
403
  "installed": installed,
383
404
  "version": installed_version,
384
405
  "latest_version": HOOKS_VERSION,
@@ -386,6 +407,9 @@ def check_status() -> dict:
386
407
  "hook_types": hook_types_found,
387
408
  "gate_enabled": has_gate,
388
409
  }
410
+ if parse_error is not None:
411
+ result["error"] = parse_error
412
+ return result
389
413
 
390
414
 
391
415
  def upgrade_hooks() -> dict: