superlocalmemory 4.1.0 → 4.1.3

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 (47) hide show
  1. package/.claude-plugin/marketplace.json +29 -1
  2. package/CHANGELOG.md +107 -0
  3. package/README.md +37 -72
  4. package/ide/configs/codex-mcp.toml +3 -1
  5. package/package.json +4 -3
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.mcp.json +1 -3
  8. package/plugin/CLAUDE.md +3 -3
  9. package/plugin/agents/slm-governance-advisor.md +1 -1
  10. package/plugin/agents/slm-loop-runner.md +1 -1
  11. package/plugin/agents/slm-memory-advisor.md +1 -1
  12. package/plugin/agents/slm-optimize-advisor.md +1 -1
  13. package/plugin/requirements.txt +1 -1
  14. package/plugin/scripts/slm-launch +100 -31
  15. package/plugin/skills/slm-cache/SKILL.md +1 -1
  16. package/plugin/skills/slm-compress/SKILL.md +1 -1
  17. package/plugin/skills/slm-governance/SKILL.md +1 -1
  18. package/plugin/skills/slm-graph/SKILL.md +1 -1
  19. package/plugin/skills/slm-loop/SKILL.md +1 -1
  20. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  21. package/plugin/skills/slm-profile/SKILL.md +1 -1
  22. package/plugin/skills/slm-recall/SKILL.md +1 -1
  23. package/plugin/skills/slm-remember/SKILL.md +1 -1
  24. package/plugin/skills/slm-scope/SKILL.md +1 -1
  25. package/plugin/skills/slm-session/SKILL.md +1 -1
  26. package/plugin/skills/slm-status/SKILL.md +1 -1
  27. package/plugin-src/rules/AGENTS.md +1 -1
  28. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  33. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-profile/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  36. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  38. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  39. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  40. package/pyproject.toml +1 -1
  41. package/src/superlocalmemory/__init__.py +1 -1
  42. package/src/superlocalmemory/cli/commands.py +94 -0
  43. package/src/superlocalmemory/server/recall_health.py +87 -10
  44. package/src/superlocalmemory/server/unified_daemon.py +55 -2
  45. package/src/superlocalmemory/storage/_migration_internals.py +23 -2
  46. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +60 -36
  47. package/src/superlocalmemory/storage/schema.py +23 -0
@@ -409,45 +409,31 @@ def _sync_lifecycle_mirror(conn: sqlite3.Connection) -> None:
409
409
  """)
410
410
 
411
411
 
412
- def verify(conn: sqlite3.Connection) -> bool:
413
- """Whether the repair's end-state holds.
414
-
415
- Called on every start for an already-complete migration. Returning False
416
- routes to ``repair()``, which makes this a standing guard: if pollution ever
417
- reappears, the next daemon start withholds it without anyone asking.
412
+ def unmet(conn: sqlite3.Connection) -> str:
413
+ """Which check does not hold, named. Empty string when all of them do.
414
+
415
+ ``verify()`` returns a bare boolean, so when a completed migration stops
416
+ verifying the runner can only say "safe repair did not restore M043". This
417
+ checks five separate things, and that sentence names none of them -- a user
418
+ hitting it had to come back and ask which, and so did we. This is the same
419
+ gap that ``migration_failure_reasons`` closed one level up, left open one
420
+ level down.
418
421
  """
419
422
  if not _table_exists(conn, "atomic_facts"):
420
- return True
423
+ return ""
421
424
  if not _has_column(conn, "atomic_facts", "quarantined"):
422
- return False
425
+ return "atomic_facts has no 'quarantined' column"
423
426
  if not _table_exists(conn, "consolidated_summaries"):
424
- return False
425
-
427
+ return "the consolidated_summaries display table is missing"
426
428
  if _table_exists(conn, "fact_consolidations"):
427
- unwithheld = _count(
429
+ n = _count(
428
430
  conn,
429
431
  "SELECT COUNT(*) FROM atomic_facts WHERE COALESCE(quarantined, 0) = 0 "
430
432
  " AND fact_id IN (" + _CONSOLIDATOR_ROWS + ")",
431
433
  )
432
- if unwithheld:
433
- return False
434
-
435
- # Every withheld row must still be visible somewhere, or the repair has
436
- # deleted the owner's view of it rather than moved it.
437
- #
438
- # BY IDENTITY *OR* CONTENT, and the "or" is what makes this an invariant
439
- # rather than a trap. Matching on content alone could never become true
440
- # once a row's content changed after being preserved: the display copy
441
- # keeps the old text, verify stays false, repair() runs apply() again,
442
- # apply() cannot change the past, and the migration is reported failed
443
- # on every start for the rest of the store's life. Matching on identity
444
- # alone fails the other way, because two withheld rows with identical
445
- # text collapse into one display row under the unique triple, leaving the
446
- # second with no row of its own id.
447
- #
448
- # Either match satisfies the guarantee that actually matters: nothing
449
- # the owner could see has stopped being visible.
450
- unpreserved = _count(conn, """
434
+ if n:
435
+ return f"{n} model-written summaries are not withheld from recall"
436
+ n = _count(conn, """
451
437
  SELECT COUNT(*) FROM atomic_facts af
452
438
  WHERE af.fact_id IN (""" + _CONSOLIDATOR_ROWS + """)
453
439
  AND NOT EXISTS (
@@ -457,13 +443,51 @@ def verify(conn: sqlite3.Connection) -> bool:
457
443
  OR cs.content = af.content)
458
444
  )
459
445
  """)
460
- if unpreserved:
461
- return False
462
-
446
+ if n:
447
+ return f"{n} withheld summaries have no display copy"
463
448
  if _table_exists(conn, "fact_retention"):
464
- if _count(conn, "SELECT COUNT(*) FROM (" + _wrongly_hidden(conn) + ")"):
465
- return False
466
- return True
449
+ n = _count(conn, "SELECT COUNT(*) FROM (" + _wrongly_hidden(conn) + ")")
450
+ if n:
451
+ return f"{n} real memories are hidden from recall and should not be"
452
+ return ""
453
+
454
+
455
+ def blocks_serving(conn: sqlite3.Connection) -> bool:
456
+ """Should a daemon refuse to serve while this check does not hold?
457
+
458
+ Only when the SCHEMA is missing. The two schema conditions here -- the
459
+ column and the display table -- mean queries would hit something that is not
460
+ there, so refusing is right. The other three are about DATA: a summary that
461
+ should be withheld is not withheld, or a real memory is hidden. Those make
462
+ some answers worse; they do not stop the store working.
463
+
464
+ The distinction matters because this ``verify()`` is a standing guard over
465
+ data that ordinary use can re-violate -- a consolidation pass hiding one more
466
+ memory is enough. Treating that like a missing table meant one drifted row
467
+ could return 503 on every route indefinitely, with a manual restart the only
468
+ way out. That is an outage caused by a quality check, which is worse than the
469
+ thing the check is for.
470
+
471
+ Reported as #125, where a user's daemon sat unusable on exactly this.
472
+ """
473
+ if not _table_exists(conn, "atomic_facts"):
474
+ return False
475
+ if not _has_column(conn, "atomic_facts", "quarantined"):
476
+ return True
477
+ return not _table_exists(conn, "consolidated_summaries")
478
+
479
+
480
+ def verify(conn: sqlite3.Connection) -> bool:
481
+ """Whether the repair's end-state holds.
482
+
483
+ Called on every start for an already-complete migration. Returning False
484
+ routes to ``repair()``, which makes this a standing guard: if pollution ever
485
+ reappears, the next daemon start withholds it without anyone asking.
486
+
487
+ Thin wrapper over ``unmet()`` so the two can never disagree about what
488
+ "verified" means.
489
+ """
490
+ return not unmet(conn)
467
491
 
468
492
 
469
493
  def repair(conn: sqlite3.Connection) -> None:
@@ -972,6 +972,18 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
972
972
  #: which SQLite offers no IF NOT EXISTS form of, so presence is checked first.
973
973
  _ADDITIVE_COLUMNS: Final[tuple[tuple[str, str, str], ...]] = (
974
974
  ("atomic_facts", "quarantined", "INTEGER NOT NULL DEFAULT 0"),
975
+ # ``pinned`` arrives with M015, which is a DEFERRED migration -- it runs
976
+ # after the engine is up. But the DDL below indexes it
977
+ # (``idx_facts_pinned``), and that DDL runs during engine start. On a store
978
+ # old enough to predate M015 the index therefore raised "no such column:
979
+ # pinned" before anything could add it, and every start failed the same way:
980
+ # the deferred pass could not run because the engine could not start, and
981
+ # the engine could not start because the deferred pass had not run.
982
+ # ``slm db migrate`` did not break the loop either -- it reports Failed=0
983
+ # and skips deferred migrations by definition.
984
+ # The column has to exist before its own index either way, so it belongs
985
+ # here, where a store gets it at start regardless of migration state.
986
+ ("atomic_facts", "pinned", "INTEGER NOT NULL DEFAULT 0"),
975
987
  )
976
988
 
977
989
 
@@ -1011,6 +1023,17 @@ def create_all_tables(conn: sqlite3.Connection) -> None:
1011
1023
  """
1012
1024
  _set_pragmas(conn)
1013
1025
 
1026
+ # Before the DDL, not only after it. The DDL below indexes columns that an
1027
+ # upgraded store may not have yet, and an index on a column that does not
1028
+ # exist is a hard error, not a skipped statement -- so the whole of
1029
+ # create_all_tables would raise and the engine would never start.
1030
+ #
1031
+ # On a fresh database this pass does nothing: the tables do not exist yet,
1032
+ # which the helper treats as "nothing to alter". On an upgraded one the
1033
+ # tables are already there and this is exactly where the columns are owed.
1034
+ # It runs again at the end for tables created during this call.
1035
+ _add_missing_columns(conn)
1036
+
1014
1037
  for ddl in _DDL_ORDERED:
1015
1038
  conn.executescript(ddl)
1016
1039