superlocalmemory 4.0.10 → 4.1.2

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 (145) hide show
  1. package/.claude-plugin/marketplace.json +12 -2
  2. package/CHANGELOG.md +244 -0
  3. package/README.md +40 -75
  4. package/package.json +6 -3
  5. package/plugin/.claude-plugin/plugin.json +2 -2
  6. package/plugin/CLAUDE.md +3 -3
  7. package/plugin/agents/slm-governance-advisor.md +1 -1
  8. package/plugin/agents/slm-loop-runner.md +4 -4
  9. package/plugin/agents/slm-memory-advisor.md +1 -1
  10. package/plugin/agents/slm-optimize-advisor.md +1 -1
  11. package/plugin/requirements.txt +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +2 -2
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +5 -5
  19. package/plugin/skills/slm-recall/SKILL.md +102 -15
  20. package/plugin/skills/slm-remember/SKILL.md +35 -3
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +29 -3
  23. package/plugin/skills/slm-status/SKILL.md +1 -1
  24. package/plugin-src/rules/AGENTS.md +16 -8
  25. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-loop/SKILL.md +2 -2
  30. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-profile/SKILL.md +5 -5
  32. package/plugin-src/skills/slm-recall/SKILL.md +102 -15
  33. package/plugin-src/skills/slm-remember/SKILL.md +35 -3
  34. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +29 -3
  36. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  37. package/pyproject.toml +1 -1
  38. package/src/superlocalmemory/__init__.py +1 -1
  39. package/src/superlocalmemory/cli/commands.py +357 -18
  40. package/src/superlocalmemory/cli/daemon.py +30 -0
  41. package/src/superlocalmemory/cli/db_migrate.py +71 -1
  42. package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
  43. package/src/superlocalmemory/cli/main.py +24 -2
  44. package/src/superlocalmemory/code_graph/database.py +44 -0
  45. package/src/superlocalmemory/compliance/gdpr.py +449 -39
  46. package/src/superlocalmemory/core/admission.py +231 -11
  47. package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
  48. package/src/superlocalmemory/core/config.py +90 -11
  49. package/src/superlocalmemory/core/consolidation_engine.py +34 -0
  50. package/src/superlocalmemory/core/engine.py +140 -11
  51. package/src/superlocalmemory/core/graph_analyzer.py +76 -112
  52. package/src/superlocalmemory/core/graph_metrics.py +597 -0
  53. package/src/superlocalmemory/core/graph_pruner.py +121 -0
  54. package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
  55. package/src/superlocalmemory/core/mode_capability.py +111 -0
  56. package/src/superlocalmemory/core/ollama_validator.py +315 -0
  57. package/src/superlocalmemory/core/projection_drain.py +380 -0
  58. package/src/superlocalmemory/core/recall_pipeline.py +390 -3
  59. package/src/superlocalmemory/core/recall_worker.py +6 -3
  60. package/src/superlocalmemory/core/scale_autopromote.py +196 -0
  61. package/src/superlocalmemory/core/scale_engine.py +16 -2
  62. package/src/superlocalmemory/core/score_contract.py +21 -1
  63. package/src/superlocalmemory/core/session_identity.py +85 -0
  64. package/src/superlocalmemory/core/status_contract.py +108 -0
  65. package/src/superlocalmemory/core/worker_pool.py +4 -4
  66. package/src/superlocalmemory/core/working_memory.py +288 -0
  67. package/src/superlocalmemory/encoding/cognitive_consolidator.py +36 -6
  68. package/src/superlocalmemory/encoding/context_generator.py +1 -1
  69. package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
  70. package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
  71. package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
  72. package/src/superlocalmemory/encoding/type_router.py +12 -12
  73. package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
  74. package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
  75. package/src/superlocalmemory/graph/cozo_backend.py +103 -138
  76. package/src/superlocalmemory/hooks/portable_kit.py +10 -2
  77. package/src/superlocalmemory/learning/bandit.py +43 -0
  78. package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
  79. package/src/superlocalmemory/learning/database.py +60 -3
  80. package/src/superlocalmemory/learning/entity_compiler.py +21 -58
  81. package/src/superlocalmemory/learning/feedback.py +3 -1
  82. package/src/superlocalmemory/learning/outcomes.py +47 -16
  83. package/src/superlocalmemory/learning/pattern_miner.py +28 -3
  84. package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
  85. package/src/superlocalmemory/learning/pcos.py +291 -0
  86. package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
  87. package/src/superlocalmemory/learning/reward_proxy.py +100 -10
  88. package/src/superlocalmemory/learning/signal_kinds.py +79 -0
  89. package/src/superlocalmemory/mcp/profiles.py +14 -2
  90. package/src/superlocalmemory/mcp/tools_active.py +2 -1
  91. package/src/superlocalmemory/mcp/tools_core.py +31 -3
  92. package/src/superlocalmemory/mcp/tools_v28.py +20 -1
  93. package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
  94. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
  95. package/src/superlocalmemory/retrieval/bm25_channel.py +64 -3
  96. package/src/superlocalmemory/retrieval/channel_status.py +117 -0
  97. package/src/superlocalmemory/retrieval/engine.py +106 -11
  98. package/src/superlocalmemory/retrieval/entity_channel.py +210 -256
  99. package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
  100. package/src/superlocalmemory/retrieval/scope_policy.py +20 -0
  101. package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
  102. package/src/superlocalmemory/retrieval/spreading.py +288 -0
  103. package/src/superlocalmemory/server/api.py +24 -5
  104. package/src/superlocalmemory/server/bandit_loops.py +17 -1
  105. package/src/superlocalmemory/server/rbac_enforce.py +26 -6
  106. package/src/superlocalmemory/server/recall_health.py +87 -10
  107. package/src/superlocalmemory/server/recall_serializer.py +9 -0
  108. package/src/superlocalmemory/server/routes/behavioral.py +75 -10
  109. package/src/superlocalmemory/server/routes/compliance.py +98 -18
  110. package/src/superlocalmemory/server/routes/config_api.py +186 -4
  111. package/src/superlocalmemory/server/routes/evolution.py +178 -0
  112. package/src/superlocalmemory/server/routes/ingest.py +8 -0
  113. package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
  114. package/src/superlocalmemory/server/routes/memories.py +49 -7
  115. package/src/superlocalmemory/server/routes/timeline.py +4 -0
  116. package/src/superlocalmemory/server/routes/v3_api.py +191 -15
  117. package/src/superlocalmemory/server/ui.py +20 -4
  118. package/src/superlocalmemory/server/unified_daemon.py +241 -7
  119. package/src/superlocalmemory/storage/_migration_internals.py +54 -2
  120. package/src/superlocalmemory/storage/_schema_version.py +24 -3
  121. package/src/superlocalmemory/storage/database.py +477 -59
  122. package/src/superlocalmemory/storage/embedding_codec.py +71 -0
  123. package/src/superlocalmemory/storage/lineage_retention.py +236 -0
  124. package/src/superlocalmemory/storage/logical_edges.py +43 -2
  125. package/src/superlocalmemory/storage/migration_runner.py +119 -0
  126. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +60 -36
  127. package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
  128. package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
  129. package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
  130. package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
  131. package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
  132. package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
  133. package/src/superlocalmemory/storage/migrations.py +18 -2
  134. package/src/superlocalmemory/storage/models.py +40 -1
  135. package/src/superlocalmemory/storage/projection_outbox.py +346 -0
  136. package/src/superlocalmemory/storage/retention_policy.py +860 -0
  137. package/src/superlocalmemory/storage/schema.py +35 -1
  138. package/src/superlocalmemory/storage/write_coordinator.py +19 -2
  139. package/src/superlocalmemory/trust/scorer.py +43 -1
  140. package/src/superlocalmemory/ui/index.html +9 -18
  141. package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
  142. package/src/superlocalmemory/ui/js/od-health.js +28 -6
  143. package/src/superlocalmemory/ui/js/od-memories.js +19 -0
  144. package/src/superlocalmemory/ui/js/od-settings.js +87 -1
  145. package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
@@ -0,0 +1,127 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """M044 — a bandit play records which memories it actually showed.
5
+
6
+ WHY
7
+ ---
8
+ A play is settled later, from evidence: did anything downstream reference one of
9
+ the memories this query surfaced? ``reward_proxy`` answered "which memories" by
10
+ reading ``learning_signals`` for the same ``query_id`` — rows written by the
11
+ recall path's ``enqueue(SignalBatch(...))``.
12
+
13
+ That enqueue is off, deliberately. It writes twenty exposure rows per query, it
14
+ was the source of a 2,675x inflation in the ranking-phase counter, and the code
15
+ comment gives a second reason: *"even a non-blocking enqueue eventually writes
16
+ canonical/learning state and turns dashboard polling into contention."*
17
+
18
+ So the evidence lookup had no evidence to read, and every settlement fell
19
+ through to the 120-second default of 0.5. Measured on a live production store::
20
+
21
+ SUM(alpha) = SUM(beta) = 867.5 = 165 priors + 1,405 plays x 0.5
22
+
23
+ An exact match on both sides across 165 arms: every one of 1,405 settlements
24
+ applied the neutral prior. Thompson sampling on Beta(a, a) is a coin flip, so
25
+ retrieval strategy was being chosen at random.
26
+
27
+ The fix is for the play to carry its own evidence. One column, written once per
28
+ recorded play, holding the handful of fact_ids that were actually shown — rather
29
+ than reinstating twenty rows per query in a table that gates a phase counter.
30
+
31
+ WHY A COLUMN AND NOT A TABLE
32
+ ----------------------------
33
+ This is one small JSON array per play, read exactly once by the settler and
34
+ then dead. A child table would need its own index, its own retention sweep, and
35
+ its own profile_id for erasure. The column is deleted with its play by the
36
+ existing ``retention_sweep``, which is the behaviour we want and get for free.
37
+
38
+ BACK-COMPATIBILITY
39
+ ------------------
40
+ Existing rows keep NULL. The settler falls back to the ``learning_signals``
41
+ lookup when the column is empty, so plays recorded before this migration settle
42
+ exactly as they did before — and installs that still run the enqueue keep
43
+ working unchanged.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import sqlite3
49
+
50
+ NAME = "M044_play_carries_its_own_evidence"
51
+ DB_TARGET = "learning"
52
+
53
+ _TABLE = "bandit_plays"
54
+ _COLUMN = "shown_fact_ids"
55
+
56
+ #: Recorded for the runner's DDL hash. SQLite has no ``ADD COLUMN IF NOT
57
+ #: EXISTS``, so ``apply()`` below runs instead of this string; it is kept
58
+ #: accurate because the hash is what detects a migration being edited after it
59
+ #: has shipped.
60
+ DDL = """
61
+ ALTER TABLE bandit_plays ADD COLUMN shown_fact_ids TEXT;
62
+ """
63
+
64
+
65
+ def _columns(conn: sqlite3.Connection) -> set[str]:
66
+ try:
67
+ return {
68
+ row[1] for row in conn.execute(f"PRAGMA table_info({_TABLE})")
69
+ }
70
+ except sqlite3.Error:
71
+ return set()
72
+
73
+
74
+ def _table_exists(conn: sqlite3.Connection) -> bool:
75
+ try:
76
+ return conn.execute(
77
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
78
+ (_TABLE,),
79
+ ).fetchone() is not None
80
+ except sqlite3.Error:
81
+ return False
82
+
83
+
84
+ def verify(conn: sqlite3.Connection) -> bool:
85
+ """End state: the column exists.
86
+
87
+ Everything ``verify`` asserts must be produced by every path through
88
+ ``apply`` — including the path where the table is absent. 4.0.10 shipped an
89
+ M043 whose ``verify`` required a table its ``apply`` created only
90
+ conditionally, which made the migration fail permanently on stores that
91
+ lacked it. Hence the branch below: no table means M005 has not run, this
92
+ migration has nothing to do, and saying so is the end state.
93
+ """
94
+ if not _table_exists(conn):
95
+ return True
96
+ return _COLUMN in _columns(conn)
97
+
98
+
99
+ def apply(conn: sqlite3.Connection) -> None:
100
+ """Add the column if it is missing. Idempotent.
101
+
102
+ Runs instead of ``DDL`` (the runner prefers a module-level ``apply``)
103
+ because ``ALTER TABLE ... ADD COLUMN`` raises when the column is already
104
+ there, and SQLite cannot guard that inside one script.
105
+ """
106
+ if not _table_exists(conn):
107
+ # M005 owns bandit_plays and is a declared dependency, so this means a
108
+ # store where the bandit tables were never created. Nothing to alter.
109
+ return
110
+ if _COLUMN in _columns(conn):
111
+ return
112
+ conn.execute(f"ALTER TABLE {_TABLE} ADD COLUMN {_COLUMN} TEXT")
113
+ if not verify(conn): # pragma: no cover — defensive
114
+ raise sqlite3.OperationalError(
115
+ f"M044: {_TABLE}.{_COLUMN} absent after ALTER"
116
+ )
117
+
118
+
119
+ def repair(conn: sqlite3.Connection) -> None:
120
+ """Identical to ``apply`` — and that is safe only because it is additive.
121
+
122
+ ``repair`` == ``apply`` is a trap in general: if ``verify`` can return a
123
+ false negative, the runner retries forever. Here ``verify`` reads
124
+ ``PRAGMA table_info``, which is the same fact ``apply`` establishes, so a
125
+ false negative would require the ALTER to have silently not happened.
126
+ """
127
+ apply(conn)
@@ -0,0 +1,158 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """M045 — per-fact outcome score (PCOS), so ranking can use what worked.
5
+
6
+ WHY
7
+ ---
8
+ Ranking scores a memory by how much it *looks like* the query. Nothing in the
9
+ pipeline knows whether a memory has ever actually helped. This table holds one
10
+ number per (fact, profile): an exponentially-weighted average of the rewards of
11
+ the settlements it took part in.
12
+
13
+ WHY IT LIVES IN memory.db
14
+ -------------------------
15
+ Beside ``atomic_facts`` and ``action_outcomes``, the backfill source, so the
16
+ whole thing stays in one file. SQLite cannot join across databases without
17
+ ATTACH, and taking an ATTACH on the recall path to reach a single REAL would
18
+ cost more than the number is worth.
19
+
20
+ The ranker reads it with one batched keyed lookup rather than a LEFT JOIN into
21
+ fact hydration: ``_row_to_fact`` builds ``AtomicFact`` from named columns and
22
+ silently drops any it does not declare, so a JOIN would mean adding fields to a
23
+ model used across the whole codebase to carry a value only the ranker reads.
24
+
25
+ profile_id IS PART OF THE PRIMARY KEY, AND THAT IS NOT COSMETIC
26
+ ---------------------------------------------------------------
27
+ A learned per-fact score is derived personal data. If it had no ``profile_id``,
28
+ ``forget_profile()`` would run, report success, and leave the erased user's
29
+ learned outcome history behind — which is the same defect already
30
+ known in ``fact_expansion_fts``. Creating a second table with the same defect
31
+ in the same release would be indefensible. ``compliance/gdpr.py`` deletes from
32
+ this table; a test asserts one profile's erasure leaves another's rows intact.
33
+
34
+ THE BACKFILL, AND A CORRECTION TO THE PLAN
35
+ ------------------------------------------
36
+ An earlier design's backfill read ``SELECT fact_id, profile_id, AVG(reward) FROM
37
+ action_outcomes GROUP BY fact_id, profile_id``. **There is no ``fact_id`` column
38
+ on that table** — it stores ``fact_ids_json``, a JSON array, because one outcome
39
+ covers the set of memories an answer used. So the backfill expands the array with
40
+ ``json_each``, guarded by ``json_valid`` (an unguarded ``json_each`` raises on the
41
+ first malformed row and takes the whole migration with it).
42
+
43
+ On a live store this initialises every touched fact at 0.5 from 162 rows
44
+ that are all 0.5 — which is the neutral prior, so the backfill adds no bias. Its
45
+ only job is to avoid a cold start looking like a penalty.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import sqlite3
51
+
52
+ NAME = "M045_fact_outcome_score"
53
+ DB_TARGET = "memory"
54
+
55
+ _TABLE = "fact_outcome_score"
56
+
57
+ DDL = """
58
+ BEGIN IMMEDIATE;
59
+ CREATE TABLE IF NOT EXISTS fact_outcome_score (
60
+ fact_id TEXT NOT NULL,
61
+ profile_id TEXT NOT NULL DEFAULT 'default',
62
+ score REAL NOT NULL DEFAULT 0.5,
63
+ play_count INTEGER NOT NULL DEFAULT 0,
64
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
65
+ PRIMARY KEY (fact_id, profile_id)
66
+ );
67
+ CREATE INDEX IF NOT EXISTS idx_fos_profile
68
+ ON fact_outcome_score (profile_id, fact_id);
69
+ COMMIT;
70
+ """
71
+
72
+ #: Expand ``fact_ids_json`` and average the reward per (fact, profile).
73
+ #:
74
+ #: ``INSERT OR IGNORE`` names no conflict target on purpose: the table has one
75
+ #: unique constraint today, and a targeted ``ON CONFLICT`` silently stops
76
+ #: covering a row the moment a second constraint is added.
77
+ _BACKFILL = """
78
+ INSERT OR IGNORE INTO fact_outcome_score
79
+ (fact_id, profile_id, score, play_count, updated_at)
80
+ SELECT
81
+ j.value AS fact_id,
82
+ o.profile_id,
83
+ AVG(o.reward),
84
+ COUNT(*),
85
+ datetime('now')
86
+ FROM action_outcomes AS o, json_each(o.fact_ids_json) AS j
87
+ WHERE o.reward IS NOT NULL
88
+ AND json_valid(o.fact_ids_json)
89
+ AND o.fact_ids_json IS NOT NULL
90
+ AND j.value IS NOT NULL AND j.value != ''
91
+ GROUP BY j.value, o.profile_id
92
+ """
93
+
94
+
95
+ def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
96
+ try:
97
+ return conn.execute(
98
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
99
+ (name,),
100
+ ).fetchone() is not None
101
+ except sqlite3.Error:
102
+ return False
103
+
104
+
105
+ def _columns(conn: sqlite3.Connection, table: str) -> set[str]:
106
+ try:
107
+ return {row[1] for row in conn.execute(f"PRAGMA table_info({table})")}
108
+ except sqlite3.Error:
109
+ return set()
110
+
111
+
112
+ def verify(conn: sqlite3.Connection) -> bool:
113
+ """End state: the table exists with profile_id in it.
114
+
115
+ Deliberately does NOT assert anything about backfilled rows. The backfill
116
+ depends on ``action_outcomes``, which is bootstrapped at engine init and may
117
+ hold nothing at all — so asserting rows would make ``verify`` fail forever
118
+ on a fresh install. Everything asserted here is produced by every path
119
+ through ``apply``; that is the M043 lesson, learned the hard way.
120
+ """
121
+ if not _table_exists(conn, _TABLE):
122
+ return False
123
+ cols = _columns(conn, _TABLE)
124
+ return {"fact_id", "profile_id", "score", "play_count"} <= cols
125
+
126
+
127
+ def apply(conn: sqlite3.Connection) -> None:
128
+ """Create the table, then backfill from reported outcomes if any exist."""
129
+ conn.executescript(DDL)
130
+ if not verify(conn): # pragma: no cover — defensive
131
+ raise sqlite3.OperationalError(
132
+ "M045 fact_outcome_score did not reach its end-state"
133
+ )
134
+ _backfill(conn)
135
+
136
+
137
+ def _backfill(conn: sqlite3.Connection) -> int:
138
+ """Seed from ``action_outcomes``. Never fatal — the table is what matters.
139
+
140
+ A failure here means facts start at the cold-start default of 0.5, which is
141
+ the same value the backfill would have written on this store anyway. Losing
142
+ the migration over that would be the wrong trade.
143
+ """
144
+ if not _table_exists(conn, "action_outcomes"):
145
+ return 0
146
+ if "reward" not in _columns(conn, "action_outcomes"):
147
+ # reward arrives with M006, which is deferred. Nothing to average yet.
148
+ return 0
149
+ try:
150
+ cur = conn.execute(_BACKFILL)
151
+ return int(cur.rowcount or 0)
152
+ except sqlite3.Error:
153
+ return 0
154
+
155
+
156
+ def repair(conn: sqlite3.Connection) -> None:
157
+ """Re-run apply. Safe: the DDL is IF NOT EXISTS and the backfill IGNOREs."""
158
+ apply(conn)