superlocalmemory 3.8.3 → 3.8.6

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 (125) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +3 -2
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +9 -4
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/access/rbac.py +68 -76
  34. package/src/superlocalmemory/cli/commands.py +158 -404
  35. package/src/superlocalmemory/cli/ingest_cmd.py +11 -1
  36. package/src/superlocalmemory/cli/main.py +30 -0
  37. package/src/superlocalmemory/cli/pending_store.py +39 -14
  38. package/src/superlocalmemory/core/backend_orchestrator.py +93 -0
  39. package/src/superlocalmemory/core/component_registry.py +4 -2
  40. package/src/superlocalmemory/core/config.py +78 -0
  41. package/src/superlocalmemory/core/consolidation_engine.py +79 -73
  42. package/src/superlocalmemory/core/embeddings.py +33 -6
  43. package/src/superlocalmemory/core/engine.py +186 -60
  44. package/src/superlocalmemory/core/engine_ingestion.py +150 -63
  45. package/src/superlocalmemory/core/fact_consolidator.py +148 -30
  46. package/src/superlocalmemory/core/graph_pruner.py +436 -39
  47. package/src/superlocalmemory/core/ingestion_command.py +273 -32
  48. package/src/superlocalmemory/core/maintenance_scheduler.py +61 -1
  49. package/src/superlocalmemory/core/mutations.py +32 -10
  50. package/src/superlocalmemory/core/recall_pipeline.py +111 -74
  51. package/src/superlocalmemory/core/registry.py +5 -1
  52. package/src/superlocalmemory/core/remember_admission.py +152 -0
  53. package/src/superlocalmemory/core/remember_runtime.py +712 -0
  54. package/src/superlocalmemory/core/remote_mode.py +3 -1
  55. package/src/superlocalmemory/core/scale_engine.py +41 -18
  56. package/src/superlocalmemory/core/store_pipeline.py +18 -4
  57. package/src/superlocalmemory/encoding/entity_resolver.py +18 -11
  58. package/src/superlocalmemory/graph/cozo_backend.py +5 -5
  59. package/src/superlocalmemory/hooks/_outcome_common.py +9 -2
  60. package/src/superlocalmemory/hooks/adapter_base.py +58 -44
  61. package/src/superlocalmemory/hooks/ide_connector.py +26 -8
  62. package/src/superlocalmemory/hooks/portable_kit.py +105 -9
  63. package/src/superlocalmemory/hooks/prewarm_auth.py +21 -2
  64. package/src/superlocalmemory/infra/auth_middleware.py +3 -1
  65. package/src/superlocalmemory/infra/cloud_backup.py +26 -27
  66. package/src/superlocalmemory/infra/event_bus.py +250 -88
  67. package/src/superlocalmemory/learning/bandit.py +50 -1
  68. package/src/superlocalmemory/learning/consolidation_cycle.py +33 -16
  69. package/src/superlocalmemory/learning/entity_compiler.py +148 -132
  70. package/src/superlocalmemory/learning/memory_merge.py +97 -82
  71. package/src/superlocalmemory/learning/reward_archive.py +98 -90
  72. package/src/superlocalmemory/learning/reward_boost.py +40 -30
  73. package/src/superlocalmemory/learning/source_quality.py +38 -35
  74. package/src/superlocalmemory/mcp/_daemon_proxy.py +38 -15
  75. package/src/superlocalmemory/mcp/http_transport.py +335 -3
  76. package/src/superlocalmemory/mcp/tools_active.py +4 -41
  77. package/src/superlocalmemory/mcp/tools_core.py +26 -87
  78. package/src/superlocalmemory/mcp/tools_evolution.py +5 -10
  79. package/src/superlocalmemory/optimize/proxy/capture.py +196 -8
  80. package/src/superlocalmemory/retrieval/engine.py +15 -4
  81. package/src/superlocalmemory/retrieval/entity_channel.py +25 -1
  82. package/src/superlocalmemory/retrieval/reranker.py +130 -22
  83. package/src/superlocalmemory/retrieval/spreading_activation.py +20 -12
  84. package/src/superlocalmemory/retrieval/vector_store.py +84 -69
  85. package/src/superlocalmemory/server/loopback.py +85 -0
  86. package/src/superlocalmemory/server/origin.py +9 -4
  87. package/src/superlocalmemory/server/profile_runtime.py +14 -0
  88. package/src/superlocalmemory/server/routes/abstraction.py +2 -4
  89. package/src/superlocalmemory/server/routes/agents.py +3 -5
  90. package/src/superlocalmemory/server/routes/backup.py +6 -2
  91. package/src/superlocalmemory/server/routes/behavioral.py +11 -25
  92. package/src/superlocalmemory/server/routes/brain.py +6 -9
  93. package/src/superlocalmemory/server/routes/compliance.py +20 -23
  94. package/src/superlocalmemory/server/routes/config_api.py +83 -0
  95. package/src/superlocalmemory/server/routes/entity.py +3 -7
  96. package/src/superlocalmemory/server/routes/evolution.py +3 -5
  97. package/src/superlocalmemory/server/routes/helpers.py +57 -25
  98. package/src/superlocalmemory/server/routes/insights.py +2 -4
  99. package/src/superlocalmemory/server/routes/learning.py +2 -5
  100. package/src/superlocalmemory/server/routes/lifecycle.py +2 -4
  101. package/src/superlocalmemory/server/routes/memories.py +119 -98
  102. package/src/superlocalmemory/server/routes/mesh.py +7 -2
  103. package/src/superlocalmemory/server/routes/profiles.py +20 -21
  104. package/src/superlocalmemory/server/routes/rbac.py +0 -1
  105. package/src/superlocalmemory/server/routes/tiers.py +28 -35
  106. package/src/superlocalmemory/server/routes/timeline.py +2 -4
  107. package/src/superlocalmemory/server/routes/v3_api.py +85 -93
  108. package/src/superlocalmemory/server/unified_daemon.py +400 -140
  109. package/src/superlocalmemory/server/write_identity.py +22 -4
  110. package/src/superlocalmemory/storage/admission_codec.py +119 -0
  111. package/src/superlocalmemory/storage/admission_journal.py +728 -0
  112. package/src/superlocalmemory/storage/database.py +168 -19
  113. package/src/superlocalmemory/storage/deferred_writes.py +209 -0
  114. package/src/superlocalmemory/storage/embedding_migrator.py +19 -0
  115. package/src/superlocalmemory/storage/memory_write.py +115 -0
  116. package/src/superlocalmemory/storage/migration_runner.py +44 -0
  117. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +113 -78
  118. package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +80 -0
  119. package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +188 -0
  120. package/src/superlocalmemory/storage/read_connection.py +115 -0
  121. package/src/superlocalmemory/storage/write_coordinator.py +756 -0
  122. package/src/superlocalmemory/storage/write_lock.py +88 -0
  123. package/src/superlocalmemory/ui/index.html +1 -1
  124. package/src/superlocalmemory/ui/js/auto-settings.js +14 -1
  125. package/src/superlocalmemory/ui/js/od-settings.js +9 -3
@@ -21,6 +21,25 @@ from pathlib import Path
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
23
 
24
+ def _daemon_unavailable(command: str, use_json: bool) -> None:
25
+ """Exit a mutation client without opening a process-local writer."""
26
+ error = {
27
+ "code": "DAEMON_UNAVAILABLE",
28
+ "message": "Owned daemon is unavailable; retry later.",
29
+ "retryable": True,
30
+ }
31
+ if use_json:
32
+ from superlocalmemory.cli.json_output import json_print
33
+
34
+ json_print(command, error=error)
35
+ else:
36
+ print(
37
+ "DAEMON_UNAVAILABLE: owned daemon is unavailable; retry later.",
38
+ file=sys.stderr,
39
+ )
40
+ raise SystemExit(1)
41
+
42
+
24
43
  def _cmd_db_dispatch(args: Namespace) -> None:
25
44
  """Route ``slm db ...`` subcommands. LLD-06 §7.2."""
26
45
  sub = getattr(args, "db_command", None)
@@ -654,14 +673,14 @@ def cmd_restart(args: Namespace) -> None:
654
673
 
655
674
  # Step 5: Database integrity check
656
675
  try:
657
- import sqlite3
676
+ from superlocalmemory.storage.memory_write import memory_read
677
+
658
678
  db_path = slm_dir / "memory.db"
659
679
  if db_path.exists():
660
- conn = sqlite3.connect(str(db_path))
661
- integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
662
- fact_count = conn.execute("SELECT COUNT(*) FROM atomic_facts").fetchone()[0]
663
- entity_count = conn.execute("SELECT COUNT(*) FROM canonical_entities").fetchone()[0]
664
- conn.close()
680
+ with memory_read(db_path) as conn:
681
+ integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
682
+ fact_count = conn.execute("SELECT COUNT(*) FROM atomic_facts").fetchone()[0]
683
+ entity_count = conn.execute("SELECT COUNT(*) FROM canonical_entities").fetchone()[0]
665
684
  _log(5, "Database integrity", "ok" if integrity == "ok" else "fail",
666
685
  f"integrity={integrity}, {fact_count} facts, {entity_count} entities")
667
686
  else:
@@ -1135,6 +1154,9 @@ def cmd_connect(args: Namespace) -> None:
1135
1154
  if ide_arg in IDE_MATRIX:
1136
1155
  here = getattr(args, "here", False)
1137
1156
  profile = getattr(args, "profile", None)
1157
+ transport = getattr(args, "transport", "stdio")
1158
+ daemon_port = getattr(args, "daemon_port", 8765)
1159
+ verify = getattr(args, "verify", False)
1138
1160
  project = None
1139
1161
  if here:
1140
1162
  import pathlib
@@ -1148,8 +1170,24 @@ def cmd_connect(args: Namespace) -> None:
1148
1170
  profile=profile,
1149
1171
  agents_md_source=_agents_md_source_factory(),
1150
1172
  dry_run=getattr(args, "dry_run", False),
1173
+ transport=transport,
1174
+ daemon_port=daemon_port,
1151
1175
  )
1152
1176
 
1177
+ # --verify: probe daemon health after writing
1178
+ if verify and transport in ("http", "http-mcp-remote") and not result.get("error"):
1179
+ from superlocalmemory.hooks.portable_kit import _check_daemon_health
1180
+ reachable = _check_daemon_health(daemon_port)
1181
+ if reachable:
1182
+ print(f"[verify] Daemon reachable at http://127.0.0.1:{daemon_port}/api/v3/health")
1183
+ else:
1184
+ print(
1185
+ f"[verify] Warning: daemon NOT reachable at "
1186
+ f"http://127.0.0.1:{daemon_port}/api/v3/health. "
1187
+ f"Run `slm serve start` to start it.",
1188
+ file=sys.stderr,
1189
+ )
1190
+
1153
1191
  if not result.get("error") and not getattr(args, "dry_run", False):
1154
1192
  from superlocalmemory.infra.local_diagnostics import record_operation
1155
1193
 
@@ -1320,8 +1358,7 @@ def cmd_list(args: Namespace) -> None:
1320
1358
 
1321
1359
 
1322
1360
  def cmd_remember(args: Namespace) -> None:
1323
- """Store a memory via the engine."""
1324
- from superlocalmemory.core.config import SLMConfig
1361
+ """Store a memory through the owned daemon."""
1325
1362
 
1326
1363
  use_json = getattr(args, 'json', False)
1327
1364
  sync_mode = getattr(args, 'sync_mode', False)
@@ -1339,128 +1376,43 @@ def cmd_remember(args: Namespace) -> None:
1339
1376
  if isinstance(_sw_raw, str) and _sw_raw.strip() else _sw_raw
1340
1377
  )
1341
1378
 
1342
- # Both paths use the one owned daemon. A second local engine for --sync
1343
- # duplicates heavyweight workers and can block for minutes on cold models.
1344
- daemon_owned = False
1345
1379
  try:
1346
1380
  from superlocalmemory.cli.daemon import (
1347
1381
  daemon_request, ensure_daemon, is_daemon_running,
1348
1382
  )
1349
- daemon_owned = is_daemon_running() or ensure_daemon()
1350
- if daemon_owned:
1351
- path = "/remember?wait=true" if sync_mode else "/remember"
1352
- result = daemon_request(
1353
- "POST", path, {
1354
- "content": args.content,
1355
- "tags": args.tags or "",
1356
- "scope": scope,
1357
- "shared_with": shared_with,
1358
- },
1359
- timeout_seconds=30,
1360
- )
1361
- if result and "fact_ids" in result:
1362
- if use_json:
1363
- from superlocalmemory.cli.json_output import json_print
1364
- json_print("remember", data=result)
1365
- else:
1366
- state = result.get("materialization_state", "queryable")
1367
- operation_id = result.get("operation_id", "unknown")
1368
- print(
1369
- f"{state.capitalize()} \u2713 {result['count']} facts "
1370
- f"(operation={operation_id})."
1371
- )
1372
- return
1373
- if sync_mode:
1374
- if use_json:
1375
- from superlocalmemory.cli.json_output import json_print
1376
- json_print("remember", error={
1377
- "code": "SYNC_TIMEOUT",
1378
- "message": (
1379
- "Canonical ingestion did not complete within 30s; "
1380
- "the durable operation remains available for retry."
1381
- ),
1382
- })
1383
- else:
1384
- print(
1385
- "Synchronous ingestion did not complete within 30s; "
1386
- "the durable operation remains queued.",
1387
- file=sys.stderr,
1388
- )
1389
- sys.exit(1)
1390
- except SystemExit:
1391
- raise
1392
- except Exception:
1393
- if sync_mode and daemon_owned:
1383
+ if not (is_daemon_running() or ensure_daemon()):
1384
+ _daemon_unavailable("remember", use_json)
1385
+ path = "/remember?wait=true" if sync_mode else "/remember"
1386
+ result = daemon_request(
1387
+ "POST", path, {
1388
+ "content": args.content,
1389
+ "tags": args.tags or "",
1390
+ "scope": scope,
1391
+ "shared_with": shared_with,
1392
+ },
1393
+ timeout_seconds=30,
1394
+ )
1395
+ if result and "fact_ids" in result:
1394
1396
  if use_json:
1395
1397
  from superlocalmemory.cli.json_output import json_print
1396
- json_print("remember", error={
1397
- "code": "SYNC_TIMEOUT",
1398
- "message": "Owned daemon request failed before completion.",
1399
- })
1400
- sys.exit(1)
1401
- # Receipt-first writes may use the authenticated local fallback when
1402
- # no owned daemon exists.
1403
-
1404
- from superlocalmemory.core.engine import MemoryEngine
1405
-
1406
- try:
1407
- config = SLMConfig.load()
1408
- engine = MemoryEngine(config)
1409
- engine.initialize()
1410
-
1411
- # v3.6.15: resolve an unset scope to the configured default_scope.
1412
- _scope = scope or getattr(getattr(config, "scope", None), "default_scope", "personal")
1413
- from superlocalmemory.core.engine_ingestion import (
1414
- canonical_store,
1415
- local_trusted_actor_id,
1416
- )
1417
-
1418
- metadata = {"tags": args.tags} if args.tags else {}
1419
- operation = canonical_store(
1420
- engine,
1421
- args.content,
1422
- source_type="cli-sync" if sync_mode else "cli-offline-canonical",
1423
- trusted_actor_id=local_trusted_actor_id("cli"),
1424
- metadata=metadata,
1425
- scope=_scope,
1426
- shared_with=shared_with,
1427
- return_receipt=True,
1428
- )
1429
- except Exception as exc:
1430
- if use_json:
1431
- from superlocalmemory.cli.json_output import json_print
1432
- json_print("remember", error={"code": "STORE_ERROR", "message": str(exc)})
1433
- sys.exit(1)
1398
+ json_print("remember", data=result)
1399
+ else:
1400
+ state = result.get("materialization_state", "queryable")
1401
+ operation_id = result.get("operation_id", "unknown")
1402
+ print(
1403
+ f"{state.capitalize()} \u2713 {result['count']} facts "
1404
+ f"(operation={operation_id})."
1405
+ )
1406
+ return
1407
+ except SystemExit:
1434
1408
  raise
1435
-
1436
- fact_ids = list(operation.fact_ids) if hasattr(operation, "fact_ids") else list(operation)
1437
- operation_data = {
1438
- "fact_ids": fact_ids,
1439
- "count": len(fact_ids),
1440
- "materialization_state": getattr(
1441
- getattr(operation, "state", None), "value", "complete"
1442
- ),
1443
- }
1444
- if getattr(operation, "operation_id", None):
1445
- operation_data["operation_id"] = operation.operation_id
1446
-
1447
- if use_json:
1448
- from superlocalmemory.cli.json_output import json_print
1449
- json_print("remember", data=operation_data,
1450
- next_actions=[
1451
- {"command": "slm recall '<query>' --json", "description": "Search your memories"},
1452
- {"command": "slm list --json -n 5", "description": "See recent memories"},
1453
- ])
1454
- return
1455
-
1456
- print(
1457
- f"Complete \u2713 {len(fact_ids)} facts "
1458
- f"(operation={operation_data.get('operation_id', 'none')})."
1459
- )
1409
+ except Exception as exc:
1410
+ logger.warning("owned daemon remember request failed: %s", exc)
1411
+ _daemon_unavailable("remember", use_json)
1460
1412
 
1461
1413
 
1462
1414
  def cmd_recall(args: Namespace) -> None:
1463
- """Search memories via the engine routes through daemon if available."""
1415
+ """Search memories through the owned daemon without a local engine."""
1464
1416
  use_json = getattr(args, 'json', False)
1465
1417
  # v3.6.15: None = "not specified" → daemon/engine resolves the configured
1466
1418
  # default (shared-off). Only an explicit --include-global / --no-global
@@ -1469,7 +1421,6 @@ def cmd_recall(args: Namespace) -> None:
1469
1421
  include_shared = getattr(args, 'include_shared', None)
1470
1422
 
1471
1423
  # V3.3.21: Route through daemon for instant response (no cold start).
1472
- # Falls back to direct engine if daemon not running.
1473
1424
  # S9-DASH-02: pass a stable session_id derived from the shell's
1474
1425
  # parent PID so a sequence of CLI recalls in one terminal can be
1475
1426
  # grouped. The Stop hook on session end won't fire for CLI, so
@@ -1515,76 +1466,10 @@ def cmd_recall(args: Namespace) -> None:
1515
1466
  return
1516
1467
  except Exception as _exc: # noqa: BLE001
1517
1468
  logger.warning(
1518
- "Daemon recall failed, falling back to direct engine: %s", _exc
1469
+ "owned daemon recall request failed: %s", _exc
1519
1470
  )
1520
1471
 
1521
- from superlocalmemory.core.config import SLMConfig
1522
- from superlocalmemory.core.engine import MemoryEngine
1523
-
1524
- try:
1525
- config = SLMConfig.load()
1526
- engine = MemoryEngine(config)
1527
- engine.initialize()
1528
-
1529
- response = engine.recall(
1530
- args.query, limit=args.limit,
1531
- # v3.8.2: --fast → True; unset → None so engine resolves the
1532
- # client-driven-agentic default (parity with the daemon path, which
1533
- # omits the fast query param when --fast is absent).
1534
- fast=(True if getattr(args, "fast", False) else None),
1535
- include_global=include_global,
1536
- include_shared=include_shared,
1537
- window=getattr(args, "window", "") or None,
1538
- )
1539
- except Exception as exc:
1540
- if use_json:
1541
- from superlocalmemory.cli.json_output import json_print
1542
- json_print("recall", error={"code": "RECALL_ERROR", "message": str(exc)})
1543
- sys.exit(1)
1544
- raise
1545
-
1546
- # v3.6.6: route the direct-fallback path through the SAME shared
1547
- # serializer the daemon uses, so CLI-without-daemon output is identical
1548
- # to CLI/MCP-with-daemon (budget + source discipline + no_confident_match).
1549
- from superlocalmemory.server.recall_serializer import (
1550
- recall_response_metadata,
1551
- serialize_recall_response,
1552
- )
1553
- _rc = getattr(config, "retrieval", None)
1554
- _ser, _no_match = serialize_recall_response(
1555
- response,
1556
- limit=args.limit,
1557
- per_fact_max=getattr(_rc, "recall_per_fact_max_chars", 2400),
1558
- total_max=getattr(_rc, "recall_total_max_chars", 12000),
1559
- full=getattr(args, "full", False),
1560
- )
1561
-
1562
- if use_json:
1563
- from superlocalmemory.cli.json_output import json_print
1564
- items = []
1565
- for d in _ser:
1566
- items.append(dict(d))
1567
- json_print("recall", data={
1568
- "results": items, "count": len(items),
1569
- "query_type": getattr(response, "query_type", "unknown"),
1570
- "no_confident_match": _no_match,
1571
- **recall_response_metadata(response),
1572
- }, next_actions=[
1573
- {"command": "slm list --json", "description": "List recent memories"},
1574
- ])
1575
- return
1576
-
1577
- # Record learning signals (CLI path — works without MCP)
1578
- try:
1579
- _cli_record_signals(config, args.query, response.results)
1580
- except Exception:
1581
- pass
1582
-
1583
- if not _ser:
1584
- print("No confident match." if _no_match else "No memories found.")
1585
- return
1586
- for i, d in enumerate(_ser, 1):
1587
- print(f" {i}. [relevance {d['relevance_score']:.2f}] {d['content']}")
1472
+ _daemon_unavailable("recall", use_json)
1588
1473
 
1589
1474
 
1590
1475
  def _cli_record_signals(config, query, results):
@@ -1614,9 +1499,14 @@ def _cli_record_signals(config, query, results):
1614
1499
 
1615
1500
 
1616
1501
  def cmd_forget(args: Namespace) -> None:
1617
- """Delete memories matching a query."""
1618
- from superlocalmemory.core.engine import MemoryEngine
1619
- from superlocalmemory.core.config import SLMConfig
1502
+ """Delete daemon-queried memories matching a query."""
1503
+ import urllib.parse
1504
+
1505
+ from superlocalmemory.cli.daemon import (
1506
+ daemon_request,
1507
+ ensure_daemon,
1508
+ is_daemon_running,
1509
+ )
1620
1510
 
1621
1511
  use_json = getattr(args, 'json', False)
1622
1512
  dry_run = getattr(args, 'dry_run', False)
@@ -1640,38 +1530,45 @@ def cmd_forget(args: Namespace) -> None:
1640
1530
  query_lower = "" if raw_query is None else raw_query.lower()
1641
1531
  query_label = raw_query if raw_query is not None else "*all*"
1642
1532
 
1643
- try:
1644
- config = SLMConfig.load()
1645
- engine = MemoryEngine(config)
1646
- engine.initialize()
1647
- facts = engine._db.get_all_facts(engine.profile_id)
1648
- matches = [f for f in facts if query_lower in f.content.lower()]
1649
- except Exception as exc:
1650
- if use_json:
1651
- from superlocalmemory.cli.json_output import json_print
1652
- json_print("forget", error={"code": "ENGINE_ERROR", "message": str(exc)})
1653
- sys.exit(1)
1654
- raise
1533
+ if not (is_daemon_running() or ensure_daemon()):
1534
+ _daemon_unavailable("forget", use_json)
1535
+
1536
+ memories: list[dict[str, object]] = []
1537
+ offset = 0
1538
+ while True:
1539
+ page = daemon_request("GET", f"/api/memories?limit=200&offset={offset}")
1540
+ rows = page.get("memories") if isinstance(page, dict) else None
1541
+ if not isinstance(rows, list):
1542
+ _daemon_unavailable("forget", use_json)
1543
+ memories.extend(row for row in rows if isinstance(row, dict))
1544
+ if not page.get("has_more"):
1545
+ break
1546
+ offset += len(rows)
1547
+ if not rows:
1548
+ _daemon_unavailable("forget", use_json)
1655
1549
 
1656
- def delete_fact_authorized_for_cli(fact_id: str) -> None:
1657
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1658
- from superlocalmemory.core.mutations import delete_fact_authorized
1550
+ matches = [
1551
+ fact for fact in memories
1552
+ if query_lower in str(fact.get("content", "")).lower()
1553
+ ]
1659
1554
 
1660
- result = delete_fact_authorized(
1661
- engine,
1662
- fact_id,
1663
- trusted_actor_id=local_trusted_actor_id("cli"),
1664
- source_agent_id="cli",
1555
+ def delete_from_daemon(fact_id: str) -> None:
1556
+ result = daemon_request(
1557
+ "DELETE",
1558
+ "/api/memories/" + urllib.parse.quote(fact_id, safe=""),
1665
1559
  )
1666
- if not result.get("ok"):
1667
- raise RuntimeError(result.get("error", "delete failed"))
1560
+ if not isinstance(result, dict) or not result.get("success"):
1561
+ _daemon_unavailable("forget", use_json)
1668
1562
 
1669
1563
  if use_json:
1670
1564
  from superlocalmemory.cli.json_output import json_print
1671
1565
  if not matches:
1672
1566
  json_print("forget", data={"matched_count": 0, "deleted_count": 0, "matches": []})
1673
1567
  return
1674
- match_items = [{"fact_id": f.fact_id, "content": f.content[:120]} for f in matches[:20]]
1568
+ match_items = [
1569
+ {"fact_id": f["id"], "content": str(f.get("content", ""))[:120]}
1570
+ for f in matches[:20]
1571
+ ]
1675
1572
  if dry_run:
1676
1573
  json_print("forget", data={
1677
1574
  "matched_count": len(matches), "deleted_count": 0,
@@ -1680,10 +1577,10 @@ def cmd_forget(args: Namespace) -> None:
1680
1577
  return
1681
1578
  if getattr(args, 'yes', False):
1682
1579
  for f in matches:
1683
- delete_fact_authorized_for_cli(f.fact_id)
1580
+ delete_from_daemon(str(f["id"]))
1684
1581
  json_print("forget", data={
1685
1582
  "matched_count": len(matches), "deleted_count": len(matches),
1686
- "deleted": [f.fact_id for f in matches],
1583
+ "deleted": [f["id"] for f in matches],
1687
1584
  }, next_actions=[
1688
1585
  {"command": "slm list --json", "description": "Verify remaining memories"},
1689
1586
  ])
@@ -1693,7 +1590,10 @@ def cmd_forget(args: Namespace) -> None:
1693
1590
  "matches": match_items,
1694
1591
  "hint": "Add --yes to confirm deletion",
1695
1592
  }, next_actions=[
1696
- {"command": f"slm forget '{query_label}' --json --yes", "description": "Confirm deletion"},
1593
+ {
1594
+ "command": f"slm forget '{query_label}' --json --yes",
1595
+ "description": "Confirm deletion",
1596
+ },
1697
1597
  ])
1698
1598
  return
1699
1599
 
@@ -1702,19 +1602,19 @@ def cmd_forget(args: Namespace) -> None:
1702
1602
  return
1703
1603
  print(f"Found {len(matches)} matching memories:")
1704
1604
  for f in matches[:10]:
1705
- print(f" - {f.fact_id[:8]}... {f.content[:80]}")
1605
+ print(f" - {str(f['id'])[:8]}... {str(f.get('content', ''))[:80]}")
1706
1606
  if dry_run:
1707
1607
  print(f"(dry run — {len(matches)} would be deleted)")
1708
1608
  return
1709
1609
  if getattr(args, 'yes', False):
1710
1610
  for f in matches:
1711
- delete_fact_authorized_for_cli(f.fact_id)
1611
+ delete_from_daemon(str(f["id"]))
1712
1612
  print(f"Deleted {len(matches)} memories.")
1713
1613
  return
1714
1614
  confirm = input(f"Delete {len(matches)} memories? [y/N] ").strip().lower()
1715
1615
  if confirm in ("y", "yes"):
1716
1616
  for f in matches:
1717
- delete_fact_authorized_for_cli(f.fact_id)
1617
+ delete_from_daemon(str(f["id"]))
1718
1618
  print(f"Deleted {len(matches)} memories.")
1719
1619
  else:
1720
1620
  print("Cancelled.")
@@ -1724,13 +1624,15 @@ def cmd_delete(args: Namespace) -> None:
1724
1624
  """Delete a specific memory by exact fact ID."""
1725
1625
  import urllib.parse
1726
1626
 
1727
- from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
1728
- from superlocalmemory.core.config import SLMConfig
1729
- from superlocalmemory.core.engine import MemoryEngine
1627
+ from superlocalmemory.cli.daemon import (
1628
+ daemon_request,
1629
+ ensure_daemon,
1630
+ is_daemon_running,
1631
+ )
1730
1632
 
1731
1633
  use_json = getattr(args, 'json', False)
1732
1634
  fact_id = args.fact_id.strip()
1733
- if is_daemon_running():
1635
+ if is_daemon_running() or ensure_daemon():
1734
1636
  path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
1735
1637
  confirmed = getattr(args, "yes", False)
1736
1638
  content = ""
@@ -1740,14 +1642,7 @@ def cmd_delete(args: Namespace) -> None:
1740
1642
  "/api/facts/" + urllib.parse.quote(fact_id, safe=""),
1741
1643
  )
1742
1644
  if not isinstance(detail, dict):
1743
- if use_json:
1744
- from superlocalmemory.cli.json_output import json_print
1745
- json_print("delete", error={
1746
- "code": "DAEMON_MUTATION_FAILED",
1747
- "message": "Resident daemon could not resolve the memory.",
1748
- })
1749
- sys.exit(1)
1750
- raise RuntimeError("Resident daemon could not resolve the memory.")
1645
+ _daemon_unavailable("delete", use_json)
1751
1646
  content = str(detail.get("content") or "")
1752
1647
  if use_json:
1753
1648
  from superlocalmemory.cli.json_output import json_print
@@ -1773,14 +1668,7 @@ def cmd_delete(args: Namespace) -> None:
1773
1668
 
1774
1669
  result = daemon_request("DELETE", path)
1775
1670
  if not isinstance(result, dict) or not result.get("success"):
1776
- if use_json:
1777
- from superlocalmemory.cli.json_output import json_print
1778
- json_print("delete", error={
1779
- "code": "DAEMON_MUTATION_FAILED",
1780
- "message": "Resident daemon rejected the delete operation.",
1781
- })
1782
- sys.exit(1)
1783
- raise RuntimeError("Resident daemon rejected the delete operation.")
1671
+ _daemon_unavailable("delete", use_json)
1784
1672
  if use_json:
1785
1673
  from superlocalmemory.cli.json_output import json_print
1786
1674
  json_print(
@@ -1796,86 +1684,18 @@ def cmd_delete(args: Namespace) -> None:
1796
1684
  else:
1797
1685
  print(f"Deleted: {fact_id}")
1798
1686
  return
1799
-
1800
- try:
1801
- config = SLMConfig.load()
1802
- engine = MemoryEngine(config)
1803
- engine.initialize()
1804
-
1805
- rows = engine._db.execute(
1806
- "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
1807
- (fact_id, engine.profile_id),
1808
- )
1809
- except Exception as exc:
1810
- if use_json:
1811
- from superlocalmemory.cli.json_output import json_print
1812
- json_print("delete", error={"code": "ENGINE_ERROR", "message": str(exc)})
1813
- sys.exit(1)
1814
- raise
1815
-
1816
- if use_json:
1817
- from superlocalmemory.cli.json_output import json_print
1818
- if not rows:
1819
- json_print("delete", error={
1820
- "code": "NOT_FOUND", "message": f"Memory not found: {fact_id}",
1821
- })
1822
- sys.exit(1)
1823
- content = dict(rows[0]).get("content", "")
1824
- if getattr(args, "yes", False):
1825
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1826
- from superlocalmemory.core.mutations import delete_fact_authorized
1827
-
1828
- delete_fact_authorized(
1829
- engine,
1830
- fact_id,
1831
- trusted_actor_id=local_trusted_actor_id("cli"),
1832
- source_agent_id="cli",
1833
- )
1834
- json_print("delete", data={"deleted": fact_id, "content": content[:120]},
1835
- next_actions=[
1836
- {"command": "slm list --json", "description": "Verify remaining memories"},
1837
- ])
1838
- else:
1839
- json_print("delete", data={
1840
- "fact_id": fact_id, "content": content[:120], "deleted": False,
1841
- "hint": "Add --yes to confirm deletion",
1842
- }, next_actions=[
1843
- {"command": f"slm delete {fact_id} --json --yes", "description": "Confirm deletion"},
1844
- ])
1845
- return
1846
-
1847
- if not rows:
1848
- print(f"Memory not found: {fact_id}")
1849
- return
1850
-
1851
- content_preview = dict(rows[0]).get("content", "")[:120]
1852
- print(f"Memory: {content_preview}")
1853
-
1854
- if not getattr(args, "yes", False):
1855
- confirm = input("Delete this memory? [y/N] ").strip().lower()
1856
- if confirm not in ("y", "yes"):
1857
- print("Cancelled.")
1858
- return
1859
-
1860
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1861
- from superlocalmemory.core.mutations import delete_fact_authorized
1862
-
1863
- delete_fact_authorized(
1864
- engine,
1865
- fact_id,
1866
- trusted_actor_id=local_trusted_actor_id("cli"),
1867
- source_agent_id="cli",
1868
- )
1869
- print(f"Deleted: {fact_id}")
1687
+ _daemon_unavailable("delete", use_json)
1870
1688
 
1871
1689
 
1872
1690
  def cmd_update(args: Namespace) -> None:
1873
1691
  """Update the content of a specific memory by exact fact ID."""
1874
1692
  import urllib.parse
1875
1693
 
1876
- from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
1877
- from superlocalmemory.core.config import SLMConfig
1878
- from superlocalmemory.core.engine import MemoryEngine
1694
+ from superlocalmemory.cli.daemon import (
1695
+ daemon_request,
1696
+ ensure_daemon,
1697
+ is_daemon_running,
1698
+ )
1879
1699
 
1880
1700
  use_json = getattr(args, 'json', False)
1881
1701
  fact_id = args.fact_id.strip()
@@ -1889,18 +1709,11 @@ def cmd_update(args: Namespace) -> None:
1889
1709
  print("Error: content cannot be empty")
1890
1710
  return
1891
1711
 
1892
- if is_daemon_running():
1712
+ if is_daemon_running() or ensure_daemon():
1893
1713
  path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
1894
1714
  result = daemon_request("PATCH", path, {"content": new_content})
1895
1715
  if not isinstance(result, dict) or not result.get("success"):
1896
- if use_json:
1897
- from superlocalmemory.cli.json_output import json_print
1898
- json_print("update", error={
1899
- "code": "DAEMON_MUTATION_FAILED",
1900
- "message": "Resident daemon rejected the update operation.",
1901
- })
1902
- sys.exit(1)
1903
- raise RuntimeError("Resident daemon rejected the update operation.")
1716
+ _daemon_unavailable("update", use_json)
1904
1717
  if use_json:
1905
1718
  from superlocalmemory.cli.json_output import json_print
1906
1719
  json_print("update", data={
@@ -1916,59 +1729,7 @@ def cmd_update(args: Namespace) -> None:
1916
1729
  print(f"New: {new_content[:100]}")
1917
1730
  print(f"Updated: {fact_id}")
1918
1731
  return
1919
-
1920
- try:
1921
- config = SLMConfig.load()
1922
- engine = MemoryEngine(config)
1923
- engine.initialize()
1924
-
1925
- rows = engine._db.execute(
1926
- "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
1927
- (fact_id, engine.profile_id),
1928
- )
1929
- except Exception as exc:
1930
- if use_json:
1931
- from superlocalmemory.cli.json_output import json_print
1932
- json_print("update", error={"code": "ENGINE_ERROR", "message": str(exc)})
1933
- sys.exit(1)
1934
- raise
1935
-
1936
- if not rows:
1937
- if use_json:
1938
- from superlocalmemory.cli.json_output import json_print
1939
- json_print("update", error={
1940
- "code": "NOT_FOUND", "message": f"Memory not found: {fact_id}",
1941
- })
1942
- sys.exit(1)
1943
- print(f"Memory not found: {fact_id}")
1944
- return
1945
-
1946
- old_content = dict(rows[0]).get("content", "")
1947
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1948
- from superlocalmemory.core.mutations import update_fact_authorized
1949
-
1950
- update_fact_authorized(
1951
- engine,
1952
- fact_id,
1953
- new_content,
1954
- trusted_actor_id=local_trusted_actor_id("cli"),
1955
- source_agent_id="cli",
1956
- )
1957
-
1958
- if use_json:
1959
- from superlocalmemory.cli.json_output import json_print
1960
- json_print("update", data={
1961
- "fact_id": fact_id,
1962
- "old_content": old_content[:120],
1963
- "new_content": new_content[:120],
1964
- }, next_actions=[
1965
- {"command": "slm list --json", "description": "List recent memories"},
1966
- ])
1967
- return
1968
-
1969
- print(f"Old: {old_content[:100]}")
1970
- print(f"New: {new_content[:100]}")
1971
- print(f"Updated: {fact_id}")
1732
+ _daemon_unavailable("update", use_json)
1972
1733
 
1973
1734
 
1974
1735
  # -- Diagnostics (all support --json) -------------------------------------
@@ -2131,12 +1892,10 @@ def cmd_health(args: Namespace) -> None:
2131
1892
  # second MemoryEngine re-runs schema initialization and can lock
2132
1893
  # the user's database. Health only needs aggregate counts, so use
2133
1894
  # a read-only snapshot connection instead.
2134
- import sqlite3
1895
+ from superlocalmemory.storage.memory_write import memory_read
1896
+
2135
1897
  db_path = config.db_path
2136
- conn = sqlite3.connect(
2137
- f"file:{db_path}?mode=ro", uri=True, timeout=5,
2138
- )
2139
- try:
1898
+ with memory_read(db_path) as conn:
2140
1899
  row = conn.execute(
2141
1900
  "SELECT COUNT(*), "
2142
1901
  "SUM(CASE WHEN fisher_mean IS NOT NULL THEN 1 ELSE 0 END), "
@@ -2145,8 +1904,6 @@ def cmd_health(args: Namespace) -> None:
2145
1904
  (config.active_profile,),
2146
1905
  ).fetchone()
2147
1906
  total_facts, fisher_count, langevin_count = row or (0, 0, 0)
2148
- finally:
2149
- conn.close()
2150
1907
  facts = [None] * int(total_facts or 0)
2151
1908
  fisher_count = int(fisher_count or 0)
2152
1909
  langevin_count = int(langevin_count or 0)
@@ -2731,10 +2488,10 @@ def cmd_doctor(args: Namespace) -> None:
2731
2488
  db_path = slm_home / "memory.db"
2732
2489
  if db_path.exists():
2733
2490
  try:
2734
- import sqlite3
2735
- conn = sqlite3.connect(str(db_path))
2736
- result = conn.execute("PRAGMA integrity_check").fetchone()
2737
- conn.close()
2491
+ from superlocalmemory.storage.memory_write import memory_read
2492
+
2493
+ with memory_read(db_path) as conn:
2494
+ result = conn.execute("PRAGMA integrity_check").fetchone()
2738
2495
  if result and result[0] == "ok":
2739
2496
  size_mb = db_path.stat().st_size / (1024 * 1024)
2740
2497
  _check("Database", "PASS", f"OK ({size_mb:.2f} MB)")
@@ -3601,7 +3358,6 @@ def cmd_session_context(args: Namespace) -> None:
3601
3358
  output across MCP and CLI surfaces. --json flag returns structured JSON.
3602
3359
  """
3603
3360
  import sqlite3
3604
- from pathlib import Path
3605
3361
  from superlocalmemory.core.config import SLMConfig
3606
3362
 
3607
3363
  use_json = getattr(args, "json", False)
@@ -3644,8 +3400,11 @@ def cmd_session_context(args: Namespace) -> None:
3644
3400
  return
3645
3401
 
3646
3402
  pid = config.active_profile
3647
- conn = sqlite3.connect(str(db_path))
3648
- conn.row_factory = sqlite3.Row
3403
+ from superlocalmemory.storage.memory_write import memory_read
3404
+
3405
+ def _read_rows(query: str, params: tuple[object, ...]) -> list[sqlite3.Row]:
3406
+ with memory_read(db_path) as conn:
3407
+ return conn.execute(query, params).fetchall()
3649
3408
 
3650
3409
  # Collect facts for injection — same queries as pre-v3.4.65 but
3651
3410
  # mapped into InjectableMemory for the shared formatter.
@@ -3653,11 +3412,11 @@ def cmd_session_context(args: Namespace) -> None:
3653
3412
 
3654
3413
  # Core Memory blocks (compiled high-value context)
3655
3414
  try:
3656
- cm_rows = conn.execute(
3415
+ cm_rows = _read_rows(
3657
3416
  "SELECT block_type, content FROM core_memory_blocks "
3658
3417
  "WHERE profile_id = ? ORDER BY block_type",
3659
3418
  (pid,),
3660
- ).fetchall()
3419
+ )
3661
3420
  for r in cm_rows:
3662
3421
  content = f"[{r['block_type']}] {r['content']}"
3663
3422
  inj_mems.append(InjectableMemory(
@@ -3675,14 +3434,14 @@ def cmd_session_context(args: Namespace) -> None:
3675
3434
  if max_age > 0 else ""
3676
3435
  )
3677
3436
  try:
3678
- fact_rows = conn.execute(
3437
+ fact_rows = _read_rows(
3679
3438
  "SELECT fact_id, content, importance, access_count, fact_type FROM atomic_facts "
3680
3439
  "WHERE profile_id = ? "
3681
3440
  f"{age_clause}"
3682
3441
  "AND lifecycle = 'active' "
3683
3442
  "ORDER BY importance DESC, created_at DESC LIMIT 10",
3684
3443
  (pid,),
3685
- ).fetchall()
3444
+ )
3686
3445
  for r in fact_rows:
3687
3446
  inj_mems.append(InjectableMemory(
3688
3447
  content=r["content"],
@@ -3696,12 +3455,12 @@ def cmd_session_context(args: Namespace) -> None:
3696
3455
 
3697
3456
  # Session markers (last session summary)
3698
3457
  try:
3699
- sess_rows = conn.execute(
3458
+ sess_rows = _read_rows(
3700
3459
  "SELECT fact_id, content, importance, access_count FROM atomic_facts "
3701
3460
  "WHERE profile_id = ? AND content LIKE 'Session%' "
3702
3461
  "ORDER BY created_at DESC LIMIT 3",
3703
3462
  (pid,),
3704
- ).fetchall()
3463
+ )
3705
3464
  for r in sess_rows:
3706
3465
  inj_mems.append(InjectableMemory(
3707
3466
  content=r["content"],
@@ -3713,22 +3472,17 @@ def cmd_session_context(args: Namespace) -> None:
3713
3472
  except sqlite3.OperationalError:
3714
3473
  pass
3715
3474
 
3716
- conn.close()
3717
-
3718
3475
  if not inj_mems:
3719
3476
  return
3720
3477
 
3721
3478
  # V3.3 Soft prompts (auto-learned patterns) — append as high-importance
3722
3479
  try:
3723
- conn2 = sqlite3.connect(str(db_path))
3724
- conn2.row_factory = sqlite3.Row
3725
- sp_rows = conn2.execute(
3480
+ sp_rows = _read_rows(
3726
3481
  "SELECT category, content FROM soft_prompt_templates "
3727
3482
  "WHERE profile_id = ? AND active = 1 "
3728
3483
  "ORDER BY confidence DESC LIMIT 5",
3729
3484
  (pid,),
3730
- ).fetchall()
3731
- conn2.close()
3485
+ )
3732
3486
  for r in sp_rows:
3733
3487
  inj_mems.append(InjectableMemory(
3734
3488
  content=f"[{r['category']}] {r['content']}",