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
|
@@ -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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|