superlocalmemory 3.8.5 → 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 (81) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +9 -4
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/cli/commands.py +139 -404
  34. package/src/superlocalmemory/core/component_registry.py +4 -2
  35. package/src/superlocalmemory/core/embeddings.py +33 -6
  36. package/src/superlocalmemory/core/engine.py +94 -49
  37. package/src/superlocalmemory/core/engine_ingestion.py +150 -63
  38. package/src/superlocalmemory/core/ingestion_command.py +133 -21
  39. package/src/superlocalmemory/core/mutations.py +32 -10
  40. package/src/superlocalmemory/core/recall_pipeline.py +111 -77
  41. package/src/superlocalmemory/core/remember_admission.py +152 -0
  42. package/src/superlocalmemory/core/remember_runtime.py +712 -0
  43. package/src/superlocalmemory/graph/cozo_backend.py +5 -5
  44. package/src/superlocalmemory/learning/bandit.py +50 -1
  45. package/src/superlocalmemory/learning/source_quality.py +38 -35
  46. package/src/superlocalmemory/mcp/_daemon_proxy.py +38 -15
  47. package/src/superlocalmemory/mcp/tools_active.py +4 -41
  48. package/src/superlocalmemory/mcp/tools_core.py +26 -87
  49. package/src/superlocalmemory/mcp/tools_evolution.py +5 -10
  50. package/src/superlocalmemory/optimize/proxy/capture.py +196 -8
  51. package/src/superlocalmemory/retrieval/engine.py +8 -3
  52. package/src/superlocalmemory/retrieval/reranker.py +35 -10
  53. package/src/superlocalmemory/server/loopback.py +7 -13
  54. package/src/superlocalmemory/server/profile_runtime.py +14 -0
  55. package/src/superlocalmemory/server/routes/abstraction.py +2 -4
  56. package/src/superlocalmemory/server/routes/agents.py +3 -5
  57. package/src/superlocalmemory/server/routes/behavioral.py +5 -13
  58. package/src/superlocalmemory/server/routes/brain.py +6 -9
  59. package/src/superlocalmemory/server/routes/entity.py +3 -7
  60. package/src/superlocalmemory/server/routes/evolution.py +3 -5
  61. package/src/superlocalmemory/server/routes/helpers.py +44 -23
  62. package/src/superlocalmemory/server/routes/insights.py +2 -4
  63. package/src/superlocalmemory/server/routes/learning.py +2 -5
  64. package/src/superlocalmemory/server/routes/lifecycle.py +2 -4
  65. package/src/superlocalmemory/server/routes/memories.py +122 -100
  66. package/src/superlocalmemory/server/routes/tiers.py +3 -22
  67. package/src/superlocalmemory/server/routes/timeline.py +2 -4
  68. package/src/superlocalmemory/server/routes/v3_api.py +18 -16
  69. package/src/superlocalmemory/server/unified_daemon.py +200 -109
  70. package/src/superlocalmemory/storage/admission_codec.py +119 -0
  71. package/src/superlocalmemory/storage/admission_journal.py +728 -0
  72. package/src/superlocalmemory/storage/database.py +59 -0
  73. package/src/superlocalmemory/storage/deferred_writes.py +67 -11
  74. package/src/superlocalmemory/storage/memory_write.py +8 -12
  75. package/src/superlocalmemory/storage/migration_runner.py +37 -0
  76. package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +188 -0
  77. package/src/superlocalmemory/storage/read_connection.py +115 -0
  78. package/src/superlocalmemory/storage/write_coordinator.py +756 -0
  79. package/src/superlocalmemory/ui/index.html +1 -1
  80. package/src/superlocalmemory/ui/js/auto-settings.js +14 -1
  81. 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:
@@ -1339,8 +1358,7 @@ def cmd_list(args: Namespace) -> None:
1339
1358
 
1340
1359
 
1341
1360
  def cmd_remember(args: Namespace) -> None:
1342
- """Store a memory via the engine."""
1343
- from superlocalmemory.core.config import SLMConfig
1361
+ """Store a memory through the owned daemon."""
1344
1362
 
1345
1363
  use_json = getattr(args, 'json', False)
1346
1364
  sync_mode = getattr(args, 'sync_mode', False)
@@ -1358,128 +1376,43 @@ def cmd_remember(args: Namespace) -> None:
1358
1376
  if isinstance(_sw_raw, str) and _sw_raw.strip() else _sw_raw
1359
1377
  )
1360
1378
 
1361
- # Both paths use the one owned daemon. A second local engine for --sync
1362
- # duplicates heavyweight workers and can block for minutes on cold models.
1363
- daemon_owned = False
1364
1379
  try:
1365
1380
  from superlocalmemory.cli.daemon import (
1366
1381
  daemon_request, ensure_daemon, is_daemon_running,
1367
1382
  )
1368
- daemon_owned = is_daemon_running() or ensure_daemon()
1369
- if daemon_owned:
1370
- path = "/remember?wait=true" if sync_mode else "/remember"
1371
- result = daemon_request(
1372
- "POST", path, {
1373
- "content": args.content,
1374
- "tags": args.tags or "",
1375
- "scope": scope,
1376
- "shared_with": shared_with,
1377
- },
1378
- timeout_seconds=30,
1379
- )
1380
- if result and "fact_ids" in result:
1381
- if use_json:
1382
- from superlocalmemory.cli.json_output import json_print
1383
- json_print("remember", data=result)
1384
- else:
1385
- state = result.get("materialization_state", "queryable")
1386
- operation_id = result.get("operation_id", "unknown")
1387
- print(
1388
- f"{state.capitalize()} \u2713 {result['count']} facts "
1389
- f"(operation={operation_id})."
1390
- )
1391
- return
1392
- if sync_mode:
1393
- if use_json:
1394
- from superlocalmemory.cli.json_output import json_print
1395
- json_print("remember", error={
1396
- "code": "SYNC_TIMEOUT",
1397
- "message": (
1398
- "Canonical ingestion did not complete within 30s; "
1399
- "the durable operation remains available for retry."
1400
- ),
1401
- })
1402
- else:
1403
- print(
1404
- "Synchronous ingestion did not complete within 30s; "
1405
- "the durable operation remains queued.",
1406
- file=sys.stderr,
1407
- )
1408
- sys.exit(1)
1409
- except SystemExit:
1410
- raise
1411
- except Exception:
1412
- 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:
1413
1396
  if use_json:
1414
1397
  from superlocalmemory.cli.json_output import json_print
1415
- json_print("remember", error={
1416
- "code": "SYNC_TIMEOUT",
1417
- "message": "Owned daemon request failed before completion.",
1418
- })
1419
- sys.exit(1)
1420
- # Receipt-first writes may use the authenticated local fallback when
1421
- # no owned daemon exists.
1422
-
1423
- from superlocalmemory.core.engine import MemoryEngine
1424
-
1425
- try:
1426
- config = SLMConfig.load()
1427
- engine = MemoryEngine(config)
1428
- engine.initialize()
1429
-
1430
- # v3.6.15: resolve an unset scope to the configured default_scope.
1431
- _scope = scope or getattr(getattr(config, "scope", None), "default_scope", "personal")
1432
- from superlocalmemory.core.engine_ingestion import (
1433
- canonical_store,
1434
- local_trusted_actor_id,
1435
- )
1436
-
1437
- metadata = {"tags": args.tags} if args.tags else {}
1438
- operation = canonical_store(
1439
- engine,
1440
- args.content,
1441
- source_type="cli-sync" if sync_mode else "cli-offline-canonical",
1442
- trusted_actor_id=local_trusted_actor_id("cli"),
1443
- metadata=metadata,
1444
- scope=_scope,
1445
- shared_with=shared_with,
1446
- return_receipt=True,
1447
- )
1448
- except Exception as exc:
1449
- if use_json:
1450
- from superlocalmemory.cli.json_output import json_print
1451
- json_print("remember", error={"code": "STORE_ERROR", "message": str(exc)})
1452
- 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:
1453
1408
  raise
1454
-
1455
- fact_ids = list(operation.fact_ids) if hasattr(operation, "fact_ids") else list(operation)
1456
- operation_data = {
1457
- "fact_ids": fact_ids,
1458
- "count": len(fact_ids),
1459
- "materialization_state": getattr(
1460
- getattr(operation, "state", None), "value", "complete"
1461
- ),
1462
- }
1463
- if getattr(operation, "operation_id", None):
1464
- operation_data["operation_id"] = operation.operation_id
1465
-
1466
- if use_json:
1467
- from superlocalmemory.cli.json_output import json_print
1468
- json_print("remember", data=operation_data,
1469
- next_actions=[
1470
- {"command": "slm recall '<query>' --json", "description": "Search your memories"},
1471
- {"command": "slm list --json -n 5", "description": "See recent memories"},
1472
- ])
1473
- return
1474
-
1475
- print(
1476
- f"Complete \u2713 {len(fact_ids)} facts "
1477
- f"(operation={operation_data.get('operation_id', 'none')})."
1478
- )
1409
+ except Exception as exc:
1410
+ logger.warning("owned daemon remember request failed: %s", exc)
1411
+ _daemon_unavailable("remember", use_json)
1479
1412
 
1480
1413
 
1481
1414
  def cmd_recall(args: Namespace) -> None:
1482
- """Search memories via the engine routes through daemon if available."""
1415
+ """Search memories through the owned daemon without a local engine."""
1483
1416
  use_json = getattr(args, 'json', False)
1484
1417
  # v3.6.15: None = "not specified" → daemon/engine resolves the configured
1485
1418
  # default (shared-off). Only an explicit --include-global / --no-global
@@ -1488,7 +1421,6 @@ def cmd_recall(args: Namespace) -> None:
1488
1421
  include_shared = getattr(args, 'include_shared', None)
1489
1422
 
1490
1423
  # V3.3.21: Route through daemon for instant response (no cold start).
1491
- # Falls back to direct engine if daemon not running.
1492
1424
  # S9-DASH-02: pass a stable session_id derived from the shell's
1493
1425
  # parent PID so a sequence of CLI recalls in one terminal can be
1494
1426
  # grouped. The Stop hook on session end won't fire for CLI, so
@@ -1534,76 +1466,10 @@ def cmd_recall(args: Namespace) -> None:
1534
1466
  return
1535
1467
  except Exception as _exc: # noqa: BLE001
1536
1468
  logger.warning(
1537
- "Daemon recall failed, falling back to direct engine: %s", _exc
1469
+ "owned daemon recall request failed: %s", _exc
1538
1470
  )
1539
1471
 
1540
- from superlocalmemory.core.config import SLMConfig
1541
- from superlocalmemory.core.engine import MemoryEngine
1542
-
1543
- try:
1544
- config = SLMConfig.load()
1545
- engine = MemoryEngine(config)
1546
- engine.initialize()
1547
-
1548
- response = engine.recall(
1549
- args.query, limit=args.limit,
1550
- # v3.8.2: --fast → True; unset → None so engine resolves the
1551
- # client-driven-agentic default (parity with the daemon path, which
1552
- # omits the fast query param when --fast is absent).
1553
- fast=(True if getattr(args, "fast", False) else None),
1554
- include_global=include_global,
1555
- include_shared=include_shared,
1556
- window=getattr(args, "window", "") or None,
1557
- )
1558
- except Exception as exc:
1559
- if use_json:
1560
- from superlocalmemory.cli.json_output import json_print
1561
- json_print("recall", error={"code": "RECALL_ERROR", "message": str(exc)})
1562
- sys.exit(1)
1563
- raise
1564
-
1565
- # v3.6.6: route the direct-fallback path through the SAME shared
1566
- # serializer the daemon uses, so CLI-without-daemon output is identical
1567
- # to CLI/MCP-with-daemon (budget + source discipline + no_confident_match).
1568
- from superlocalmemory.server.recall_serializer import (
1569
- recall_response_metadata,
1570
- serialize_recall_response,
1571
- )
1572
- _rc = getattr(config, "retrieval", None)
1573
- _ser, _no_match = serialize_recall_response(
1574
- response,
1575
- limit=args.limit,
1576
- per_fact_max=getattr(_rc, "recall_per_fact_max_chars", 2400),
1577
- total_max=getattr(_rc, "recall_total_max_chars", 12000),
1578
- full=getattr(args, "full", False),
1579
- )
1580
-
1581
- if use_json:
1582
- from superlocalmemory.cli.json_output import json_print
1583
- items = []
1584
- for d in _ser:
1585
- items.append(dict(d))
1586
- json_print("recall", data={
1587
- "results": items, "count": len(items),
1588
- "query_type": getattr(response, "query_type", "unknown"),
1589
- "no_confident_match": _no_match,
1590
- **recall_response_metadata(response),
1591
- }, next_actions=[
1592
- {"command": "slm list --json", "description": "List recent memories"},
1593
- ])
1594
- return
1595
-
1596
- # Record learning signals (CLI path — works without MCP)
1597
- try:
1598
- _cli_record_signals(config, args.query, response.results)
1599
- except Exception:
1600
- pass
1601
-
1602
- if not _ser:
1603
- print("No confident match." if _no_match else "No memories found.")
1604
- return
1605
- for i, d in enumerate(_ser, 1):
1606
- print(f" {i}. [relevance {d['relevance_score']:.2f}] {d['content']}")
1472
+ _daemon_unavailable("recall", use_json)
1607
1473
 
1608
1474
 
1609
1475
  def _cli_record_signals(config, query, results):
@@ -1633,9 +1499,14 @@ def _cli_record_signals(config, query, results):
1633
1499
 
1634
1500
 
1635
1501
  def cmd_forget(args: Namespace) -> None:
1636
- """Delete memories matching a query."""
1637
- from superlocalmemory.core.engine import MemoryEngine
1638
- 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
+ )
1639
1510
 
1640
1511
  use_json = getattr(args, 'json', False)
1641
1512
  dry_run = getattr(args, 'dry_run', False)
@@ -1659,38 +1530,45 @@ def cmd_forget(args: Namespace) -> None:
1659
1530
  query_lower = "" if raw_query is None else raw_query.lower()
1660
1531
  query_label = raw_query if raw_query is not None else "*all*"
1661
1532
 
1662
- try:
1663
- config = SLMConfig.load()
1664
- engine = MemoryEngine(config)
1665
- engine.initialize()
1666
- facts = engine._db.get_all_facts(engine.profile_id)
1667
- matches = [f for f in facts if query_lower in f.content.lower()]
1668
- except Exception as exc:
1669
- if use_json:
1670
- from superlocalmemory.cli.json_output import json_print
1671
- json_print("forget", error={"code": "ENGINE_ERROR", "message": str(exc)})
1672
- sys.exit(1)
1673
- 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)
1674
1549
 
1675
- def delete_fact_authorized_for_cli(fact_id: str) -> None:
1676
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1677
- 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
+ ]
1678
1554
 
1679
- result = delete_fact_authorized(
1680
- engine,
1681
- fact_id,
1682
- trusted_actor_id=local_trusted_actor_id("cli"),
1683
- 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=""),
1684
1559
  )
1685
- if not result.get("ok"):
1686
- raise RuntimeError(result.get("error", "delete failed"))
1560
+ if not isinstance(result, dict) or not result.get("success"):
1561
+ _daemon_unavailable("forget", use_json)
1687
1562
 
1688
1563
  if use_json:
1689
1564
  from superlocalmemory.cli.json_output import json_print
1690
1565
  if not matches:
1691
1566
  json_print("forget", data={"matched_count": 0, "deleted_count": 0, "matches": []})
1692
1567
  return
1693
- 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
+ ]
1694
1572
  if dry_run:
1695
1573
  json_print("forget", data={
1696
1574
  "matched_count": len(matches), "deleted_count": 0,
@@ -1699,10 +1577,10 @@ def cmd_forget(args: Namespace) -> None:
1699
1577
  return
1700
1578
  if getattr(args, 'yes', False):
1701
1579
  for f in matches:
1702
- delete_fact_authorized_for_cli(f.fact_id)
1580
+ delete_from_daemon(str(f["id"]))
1703
1581
  json_print("forget", data={
1704
1582
  "matched_count": len(matches), "deleted_count": len(matches),
1705
- "deleted": [f.fact_id for f in matches],
1583
+ "deleted": [f["id"] for f in matches],
1706
1584
  }, next_actions=[
1707
1585
  {"command": "slm list --json", "description": "Verify remaining memories"},
1708
1586
  ])
@@ -1712,7 +1590,10 @@ def cmd_forget(args: Namespace) -> None:
1712
1590
  "matches": match_items,
1713
1591
  "hint": "Add --yes to confirm deletion",
1714
1592
  }, next_actions=[
1715
- {"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
+ },
1716
1597
  ])
1717
1598
  return
1718
1599
 
@@ -1721,19 +1602,19 @@ def cmd_forget(args: Namespace) -> None:
1721
1602
  return
1722
1603
  print(f"Found {len(matches)} matching memories:")
1723
1604
  for f in matches[:10]:
1724
- print(f" - {f.fact_id[:8]}... {f.content[:80]}")
1605
+ print(f" - {str(f['id'])[:8]}... {str(f.get('content', ''))[:80]}")
1725
1606
  if dry_run:
1726
1607
  print(f"(dry run — {len(matches)} would be deleted)")
1727
1608
  return
1728
1609
  if getattr(args, 'yes', False):
1729
1610
  for f in matches:
1730
- delete_fact_authorized_for_cli(f.fact_id)
1611
+ delete_from_daemon(str(f["id"]))
1731
1612
  print(f"Deleted {len(matches)} memories.")
1732
1613
  return
1733
1614
  confirm = input(f"Delete {len(matches)} memories? [y/N] ").strip().lower()
1734
1615
  if confirm in ("y", "yes"):
1735
1616
  for f in matches:
1736
- delete_fact_authorized_for_cli(f.fact_id)
1617
+ delete_from_daemon(str(f["id"]))
1737
1618
  print(f"Deleted {len(matches)} memories.")
1738
1619
  else:
1739
1620
  print("Cancelled.")
@@ -1743,13 +1624,15 @@ def cmd_delete(args: Namespace) -> None:
1743
1624
  """Delete a specific memory by exact fact ID."""
1744
1625
  import urllib.parse
1745
1626
 
1746
- from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
1747
- from superlocalmemory.core.config import SLMConfig
1748
- from superlocalmemory.core.engine import MemoryEngine
1627
+ from superlocalmemory.cli.daemon import (
1628
+ daemon_request,
1629
+ ensure_daemon,
1630
+ is_daemon_running,
1631
+ )
1749
1632
 
1750
1633
  use_json = getattr(args, 'json', False)
1751
1634
  fact_id = args.fact_id.strip()
1752
- if is_daemon_running():
1635
+ if is_daemon_running() or ensure_daemon():
1753
1636
  path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
1754
1637
  confirmed = getattr(args, "yes", False)
1755
1638
  content = ""
@@ -1759,14 +1642,7 @@ def cmd_delete(args: Namespace) -> None:
1759
1642
  "/api/facts/" + urllib.parse.quote(fact_id, safe=""),
1760
1643
  )
1761
1644
  if not isinstance(detail, dict):
1762
- if use_json:
1763
- from superlocalmemory.cli.json_output import json_print
1764
- json_print("delete", error={
1765
- "code": "DAEMON_MUTATION_FAILED",
1766
- "message": "Resident daemon could not resolve the memory.",
1767
- })
1768
- sys.exit(1)
1769
- raise RuntimeError("Resident daemon could not resolve the memory.")
1645
+ _daemon_unavailable("delete", use_json)
1770
1646
  content = str(detail.get("content") or "")
1771
1647
  if use_json:
1772
1648
  from superlocalmemory.cli.json_output import json_print
@@ -1792,14 +1668,7 @@ def cmd_delete(args: Namespace) -> None:
1792
1668
 
1793
1669
  result = daemon_request("DELETE", path)
1794
1670
  if not isinstance(result, dict) or not result.get("success"):
1795
- if use_json:
1796
- from superlocalmemory.cli.json_output import json_print
1797
- json_print("delete", error={
1798
- "code": "DAEMON_MUTATION_FAILED",
1799
- "message": "Resident daemon rejected the delete operation.",
1800
- })
1801
- sys.exit(1)
1802
- raise RuntimeError("Resident daemon rejected the delete operation.")
1671
+ _daemon_unavailable("delete", use_json)
1803
1672
  if use_json:
1804
1673
  from superlocalmemory.cli.json_output import json_print
1805
1674
  json_print(
@@ -1815,86 +1684,18 @@ def cmd_delete(args: Namespace) -> None:
1815
1684
  else:
1816
1685
  print(f"Deleted: {fact_id}")
1817
1686
  return
1818
-
1819
- try:
1820
- config = SLMConfig.load()
1821
- engine = MemoryEngine(config)
1822
- engine.initialize()
1823
-
1824
- rows = engine._db.execute(
1825
- "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
1826
- (fact_id, engine.profile_id),
1827
- )
1828
- except Exception as exc:
1829
- if use_json:
1830
- from superlocalmemory.cli.json_output import json_print
1831
- json_print("delete", error={"code": "ENGINE_ERROR", "message": str(exc)})
1832
- sys.exit(1)
1833
- raise
1834
-
1835
- if use_json:
1836
- from superlocalmemory.cli.json_output import json_print
1837
- if not rows:
1838
- json_print("delete", error={
1839
- "code": "NOT_FOUND", "message": f"Memory not found: {fact_id}",
1840
- })
1841
- sys.exit(1)
1842
- content = dict(rows[0]).get("content", "")
1843
- if getattr(args, "yes", False):
1844
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1845
- from superlocalmemory.core.mutations import delete_fact_authorized
1846
-
1847
- delete_fact_authorized(
1848
- engine,
1849
- fact_id,
1850
- trusted_actor_id=local_trusted_actor_id("cli"),
1851
- source_agent_id="cli",
1852
- )
1853
- json_print("delete", data={"deleted": fact_id, "content": content[:120]},
1854
- next_actions=[
1855
- {"command": "slm list --json", "description": "Verify remaining memories"},
1856
- ])
1857
- else:
1858
- json_print("delete", data={
1859
- "fact_id": fact_id, "content": content[:120], "deleted": False,
1860
- "hint": "Add --yes to confirm deletion",
1861
- }, next_actions=[
1862
- {"command": f"slm delete {fact_id} --json --yes", "description": "Confirm deletion"},
1863
- ])
1864
- return
1865
-
1866
- if not rows:
1867
- print(f"Memory not found: {fact_id}")
1868
- return
1869
-
1870
- content_preview = dict(rows[0]).get("content", "")[:120]
1871
- print(f"Memory: {content_preview}")
1872
-
1873
- if not getattr(args, "yes", False):
1874
- confirm = input("Delete this memory? [y/N] ").strip().lower()
1875
- if confirm not in ("y", "yes"):
1876
- print("Cancelled.")
1877
- return
1878
-
1879
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1880
- from superlocalmemory.core.mutations import delete_fact_authorized
1881
-
1882
- delete_fact_authorized(
1883
- engine,
1884
- fact_id,
1885
- trusted_actor_id=local_trusted_actor_id("cli"),
1886
- source_agent_id="cli",
1887
- )
1888
- print(f"Deleted: {fact_id}")
1687
+ _daemon_unavailable("delete", use_json)
1889
1688
 
1890
1689
 
1891
1690
  def cmd_update(args: Namespace) -> None:
1892
1691
  """Update the content of a specific memory by exact fact ID."""
1893
1692
  import urllib.parse
1894
1693
 
1895
- from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
1896
- from superlocalmemory.core.config import SLMConfig
1897
- from superlocalmemory.core.engine import MemoryEngine
1694
+ from superlocalmemory.cli.daemon import (
1695
+ daemon_request,
1696
+ ensure_daemon,
1697
+ is_daemon_running,
1698
+ )
1898
1699
 
1899
1700
  use_json = getattr(args, 'json', False)
1900
1701
  fact_id = args.fact_id.strip()
@@ -1908,18 +1709,11 @@ def cmd_update(args: Namespace) -> None:
1908
1709
  print("Error: content cannot be empty")
1909
1710
  return
1910
1711
 
1911
- if is_daemon_running():
1712
+ if is_daemon_running() or ensure_daemon():
1912
1713
  path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
1913
1714
  result = daemon_request("PATCH", path, {"content": new_content})
1914
1715
  if not isinstance(result, dict) or not result.get("success"):
1915
- if use_json:
1916
- from superlocalmemory.cli.json_output import json_print
1917
- json_print("update", error={
1918
- "code": "DAEMON_MUTATION_FAILED",
1919
- "message": "Resident daemon rejected the update operation.",
1920
- })
1921
- sys.exit(1)
1922
- raise RuntimeError("Resident daemon rejected the update operation.")
1716
+ _daemon_unavailable("update", use_json)
1923
1717
  if use_json:
1924
1718
  from superlocalmemory.cli.json_output import json_print
1925
1719
  json_print("update", data={
@@ -1935,59 +1729,7 @@ def cmd_update(args: Namespace) -> None:
1935
1729
  print(f"New: {new_content[:100]}")
1936
1730
  print(f"Updated: {fact_id}")
1937
1731
  return
1938
-
1939
- try:
1940
- config = SLMConfig.load()
1941
- engine = MemoryEngine(config)
1942
- engine.initialize()
1943
-
1944
- rows = engine._db.execute(
1945
- "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
1946
- (fact_id, engine.profile_id),
1947
- )
1948
- except Exception as exc:
1949
- if use_json:
1950
- from superlocalmemory.cli.json_output import json_print
1951
- json_print("update", error={"code": "ENGINE_ERROR", "message": str(exc)})
1952
- sys.exit(1)
1953
- raise
1954
-
1955
- if not rows:
1956
- if use_json:
1957
- from superlocalmemory.cli.json_output import json_print
1958
- json_print("update", error={
1959
- "code": "NOT_FOUND", "message": f"Memory not found: {fact_id}",
1960
- })
1961
- sys.exit(1)
1962
- print(f"Memory not found: {fact_id}")
1963
- return
1964
-
1965
- old_content = dict(rows[0]).get("content", "")
1966
- from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
1967
- from superlocalmemory.core.mutations import update_fact_authorized
1968
-
1969
- update_fact_authorized(
1970
- engine,
1971
- fact_id,
1972
- new_content,
1973
- trusted_actor_id=local_trusted_actor_id("cli"),
1974
- source_agent_id="cli",
1975
- )
1976
-
1977
- if use_json:
1978
- from superlocalmemory.cli.json_output import json_print
1979
- json_print("update", data={
1980
- "fact_id": fact_id,
1981
- "old_content": old_content[:120],
1982
- "new_content": new_content[:120],
1983
- }, next_actions=[
1984
- {"command": "slm list --json", "description": "List recent memories"},
1985
- ])
1986
- return
1987
-
1988
- print(f"Old: {old_content[:100]}")
1989
- print(f"New: {new_content[:100]}")
1990
- print(f"Updated: {fact_id}")
1732
+ _daemon_unavailable("update", use_json)
1991
1733
 
1992
1734
 
1993
1735
  # -- Diagnostics (all support --json) -------------------------------------
@@ -2150,12 +1892,10 @@ def cmd_health(args: Namespace) -> None:
2150
1892
  # second MemoryEngine re-runs schema initialization and can lock
2151
1893
  # the user's database. Health only needs aggregate counts, so use
2152
1894
  # a read-only snapshot connection instead.
2153
- import sqlite3
1895
+ from superlocalmemory.storage.memory_write import memory_read
1896
+
2154
1897
  db_path = config.db_path
2155
- conn = sqlite3.connect(
2156
- f"file:{db_path}?mode=ro", uri=True, timeout=5,
2157
- )
2158
- try:
1898
+ with memory_read(db_path) as conn:
2159
1899
  row = conn.execute(
2160
1900
  "SELECT COUNT(*), "
2161
1901
  "SUM(CASE WHEN fisher_mean IS NOT NULL THEN 1 ELSE 0 END), "
@@ -2164,8 +1904,6 @@ def cmd_health(args: Namespace) -> None:
2164
1904
  (config.active_profile,),
2165
1905
  ).fetchone()
2166
1906
  total_facts, fisher_count, langevin_count = row or (0, 0, 0)
2167
- finally:
2168
- conn.close()
2169
1907
  facts = [None] * int(total_facts or 0)
2170
1908
  fisher_count = int(fisher_count or 0)
2171
1909
  langevin_count = int(langevin_count or 0)
@@ -2750,10 +2488,10 @@ def cmd_doctor(args: Namespace) -> None:
2750
2488
  db_path = slm_home / "memory.db"
2751
2489
  if db_path.exists():
2752
2490
  try:
2753
- import sqlite3
2754
- conn = sqlite3.connect(str(db_path))
2755
- result = conn.execute("PRAGMA integrity_check").fetchone()
2756
- 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()
2757
2495
  if result and result[0] == "ok":
2758
2496
  size_mb = db_path.stat().st_size / (1024 * 1024)
2759
2497
  _check("Database", "PASS", f"OK ({size_mb:.2f} MB)")
@@ -3620,7 +3358,6 @@ def cmd_session_context(args: Namespace) -> None:
3620
3358
  output across MCP and CLI surfaces. --json flag returns structured JSON.
3621
3359
  """
3622
3360
  import sqlite3
3623
- from pathlib import Path
3624
3361
  from superlocalmemory.core.config import SLMConfig
3625
3362
 
3626
3363
  use_json = getattr(args, "json", False)
@@ -3663,8 +3400,11 @@ def cmd_session_context(args: Namespace) -> None:
3663
3400
  return
3664
3401
 
3665
3402
  pid = config.active_profile
3666
- conn = sqlite3.connect(str(db_path))
3667
- 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()
3668
3408
 
3669
3409
  # Collect facts for injection — same queries as pre-v3.4.65 but
3670
3410
  # mapped into InjectableMemory for the shared formatter.
@@ -3672,11 +3412,11 @@ def cmd_session_context(args: Namespace) -> None:
3672
3412
 
3673
3413
  # Core Memory blocks (compiled high-value context)
3674
3414
  try:
3675
- cm_rows = conn.execute(
3415
+ cm_rows = _read_rows(
3676
3416
  "SELECT block_type, content FROM core_memory_blocks "
3677
3417
  "WHERE profile_id = ? ORDER BY block_type",
3678
3418
  (pid,),
3679
- ).fetchall()
3419
+ )
3680
3420
  for r in cm_rows:
3681
3421
  content = f"[{r['block_type']}] {r['content']}"
3682
3422
  inj_mems.append(InjectableMemory(
@@ -3694,14 +3434,14 @@ def cmd_session_context(args: Namespace) -> None:
3694
3434
  if max_age > 0 else ""
3695
3435
  )
3696
3436
  try:
3697
- fact_rows = conn.execute(
3437
+ fact_rows = _read_rows(
3698
3438
  "SELECT fact_id, content, importance, access_count, fact_type FROM atomic_facts "
3699
3439
  "WHERE profile_id = ? "
3700
3440
  f"{age_clause}"
3701
3441
  "AND lifecycle = 'active' "
3702
3442
  "ORDER BY importance DESC, created_at DESC LIMIT 10",
3703
3443
  (pid,),
3704
- ).fetchall()
3444
+ )
3705
3445
  for r in fact_rows:
3706
3446
  inj_mems.append(InjectableMemory(
3707
3447
  content=r["content"],
@@ -3715,12 +3455,12 @@ def cmd_session_context(args: Namespace) -> None:
3715
3455
 
3716
3456
  # Session markers (last session summary)
3717
3457
  try:
3718
- sess_rows = conn.execute(
3458
+ sess_rows = _read_rows(
3719
3459
  "SELECT fact_id, content, importance, access_count FROM atomic_facts "
3720
3460
  "WHERE profile_id = ? AND content LIKE 'Session%' "
3721
3461
  "ORDER BY created_at DESC LIMIT 3",
3722
3462
  (pid,),
3723
- ).fetchall()
3463
+ )
3724
3464
  for r in sess_rows:
3725
3465
  inj_mems.append(InjectableMemory(
3726
3466
  content=r["content"],
@@ -3732,22 +3472,17 @@ def cmd_session_context(args: Namespace) -> None:
3732
3472
  except sqlite3.OperationalError:
3733
3473
  pass
3734
3474
 
3735
- conn.close()
3736
-
3737
3475
  if not inj_mems:
3738
3476
  return
3739
3477
 
3740
3478
  # V3.3 Soft prompts (auto-learned patterns) — append as high-importance
3741
3479
  try:
3742
- conn2 = sqlite3.connect(str(db_path))
3743
- conn2.row_factory = sqlite3.Row
3744
- sp_rows = conn2.execute(
3480
+ sp_rows = _read_rows(
3745
3481
  "SELECT category, content FROM soft_prompt_templates "
3746
3482
  "WHERE profile_id = ? AND active = 1 "
3747
3483
  "ORDER BY confidence DESC LIMIT 5",
3748
3484
  (pid,),
3749
- ).fetchall()
3750
- conn2.close()
3485
+ )
3751
3486
  for r in sp_rows:
3752
3487
  inj_mems.append(InjectableMemory(
3753
3488
  content=f"[{r['category']}] {r['content']}",