superlocalmemory 4.0.9 → 4.0.10
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/CHANGELOG.md +75 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +45 -2
- package/src/superlocalmemory/cli/main.py +2 -2
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
- package/src/superlocalmemory/core/fact_consolidator.py +316 -125
- package/src/superlocalmemory/core/maintenance.py +44 -6
- package/src/superlocalmemory/core/memory_health.py +266 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
- package/src/superlocalmemory/core/operation_request.py +1 -1
- package/src/superlocalmemory/core/ops_remediation.py +2 -2
- package/src/superlocalmemory/core/store_pipeline.py +78 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +15 -1
- package/src/superlocalmemory/mcp/server.py +1 -1
- package/src/superlocalmemory/mcp/session_binding.py +92 -0
- package/src/superlocalmemory/mcp/tools_core.py +40 -39
- package/src/superlocalmemory/mcp/tools_ops.py +2 -2
- package/src/superlocalmemory/retrieval/bm25_channel.py +4 -8
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -1
- package/src/superlocalmemory/retrieval/scope_policy.py +22 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +13 -1
- package/src/superlocalmemory/retrieval/vector_store.py +63 -0
- package/src/superlocalmemory/server/api.py +6 -1
- package/src/superlocalmemory/server/asset_versions.py +171 -0
- package/src/superlocalmemory/server/routes/abstraction.py +201 -0
- package/src/superlocalmemory/server/routes/data_io.py +29 -1
- package/src/superlocalmemory/server/routes/entity.py +13 -1
- package/src/superlocalmemory/server/routes/mesh.py +1 -1
- package/src/superlocalmemory/server/routes/v3_api.py +2 -2
- package/src/superlocalmemory/server/ui.py +8 -1
- package/src/superlocalmemory/server/unified_daemon.py +111 -9
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/database.py +128 -30
- package/src/superlocalmemory/storage/migration_runner.py +11 -0
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
- package/src/superlocalmemory/storage/schema.py +98 -0
- package/src/superlocalmemory/summaries/base.py +1 -1
- package/src/superlocalmemory/summaries/non_answer.py +223 -0
- package/src/superlocalmemory/ui/index.html +1 -1
- package/src/superlocalmemory/ui/js/od-memories.js +190 -1
- package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
|
@@ -686,8 +686,12 @@ class DatabaseManager:
|
|
|
686
686
|
include_shared=include_shared,
|
|
687
687
|
prefix="f",
|
|
688
688
|
)
|
|
689
|
+
# Pins are injected straight into an agent's context, which makes this
|
|
690
|
+
# the most consequential display path in the class: a withheld row here
|
|
691
|
+
# is not merely shown, it is asserted as background truth.
|
|
689
692
|
rows = self.execute(
|
|
690
693
|
f"SELECT f.* FROM atomic_facts f WHERE {where} AND f.pinned = 1 "
|
|
694
|
+
f"{self.visible_fact_clause('f')} "
|
|
691
695
|
"AND NOT EXISTS ("
|
|
692
696
|
" SELECT 1 FROM fact_temporal_validity tv "
|
|
693
697
|
" WHERE tv.fact_id = f.fact_id "
|
|
@@ -733,6 +737,65 @@ class DatabaseManager:
|
|
|
733
737
|
self._archive_col_present = True
|
|
734
738
|
return present
|
|
735
739
|
|
|
740
|
+
def _has_quarantine_column(self) -> bool:
|
|
741
|
+
"""Whether atomic_facts carries the ``quarantined`` column.
|
|
742
|
+
|
|
743
|
+
Same shape as ``_has_archive_status``: cached once True (a column never
|
|
744
|
+
disappears), re-checked while absent so a later schema pass is picked
|
|
745
|
+
up. ``storage.schema.create_all_tables`` adds the column at every engine
|
|
746
|
+
init, so on any store the daemon has opened this is True — the guard
|
|
747
|
+
exists for a bare DatabaseManager pointed at a store that engine init
|
|
748
|
+
never touched, where filtering on the column would raise instead of
|
|
749
|
+
returning results.
|
|
750
|
+
"""
|
|
751
|
+
if getattr(self, "_quarantine_col_present", False):
|
|
752
|
+
return True
|
|
753
|
+
present = any(
|
|
754
|
+
dict(row).get("name") == "quarantined"
|
|
755
|
+
for row in self.execute("PRAGMA table_info(atomic_facts)")
|
|
756
|
+
)
|
|
757
|
+
if present:
|
|
758
|
+
self._quarantine_col_present = True
|
|
759
|
+
return present
|
|
760
|
+
|
|
761
|
+
def visible_fact_clause(
|
|
762
|
+
self, prefix: str = "", *, include_quarantined: bool = False,
|
|
763
|
+
) -> str:
|
|
764
|
+
"""AND-clause excluding rows no caller should be shown as a memory.
|
|
765
|
+
|
|
766
|
+
Two exclusions, one definition: soft-deleted (``archive_status``) and
|
|
767
|
+
withheld (``quarantined``). Both are presence-guarded, because each
|
|
768
|
+
column arrives with a migration and may be absent on a store the engine
|
|
769
|
+
has not opened.
|
|
770
|
+
|
|
771
|
+
WHY THIS EXISTS AS A FUNCTION. 4.0.10 first put the quarantine filter in
|
|
772
|
+
``get_facts_by_ids`` alone, reasoning that every retrieval channel
|
|
773
|
+
re-authorises through it and the engine drops what it cannot hydrate.
|
|
774
|
+
That reasoning was correct and the conclusion was wrong: it covered the
|
|
775
|
+
RECALL pipeline, and ``search``, ``list_recent``, ``fetch``, the MCP
|
|
776
|
+
resources and the dashboard's own search are not the recall pipeline.
|
|
777
|
+
Measured on a copy of the author's store, ``search_facts_fts`` returned
|
|
778
|
+
20 withheld rows out of 50 and ``get_all_facts`` 66 out of 400 — the
|
|
779
|
+
exact defect the design was meant to prevent, in the paths the design
|
|
780
|
+
never looked at.
|
|
781
|
+
|
|
782
|
+
There is no single SQL choke point in this codebase; ``_scope_where`` is
|
|
783
|
+
spliced against six other tables and cannot carry a fact column. So the
|
|
784
|
+
honest form of "one place" is one CLAUSE with an enumerable set of call
|
|
785
|
+
sites, and a test that fails when a read path does not use it:
|
|
786
|
+
tests/test_storage/test_no_read_path_shows_a_withheld_row.py
|
|
787
|
+
|
|
788
|
+
``include_quarantined=True`` is for repair, erasure and export — paths
|
|
789
|
+
that must reach a withheld row to act on it.
|
|
790
|
+
"""
|
|
791
|
+
table = f"{prefix}." if prefix else ""
|
|
792
|
+
clause = ""
|
|
793
|
+
if self._has_archive_status():
|
|
794
|
+
clause += f" AND COALESCE({table}archive_status, 'live') != 'archived'"
|
|
795
|
+
if not include_quarantined and self._has_quarantine_column():
|
|
796
|
+
clause += f" AND COALESCE({table}quarantined, 0) = 0"
|
|
797
|
+
return clause
|
|
798
|
+
|
|
736
799
|
def get_all_facts(
|
|
737
800
|
self, profile_id: str, limit: int | None = None,
|
|
738
801
|
*,
|
|
@@ -755,14 +818,10 @@ class DatabaseManager:
|
|
|
755
818
|
# hard, env-tunable ceiling even when the caller passes limit=None.
|
|
756
819
|
if limit is None:
|
|
757
820
|
limit = _unbounded_facts_ceiling()
|
|
758
|
-
#
|
|
759
|
-
archive_clause = (
|
|
760
|
-
" AND COALESCE(archive_status, 'live') != 'archived'"
|
|
761
|
-
if self._has_archive_status()
|
|
762
|
-
else ""
|
|
763
|
-
)
|
|
821
|
+
# Soft-deleted and withheld rows are not memories a caller may see.
|
|
764
822
|
rows = self.execute(
|
|
765
|
-
f"SELECT * FROM atomic_facts WHERE {where}
|
|
823
|
+
f"SELECT * FROM atomic_facts WHERE {where}"
|
|
824
|
+
f"{self.visible_fact_clause()} "
|
|
766
825
|
"ORDER BY created_at DESC LIMIT ?",
|
|
767
826
|
(*params, int(limit)),
|
|
768
827
|
)
|
|
@@ -790,14 +849,12 @@ class DatabaseManager:
|
|
|
790
849
|
include_global=include_global,
|
|
791
850
|
include_shared=include_shared,
|
|
792
851
|
)
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
else ""
|
|
797
|
-
)
|
|
852
|
+
# Crossing a profile boundary is the last place a withheld row should
|
|
853
|
+
# appear: it would be a model's non-answer presented to somebody else
|
|
854
|
+
# as one of this profile's shared memories.
|
|
798
855
|
rows = self.execute(
|
|
799
856
|
f"SELECT * FROM atomic_facts WHERE {where} AND profile_id != ?"
|
|
800
|
-
f"{
|
|
857
|
+
f"{self.visible_fact_clause()} ORDER BY created_at DESC",
|
|
801
858
|
(*params, profile_id),
|
|
802
859
|
)
|
|
803
860
|
return [self._row_to_fact(r) for r in rows]
|
|
@@ -953,14 +1010,21 @@ class DatabaseManager:
|
|
|
953
1010
|
include_global: bool = False,
|
|
954
1011
|
include_shared: bool = False,
|
|
955
1012
|
) -> int:
|
|
956
|
-
"""
|
|
1013
|
+
"""Memories this profile has, as the owner would count them.
|
|
1014
|
+
|
|
1015
|
+
Counts what a caller can be shown, which is why it applies
|
|
1016
|
+
``visible_fact_clause``. It fed the dashboard's "All memories 5,093" and
|
|
1017
|
+
was counting 1,195 withheld summaries and every soft-deleted row into
|
|
1018
|
+
that figure -- a number the owner reads as "how much do I remember".
|
|
1019
|
+
"""
|
|
957
1020
|
where, params = _scope_where(
|
|
958
1021
|
profile_id,
|
|
959
1022
|
include_global=include_global,
|
|
960
1023
|
include_shared=include_shared,
|
|
961
1024
|
)
|
|
962
1025
|
rows = self.execute(
|
|
963
|
-
f"SELECT COUNT(*) AS c FROM atomic_facts WHERE {where}"
|
|
1026
|
+
f"SELECT COUNT(*) AS c FROM atomic_facts WHERE {where}"
|
|
1027
|
+
f"{self.visible_fact_clause()}", (*params,),
|
|
964
1028
|
)
|
|
965
1029
|
return int(rows[0]["c"]) if rows else 0
|
|
966
1030
|
|
|
@@ -1230,16 +1294,15 @@ class DatabaseManager:
|
|
|
1230
1294
|
include_shared=include_shared,
|
|
1231
1295
|
prefix="f",
|
|
1232
1296
|
)
|
|
1233
|
-
#
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
else ""
|
|
1238
|
-
)
|
|
1297
|
+
# Full-text search is a display path: the dashboard search box, the
|
|
1298
|
+
# `search` tool and `fetch` all land here, and none of them go through
|
|
1299
|
+
# the recall engine. Before 4.0.10 put the clause here it returned 20
|
|
1300
|
+
# withheld rows out of 50 on the author's store.
|
|
1239
1301
|
rows = self.execute(
|
|
1240
1302
|
f"""SELECT f.* FROM atomic_facts_fts AS fts
|
|
1241
1303
|
JOIN atomic_facts AS f ON f.fact_id = fts.fact_id
|
|
1242
|
-
WHERE fts.atomic_facts_fts MATCH ? AND {where}
|
|
1304
|
+
WHERE fts.atomic_facts_fts MATCH ? AND {where}
|
|
1305
|
+
{self.visible_fact_clause('f')}
|
|
1243
1306
|
ORDER BY fts.rank LIMIT ?""",
|
|
1244
1307
|
(match_expr, *params, limit),
|
|
1245
1308
|
)
|
|
@@ -1270,12 +1333,22 @@ class DatabaseManager:
|
|
|
1270
1333
|
# ------------------------------------------------------------------
|
|
1271
1334
|
|
|
1272
1335
|
def get_fact(self, fact_id: str, profile_id: str | None = None) -> AtomicFact | None:
|
|
1273
|
-
"""Get a single
|
|
1336
|
+
"""Get a single row by ID, exactly as stored. NOT a display path.
|
|
1274
1337
|
|
|
1275
1338
|
C4 defense-in-depth: when ``profile_id`` is provided the lookup is
|
|
1276
1339
|
tenant-scoped so a fact_id from another profile cannot resolve. Left
|
|
1277
1340
|
optional (fact_id is a random UUID sourced from already-scoped queries)
|
|
1278
1341
|
to avoid destabilizing the core store/consolidation write path.
|
|
1342
|
+
|
|
1343
|
+
DELIBERATELY UNFILTERED, and this is load-bearing. It applies neither
|
|
1344
|
+
``archive_status`` nor ``quarantined`` because it is the primitive that
|
|
1345
|
+
write paths, correction handling and the 4.0.10 repair use to read a row
|
|
1346
|
+
they already hold the id of — including a withheld one, which they must
|
|
1347
|
+
be able to see in order to act on it. ``visible_fact_clause`` is for the
|
|
1348
|
+
paths that answer a question; this one answers "what is in that row".
|
|
1349
|
+
|
|
1350
|
+
A caller taking a fact_id from user input and rendering the result wants
|
|
1351
|
+
``get_facts_by_ids`` instead.
|
|
1279
1352
|
"""
|
|
1280
1353
|
if profile_id is not None:
|
|
1281
1354
|
rows = self.execute(
|
|
@@ -1292,8 +1365,36 @@ class DatabaseManager:
|
|
|
1292
1365
|
self, fact_ids: list[str], profile_id: str,
|
|
1293
1366
|
include_global: bool = False,
|
|
1294
1367
|
include_shared: bool = False,
|
|
1368
|
+
*,
|
|
1369
|
+
include_quarantined: bool = False,
|
|
1295
1370
|
) -> list[AtomicFact]:
|
|
1296
|
-
"""Get multiple facts by their IDs, scoped to a profile.
|
|
1371
|
+
"""Get multiple facts by their IDs, scoped to a profile.
|
|
1372
|
+
|
|
1373
|
+
THIS IS THE PLACE QUARANTINE IS ENFORCED, and the only one.
|
|
1374
|
+
|
|
1375
|
+
Every retrieval channel re-authorises its candidates through here
|
|
1376
|
+
(``retrieval/scope_policy.py`` — "candidate generators may use caches,
|
|
1377
|
+
approximate indexes, or graph stores that are not the authorization
|
|
1378
|
+
source of truth"), and the engine hydrates the fused set from here too.
|
|
1379
|
+
A fact this method does not return has no content to show, and
|
|
1380
|
+
``retrieval/engine.py`` drops it: ``if fact is None: continue``. So one
|
|
1381
|
+
clause here covers bm25, semantic, temporal, entity, hopfield and
|
|
1382
|
+
spreading activation, in normal and deep recall alike, whether or not
|
|
1383
|
+
the forgetting filter is registered.
|
|
1384
|
+
|
|
1385
|
+
The alternatives were checked and rejected. ``_scope_where`` looks like
|
|
1386
|
+
the natural home but is spliced against ``graph_edges``,
|
|
1387
|
+
``temporal_events``, ``memories``, ``bm25_tokens``,
|
|
1388
|
+
``fact_temporal_validity`` and ``correction_cases`` as well as
|
|
1389
|
+
``atomic_facts``, so a column reference there breaks eight call sites.
|
|
1390
|
+
``ForgettingFilter`` is optional (it no-ops when forgetting is
|
|
1391
|
+
disabled) and excludes nothing in deep recall.
|
|
1392
|
+
|
|
1393
|
+
``include_quarantined=True`` is for repair, export and erasure — paths
|
|
1394
|
+
that must be able to see a withheld row in order to act on it. It is
|
|
1395
|
+
keyword-only and greppable on purpose: every caller that opts in is
|
|
1396
|
+
meant to be found in one search.
|
|
1397
|
+
"""
|
|
1297
1398
|
if not fact_ids:
|
|
1298
1399
|
return []
|
|
1299
1400
|
where, params = _scope_where(
|
|
@@ -1301,15 +1402,12 @@ class DatabaseManager:
|
|
|
1301
1402
|
include_global=include_global,
|
|
1302
1403
|
include_shared=include_shared,
|
|
1303
1404
|
)
|
|
1304
|
-
archive_clause = (
|
|
1305
|
-
" AND COALESCE(archive_status, 'live') != 'archived'"
|
|
1306
|
-
if self._has_archive_status()
|
|
1307
|
-
else ""
|
|
1308
|
-
)
|
|
1309
1405
|
placeholders = ",".join("?" for _ in fact_ids)
|
|
1310
1406
|
rows = self.execute(
|
|
1311
1407
|
f"SELECT * FROM atomic_facts WHERE fact_id IN ({placeholders}) "
|
|
1312
|
-
f"AND {where}
|
|
1408
|
+
f"AND {where}"
|
|
1409
|
+
f"{self.visible_fact_clause(include_quarantined=include_quarantined)} "
|
|
1410
|
+
"ORDER BY created_at DESC",
|
|
1313
1411
|
(*fact_ids, *params),
|
|
1314
1412
|
)
|
|
1315
1413
|
return [self._row_to_fact(r) for r in rows]
|
|
@@ -159,6 +159,9 @@ from superlocalmemory.storage.migrations import (
|
|
|
159
159
|
from superlocalmemory.storage.migrations import (
|
|
160
160
|
M042_correction_case_ledger as _M042,
|
|
161
161
|
)
|
|
162
|
+
from superlocalmemory.storage.migrations import (
|
|
163
|
+
M043_quarantine_display_summaries as _M043,
|
|
164
|
+
)
|
|
162
165
|
from superlocalmemory.storage._schema_version import (
|
|
163
166
|
SUPPORTED_SCHEMA_VERSION,
|
|
164
167
|
SchemaVersionError,
|
|
@@ -312,6 +315,14 @@ DEFERRED_MIGRATIONS: list[Migration] = [
|
|
|
312
315
|
# Main-line M034 is renumbered in V4. It must remain deferred because its
|
|
313
316
|
# backfill joins engine-bootstrapped memory_scenes and atomic_facts.
|
|
314
317
|
Migration(name=_M039.NAME, db_target="memory", ddl=_M039.DDL),
|
|
318
|
+
# M043 withholds model-written summaries from the retrieval corpus and
|
|
319
|
+
# un-hides the memories they displaced. Deferred because it reads and
|
|
320
|
+
# writes atomic_facts + fact_retention, both bootstrapped at engine init —
|
|
321
|
+
# the same reason M011/M013/M015/M016 are deferred. apply_deferred takes a
|
|
322
|
+
# verified snapshot before the first migration it actually applies, so the
|
|
323
|
+
# store is recoverable.
|
|
324
|
+
Migration(name=_M043.NAME, db_target="memory", ddl=_M043.DDL,
|
|
325
|
+
dependencies=(_M011.NAME,)),
|
|
315
326
|
]
|
|
316
327
|
|
|
317
328
|
|