superlocalmemory 3.4.19 → 3.4.21

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 (170) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +42 -34
  3. package/bin/slm +11 -0
  4. package/bin/slm.bat +12 -0
  5. package/package.json +4 -3
  6. package/pyproject.toml +3 -2
  7. package/scripts/build-slm-hook.ps1 +40 -0
  8. package/scripts/build-slm-hook.sh +45 -0
  9. package/scripts/build_entry.py +452 -0
  10. package/scripts/ci/stage5b_gate.sh +50 -0
  11. package/scripts/postinstall/validation.js +187 -0
  12. package/scripts/postinstall-interactive.js +756 -0
  13. package/scripts/postinstall_binary.js +287 -0
  14. package/scripts/release_manifest.py +273 -0
  15. package/scripts/slm-hook.spec +56 -0
  16. package/skills/slm-build-graph/SKILL.md +423 -0
  17. package/skills/slm-list-recent/SKILL.md +348 -0
  18. package/skills/slm-recall/SKILL.md +343 -0
  19. package/skills/slm-remember/SKILL.md +194 -0
  20. package/skills/slm-show-patterns/SKILL.md +224 -0
  21. package/skills/slm-status/SKILL.md +363 -0
  22. package/skills/slm-switch-profile/SKILL.md +442 -0
  23. package/src/superlocalmemory/cli/commands.py +219 -79
  24. package/src/superlocalmemory/cli/context_commands.py +192 -0
  25. package/src/superlocalmemory/cli/daemon.py +15 -1
  26. package/src/superlocalmemory/cli/db_migrate.py +80 -0
  27. package/src/superlocalmemory/cli/escape_hatch.py +220 -0
  28. package/src/superlocalmemory/cli/main.py +72 -1
  29. package/src/superlocalmemory/core/context_cache.py +397 -0
  30. package/src/superlocalmemory/core/engine.py +38 -2
  31. package/src/superlocalmemory/core/engine_wiring.py +1 -1
  32. package/src/superlocalmemory/core/ram_lock.py +111 -0
  33. package/src/superlocalmemory/core/recall_pipeline.py +433 -3
  34. package/src/superlocalmemory/core/recall_worker.py +8 -3
  35. package/src/superlocalmemory/core/security_primitives.py +635 -0
  36. package/src/superlocalmemory/core/shadow_router.py +319 -0
  37. package/src/superlocalmemory/core/slm_disabled.py +87 -0
  38. package/src/superlocalmemory/core/slmignore.py +125 -0
  39. package/src/superlocalmemory/core/topic_signature.py +143 -0
  40. package/src/superlocalmemory/core/worker_pool.py +14 -3
  41. package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
  42. package/src/superlocalmemory/evolution/budget.py +321 -0
  43. package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
  44. package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
  45. package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
  46. package/src/superlocalmemory/hooks/adapter_base.py +317 -0
  47. package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
  48. package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
  49. package/src/superlocalmemory/hooks/context_payload.py +312 -0
  50. package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
  51. package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
  52. package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
  53. package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
  54. package/src/superlocalmemory/hooks/ide_connector.py +25 -2
  55. package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
  56. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
  57. package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
  58. package/src/superlocalmemory/hooks/session_registry.py +186 -0
  59. package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
  60. package/src/superlocalmemory/hooks/sync_loop.py +114 -0
  61. package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
  62. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
  63. package/src/superlocalmemory/infra/backup.py +3 -3
  64. package/src/superlocalmemory/infra/cloud_backup.py +2 -2
  65. package/src/superlocalmemory/infra/event_bus.py +2 -2
  66. package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
  67. package/src/superlocalmemory/learning/arm_catalog.py +99 -0
  68. package/src/superlocalmemory/learning/bandit.py +526 -0
  69. package/src/superlocalmemory/learning/bandit_cache.py +133 -0
  70. package/src/superlocalmemory/learning/behavioral.py +53 -1
  71. package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
  72. package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
  73. package/src/superlocalmemory/learning/database.py +256 -0
  74. package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
  75. package/src/superlocalmemory/learning/ensemble.py +300 -0
  76. package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
  77. package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
  78. package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
  79. package/src/superlocalmemory/learning/labeler.py +87 -0
  80. package/src/superlocalmemory/learning/legacy_migration.py +277 -0
  81. package/src/superlocalmemory/learning/memory_merge.py +160 -0
  82. package/src/superlocalmemory/learning/model_cache.py +269 -0
  83. package/src/superlocalmemory/learning/model_rollback.py +278 -0
  84. package/src/superlocalmemory/learning/outcome_queue.py +284 -0
  85. package/src/superlocalmemory/learning/pattern_miner.py +415 -0
  86. package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
  87. package/src/superlocalmemory/learning/ranker.py +225 -81
  88. package/src/superlocalmemory/learning/ranker_common.py +163 -0
  89. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
  90. package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
  91. package/src/superlocalmemory/learning/reward.py +777 -0
  92. package/src/superlocalmemory/learning/reward_archive.py +210 -0
  93. package/src/superlocalmemory/learning/reward_boost.py +201 -0
  94. package/src/superlocalmemory/learning/reward_proxy.py +326 -0
  95. package/src/superlocalmemory/learning/shadow_test.py +524 -0
  96. package/src/superlocalmemory/learning/signal_worker.py +270 -0
  97. package/src/superlocalmemory/learning/signals.py +314 -0
  98. package/src/superlocalmemory/learning/trigram_index.py +547 -0
  99. package/src/superlocalmemory/mcp/server.py +5 -5
  100. package/src/superlocalmemory/mcp/tools_context.py +183 -0
  101. package/src/superlocalmemory/mcp/tools_core.py +92 -27
  102. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
  103. package/src/superlocalmemory/retrieval/engine.py +52 -0
  104. package/src/superlocalmemory/server/api.py +2 -2
  105. package/src/superlocalmemory/server/bandit_loops.py +140 -0
  106. package/src/superlocalmemory/server/middleware/__init__.py +11 -0
  107. package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
  108. package/src/superlocalmemory/server/routes/backup.py +36 -13
  109. package/src/superlocalmemory/server/routes/behavioral.py +50 -19
  110. package/src/superlocalmemory/server/routes/brain.py +1234 -0
  111. package/src/superlocalmemory/server/routes/data_io.py +4 -4
  112. package/src/superlocalmemory/server/routes/events.py +2 -2
  113. package/src/superlocalmemory/server/routes/helpers.py +1 -1
  114. package/src/superlocalmemory/server/routes/learning.py +192 -7
  115. package/src/superlocalmemory/server/routes/memories.py +189 -1
  116. package/src/superlocalmemory/server/routes/prewarm.py +171 -0
  117. package/src/superlocalmemory/server/routes/profiles.py +3 -3
  118. package/src/superlocalmemory/server/routes/token.py +88 -0
  119. package/src/superlocalmemory/server/routes/ws.py +5 -5
  120. package/src/superlocalmemory/server/security_middleware.py +13 -7
  121. package/src/superlocalmemory/server/ui.py +2 -2
  122. package/src/superlocalmemory/server/unified_daemon.py +335 -3
  123. package/src/superlocalmemory/storage/migration_runner.py +545 -0
  124. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
  125. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
  126. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
  127. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
  128. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
  129. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
  130. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
  131. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
  132. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
  133. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
  134. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
  135. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
  136. package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
  137. package/src/superlocalmemory/storage/models.py +4 -0
  138. package/src/superlocalmemory/ui/css/brain.css +409 -0
  139. package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
  140. package/src/superlocalmemory/ui/index.html +459 -1345
  141. package/src/superlocalmemory/ui/js/brain.js +1321 -0
  142. package/src/superlocalmemory/ui/js/clusters.js +123 -4
  143. package/src/superlocalmemory/ui/js/init.js +48 -39
  144. package/src/superlocalmemory/ui/js/memories.js +88 -2
  145. package/src/superlocalmemory/ui/js/modal.js +71 -1
  146. package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
  147. package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
  148. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
  149. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  150. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  151. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
  152. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
  153. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
  154. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
  155. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
  156. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
  157. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  158. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  159. package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
  160. package/src/superlocalmemory/ui/js/behavioral.js +0 -447
  161. package/src/superlocalmemory/ui/js/graph-core.js +0 -447
  162. package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
  163. package/src/superlocalmemory/ui/js/learning.js +0 -435
  164. package/src/superlocalmemory/ui/js/patterns.js +0 -93
  165. package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
  166. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
  167. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  168. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  169. package/src/superlocalmemory.egg-info/requires.txt +0 -58
  170. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -0,0 +1,132 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-07 §3.2
4
+
5
+ """M002 — rebuild learning_model_state without UNIQUE(profile_id).
6
+
7
+ Enables shadow testing (old + new model live side-by-side). SQLite cannot
8
+ drop a UNIQUE constraint directly, so we use the new-table-rename pattern
9
+ wrapped in a single transaction. Existing rows are copied forward and
10
+ marked ``is_active = 1``.
11
+
12
+ SEC-02-02: ``bytes_sha256`` integrity column added. The daemon will
13
+ compute and verify this on every load to block corrupted model BLOBs from
14
+ reaching the LightGBM deserialiser.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import sqlite3
21
+
22
+ NAME = "M002_model_state_history"
23
+ DB_TARGET = "learning"
24
+
25
+ _REQUIRED_COLS = frozenset({
26
+ "model_version", "bytes_sha256", "trained_on_count",
27
+ "feature_names", "metrics_json", "is_active",
28
+ "trained_at", "updated_at",
29
+ })
30
+
31
+
32
+ def verify(conn: sqlite3.Connection) -> bool:
33
+ """Return True if the rebuilt model_state schema is in place."""
34
+ try:
35
+ cols = {r[1] for r in conn.execute(
36
+ "PRAGMA table_info(learning_model_state)"
37
+ ).fetchall()}
38
+ except sqlite3.Error:
39
+ return False
40
+ return _REQUIRED_COLS <= cols
41
+
42
+
43
+ # S9-W1 M-DATA-01: prior version of this migration hardcoded ``is_active=1``
44
+ # for every copied row. v3.4.19 mainline has ``UNIQUE(profile_id)`` which
45
+ # guarantees one row per profile so the hardcode worked — but a non-mainline
46
+ # dev-build could have multiple rows and the partial unique index created
47
+ # after rebuild would fail transactionally. We now mark only the row with
48
+ # the MAX(id) per profile as active; everything else becomes history.
49
+ DDL = """
50
+ BEGIN IMMEDIATE;
51
+
52
+ CREATE TABLE learning_model_state_new (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
54
+ profile_id TEXT NOT NULL,
55
+ model_version TEXT NOT NULL DEFAULT '3.4.21',
56
+ state_bytes BLOB NOT NULL,
57
+ bytes_sha256 TEXT NOT NULL DEFAULT '',
58
+ trained_on_count INTEGER NOT NULL DEFAULT 0,
59
+ feature_names TEXT NOT NULL DEFAULT '[]',
60
+ metrics_json TEXT NOT NULL DEFAULT '{}',
61
+ is_active INTEGER NOT NULL DEFAULT 0,
62
+ trained_at TEXT NOT NULL,
63
+ updated_at TEXT NOT NULL
64
+ );
65
+
66
+ INSERT INTO learning_model_state_new
67
+ (profile_id, state_bytes, is_active, trained_at, updated_at)
68
+ SELECT lms.profile_id, lms.state_bytes,
69
+ CASE WHEN lms.id = (
70
+ SELECT MAX(lms2.id)
71
+ FROM learning_model_state lms2
72
+ WHERE lms2.profile_id = lms.profile_id
73
+ ) THEN 1 ELSE 0 END,
74
+ lms.updated_at, lms.updated_at
75
+ FROM learning_model_state lms;
76
+
77
+ DROP TABLE learning_model_state;
78
+ ALTER TABLE learning_model_state_new RENAME TO learning_model_state;
79
+
80
+ CREATE UNIQUE INDEX idx_model_active
81
+ ON learning_model_state(profile_id)
82
+ WHERE is_active = 1;
83
+
84
+ CREATE INDEX idx_model_profile_time
85
+ ON learning_model_state(profile_id, trained_at);
86
+
87
+ COMMIT;
88
+ """
89
+
90
+
91
+ def post_ddl_hook(conn: sqlite3.Connection) -> None:
92
+ """S9-W1 H-DATA-01: backfill ``bytes_sha256`` for every row copied forward.
93
+
94
+ The DDL INSERT could not list ``bytes_sha256`` because SQLite cannot
95
+ call a Python function inside an ``executescript`` block unless the
96
+ function is registered beforehand, and registering a UDF mid-DDL is
97
+ fragile. Instead, we run one UPDATE pass after the DDL commits.
98
+
99
+ Without this backfill, ``model_cache._parse_row`` calls
100
+ ``verify_sha256(state_bytes, '')`` which raises IntegrityError, the
101
+ parser tombstones the cache entry, and EVERY 18,000+ user who had a
102
+ trained model on v3.4.19 loses usable learned-ranker state on upgrade.
103
+
104
+ The fix is safe: SHA-256 of the already-persisted blob is
105
+ deterministic, adds <1 ms per profile, and never alters
106
+ ``state_bytes``. Runs inside the same connection so any UPDATE error
107
+ surfaces to the runner as ``post_ddl_hook`` failed.
108
+ """
109
+ try:
110
+ rows = conn.execute(
111
+ "SELECT id, state_bytes FROM learning_model_state "
112
+ "WHERE bytes_sha256 = '' OR bytes_sha256 IS NULL"
113
+ ).fetchall()
114
+ except sqlite3.Error:
115
+ return # table empty or schema not yet present — nothing to do.
116
+
117
+ if not rows:
118
+ return
119
+
120
+ updates = []
121
+ for row_id, state_bytes in rows:
122
+ if state_bytes is None:
123
+ continue
124
+ sha = hashlib.sha256(state_bytes).hexdigest()
125
+ updates.append((sha, row_id))
126
+
127
+ if updates:
128
+ conn.executemany(
129
+ "UPDATE learning_model_state SET bytes_sha256 = ? WHERE id = ?",
130
+ updates,
131
+ )
132
+ conn.commit()
@@ -0,0 +1,38 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-07 §3.3
4
+
5
+ """M003 — bootstrap migration_log table.
6
+
7
+ Runs FIRST on both DBs. Purely idempotent ``CREATE TABLE IF NOT EXISTS``.
8
+ No transaction needed — a single DDL statement on SQLite is atomic.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sqlite3
14
+
15
+ NAME = "M003_migration_log"
16
+
17
+
18
+ def verify(conn: sqlite3.Connection) -> bool:
19
+ """Return True if migration_log already exists with expected columns."""
20
+ try:
21
+ cols = {r[1] for r in conn.execute(
22
+ "PRAGMA table_info(migration_log)"
23
+ ).fetchall()}
24
+ except sqlite3.Error:
25
+ return False
26
+ return {"name", "applied_at", "ddl_sha256",
27
+ "rows_affected", "status"} <= cols
28
+ DB_TARGET = "learning" # Also gets replicated to memory DB by the runner.
29
+
30
+ DDL = """
31
+ CREATE TABLE IF NOT EXISTS migration_log (
32
+ name TEXT PRIMARY KEY,
33
+ applied_at TEXT NOT NULL,
34
+ ddl_sha256 TEXT NOT NULL,
35
+ rows_affected INTEGER NOT NULL DEFAULT 0,
36
+ status TEXT NOT NULL
37
+ );
38
+ """
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-07 §3.4
4
+
5
+ """M004 — cross-platform adapter sync log (memory.db).
6
+
7
+ LLD-05 requirement. SEC-05-04: stores target paths as SHA-256 digests, not
8
+ raw strings — keeps plaintext filesystem paths out of the DB. A separate
9
+ ``target_basename`` column keeps the last path segment for the dashboard
10
+ without exposing the full path.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import sqlite3
16
+
17
+ NAME = "M004_cross_platform_sync_log"
18
+ DB_TARGET = "memory"
19
+
20
+
21
+ def verify(conn: sqlite3.Connection) -> bool:
22
+ """Return True if cross_platform_sync_log table is in place."""
23
+ try:
24
+ cols = {r[1] for r in conn.execute(
25
+ "PRAGMA table_info(cross_platform_sync_log)"
26
+ ).fetchall()}
27
+ except sqlite3.Error:
28
+ return False
29
+ return {"adapter_name", "profile_id", "target_path_sha256",
30
+ "target_basename", "last_sync_at", "bytes_written",
31
+ "content_sha256", "success"} <= cols
32
+
33
+ DDL = """
34
+ CREATE TABLE IF NOT EXISTS cross_platform_sync_log (
35
+ adapter_name TEXT NOT NULL,
36
+ profile_id TEXT NOT NULL,
37
+ target_path_sha256 TEXT NOT NULL,
38
+ target_basename TEXT NOT NULL,
39
+ last_sync_at TEXT NOT NULL,
40
+ bytes_written INTEGER NOT NULL,
41
+ content_sha256 TEXT NOT NULL,
42
+ success INTEGER NOT NULL,
43
+ error_msg TEXT,
44
+ PRIMARY KEY (adapter_name, target_path_sha256)
45
+ );
46
+ """
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-07 §3.5
4
+
5
+ """M005 — Thompson-sampling bandit tables (learning.db, LLD-03).
6
+
7
+ Two tables:
8
+ - ``bandit_arms`` — per (profile, stratum, arm) Beta-distribution state.
9
+ ``WITHOUT ROWID`` since primary key is a natural composite.
10
+ - ``bandit_plays`` — individual play events with delayed-reward settlement.
11
+
12
+ All indexes are ``IF NOT EXISTS`` so reapply is a no-op.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sqlite3
18
+
19
+ NAME = "M005_bandit_tables"
20
+ DB_TARGET = "learning"
21
+
22
+
23
+ def verify(conn: sqlite3.Connection) -> bool:
24
+ """Return True if bandit tables exist with expected columns."""
25
+ try:
26
+ arms_cols = {r[1] for r in conn.execute(
27
+ "PRAGMA table_info(bandit_arms)"
28
+ ).fetchall()}
29
+ plays_cols = {r[1] for r in conn.execute(
30
+ "PRAGMA table_info(bandit_plays)"
31
+ ).fetchall()}
32
+ except sqlite3.Error:
33
+ return False
34
+ return (
35
+ {"profile_id", "stratum", "arm_id", "alpha",
36
+ "beta", "plays"} <= arms_cols
37
+ and {"play_id", "profile_id", "query_id", "stratum",
38
+ "arm_id", "played_at"} <= plays_cols
39
+ )
40
+
41
+ DDL = """
42
+ CREATE TABLE IF NOT EXISTS bandit_arms (
43
+ profile_id TEXT NOT NULL,
44
+ stratum TEXT NOT NULL,
45
+ arm_id TEXT NOT NULL,
46
+ alpha REAL NOT NULL DEFAULT 1.0,
47
+ beta REAL NOT NULL DEFAULT 1.0,
48
+ plays INTEGER NOT NULL DEFAULT 0,
49
+ last_played_at TEXT,
50
+ PRIMARY KEY (profile_id, stratum, arm_id)
51
+ ) WITHOUT ROWID;
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_bandit_profile_strat
54
+ ON bandit_arms(profile_id, stratum);
55
+
56
+ CREATE TABLE IF NOT EXISTS bandit_plays (
57
+ play_id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ profile_id TEXT NOT NULL,
59
+ query_id TEXT NOT NULL,
60
+ stratum TEXT NOT NULL,
61
+ arm_id TEXT NOT NULL,
62
+ played_at TEXT NOT NULL,
63
+ reward REAL,
64
+ settled_at TEXT,
65
+ settlement_type TEXT
66
+ );
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_plays_query
69
+ ON bandit_plays(query_id);
70
+ CREATE INDEX IF NOT EXISTS idx_plays_unsettled
71
+ ON bandit_plays(profile_id, played_at)
72
+ WHERE settled_at IS NULL;
73
+ CREATE INDEX IF NOT EXISTS idx_plays_retention
74
+ ON bandit_plays(settled_at);
75
+ """
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-07 §3.6
4
+
5
+ """M006 — action_outcomes reward + settlement columns (memory.db).
6
+
7
+ Extends ``action_outcomes`` with the four columns the learning trainer
8
+ needs in order to swap off the position proxy onto the real reward
9
+ label:
10
+
11
+ * ``reward REAL`` — numeric reward (usually in [-1, 1]).
12
+ * ``settled INTEGER DEFAULT 0`` — 1 when the outcome has been settled.
13
+ * ``settled_at TEXT`` — ISO-8601 timestamp of settlement.
14
+ * ``recall_query_id TEXT`` — links the outcome back to the recall that
15
+ produced the candidate facts (so the trainer can join against
16
+ ``learning_signals.query_id``).
17
+
18
+ The gate in ``learning.database.fetch_training_examples``
19
+ (``_migration_applied("M006_action_outcomes_reward")``) already falls
20
+ back to the position proxy when this migration hasn't completed, so a
21
+ skipped/failed apply never crashes the trainer.
22
+
23
+ Deferred: this migration is NOT in ``MIGRATIONS``. It ships via
24
+ ``DEFERRED_MIGRATIONS`` and runs from the daemon lifespan immediately
25
+ after ``MemoryEngine.initialize()`` has bootstrapped the
26
+ ``action_outcomes`` table. Idempotent — ``verify(conn)`` returns True
27
+ once all four columns are present, so reapply is a no-op.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import sqlite3
33
+
34
+ NAME = "M006_action_outcomes_reward"
35
+ # action_outcomes lives in memory.db (see storage.schema).
36
+ DB_TARGET = "memory"
37
+
38
+ _REQUIRED_COLS = frozenset({"reward", "settled", "settled_at", "recall_query_id"})
39
+
40
+
41
+ def verify(conn: sqlite3.Connection) -> bool:
42
+ """Return True once every M006 column is present on ``action_outcomes``.
43
+
44
+ Lets the migration runner detect "already applied" state when a retry
45
+ would otherwise hit a duplicate-column error mid-script.
46
+ """
47
+ try:
48
+ cols = {
49
+ r[1]
50
+ for r in conn.execute("PRAGMA table_info(action_outcomes)").fetchall()
51
+ }
52
+ except sqlite3.Error:
53
+ return False
54
+ return _REQUIRED_COLS.issubset(cols)
55
+
56
+
57
+ # DDL. Runs inside an explicit transaction opened by the migration runner.
58
+ # SQLite doesn't support ``ALTER TABLE IF EXISTS`` — if the table is missing
59
+ # this raises sqlite3.OperationalError which the runner catches and records
60
+ # as ``failed``; the gate in ``fetch_training_examples`` keeps the trainer
61
+ # on the position proxy in that case, so the daemon is still healthy.
62
+ DDL = """
63
+ ALTER TABLE action_outcomes ADD COLUMN reward REAL;
64
+ ALTER TABLE action_outcomes ADD COLUMN settled INTEGER NOT NULL DEFAULT 0;
65
+ ALTER TABLE action_outcomes ADD COLUMN settled_at TEXT;
66
+ ALTER TABLE action_outcomes ADD COLUMN recall_query_id TEXT;
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_action_outcomes_settled_reward
69
+ ON action_outcomes(settled, settled_at)
70
+ WHERE reward IS NOT NULL;
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_action_outcomes_recall_query
73
+ ON action_outcomes(recall_query_id)
74
+ WHERE recall_query_id IS NOT NULL;
75
+ """
@@ -0,0 +1,63 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-00 §1.2 + LLD-08
4
+
5
+ """M007 — pending_outcomes table (memory.db).
6
+
7
+ LLD-00 §1.2 makes ``pending_outcomes`` the single source of truth for
8
+ in-flight recall outcomes awaiting reward settlement. An earlier draft
9
+ split pending state across a cache.db table which has been retired
10
+ (see LLD-00 §1.2 for the name of the predecessor). All pending rows
11
+ are now crash-safe, profile-scoped, and one-row-per-recall — signals
12
+ live in the ``signals_json`` blob, not separate rows.
13
+
14
+ Target DB: memory.db. Schema follows LLD-00 §1.2 verbatim. Idempotent:
15
+ ``verify(conn)`` returns True once every required column is present on
16
+ the ``pending_outcomes`` table.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import sqlite3
22
+
23
+ NAME = "M007_pending_outcomes"
24
+ DB_TARGET = "memory"
25
+
26
+ _REQUIRED_COLS = frozenset({
27
+ "outcome_id", "profile_id", "session_id", "recall_query_id",
28
+ "fact_ids_json", "query_text_hash", "created_at_ms", "expires_at_ms",
29
+ "signals_json", "status",
30
+ })
31
+
32
+
33
+ def verify(conn: sqlite3.Connection) -> bool:
34
+ try:
35
+ cols = {
36
+ r[1]
37
+ for r in conn.execute(
38
+ "PRAGMA table_info(pending_outcomes)"
39
+ ).fetchall()
40
+ }
41
+ except sqlite3.Error:
42
+ return False
43
+ return _REQUIRED_COLS.issubset(cols)
44
+
45
+
46
+ DDL = """
47
+ CREATE TABLE IF NOT EXISTS pending_outcomes (
48
+ outcome_id TEXT PRIMARY KEY,
49
+ profile_id TEXT NOT NULL,
50
+ session_id TEXT NOT NULL,
51
+ recall_query_id TEXT NOT NULL,
52
+ fact_ids_json TEXT NOT NULL,
53
+ query_text_hash TEXT NOT NULL,
54
+ created_at_ms INTEGER NOT NULL,
55
+ expires_at_ms INTEGER NOT NULL,
56
+ signals_json TEXT NOT NULL DEFAULT '{}',
57
+ status TEXT NOT NULL DEFAULT 'pending'
58
+ );
59
+ CREATE INDEX IF NOT EXISTS idx_pending_profile_expires
60
+ ON pending_outcomes(profile_id, expires_at_ms);
61
+ CREATE INDEX IF NOT EXISTS idx_pending_status
62
+ ON pending_outcomes(status, expires_at_ms);
63
+ """
@@ -0,0 +1,54 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-00 §1.3 + LLD-10
4
+
5
+ """M009 — learning_model_state lineage columns (learning.db).
6
+
7
+ LLD-10 online retrain + shadow test + auto-rollback needs to track
8
+ three models concurrently: the live active model, the previously
9
+ active (for rollback), and a candidate under A/B shadow validation.
10
+
11
+ This migration extends ``learning_model_state`` (created by M002) with
12
+ six additive columns and two partial unique indexes that enforce
13
+ single-active + single-candidate per profile. All additive — no
14
+ existing behaviour changes.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sqlite3
20
+
21
+ NAME = "M009_model_lineage"
22
+ DB_TARGET = "learning"
23
+
24
+ _REQUIRED_COLS = frozenset({
25
+ "is_previous", "is_rollback", "is_candidate",
26
+ "shadow_results_json", "promoted_at", "rollback_reason",
27
+ })
28
+
29
+
30
+ def verify(conn: sqlite3.Connection) -> bool:
31
+ try:
32
+ cols = {
33
+ r[1]
34
+ for r in conn.execute(
35
+ "PRAGMA table_info(learning_model_state)"
36
+ ).fetchall()
37
+ }
38
+ except sqlite3.Error:
39
+ return False
40
+ return _REQUIRED_COLS.issubset(cols)
41
+
42
+
43
+ DDL = """
44
+ ALTER TABLE learning_model_state ADD COLUMN is_previous INTEGER DEFAULT 0;
45
+ ALTER TABLE learning_model_state ADD COLUMN is_rollback INTEGER DEFAULT 0;
46
+ ALTER TABLE learning_model_state ADD COLUMN is_candidate INTEGER DEFAULT 0;
47
+ ALTER TABLE learning_model_state ADD COLUMN shadow_results_json TEXT;
48
+ ALTER TABLE learning_model_state ADD COLUMN promoted_at TEXT;
49
+ ALTER TABLE learning_model_state ADD COLUMN rollback_reason TEXT;
50
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_model_active_one
51
+ ON learning_model_state(profile_id) WHERE is_active=1;
52
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_model_candidate_one
53
+ ON learning_model_state(profile_id) WHERE is_candidate=1;
54
+ """
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-00 §1.6 + LLD-11
4
+
5
+ """M010 — evolution_config + evolution_llm_cost_log tables (learning.db).
6
+
7
+ LLD-11 skill evolution is opt-in by default (MASTER-PLAN D3). This
8
+ migration creates the two tables the evolution subsystem needs:
9
+
10
+ - ``evolution_config`` — per-profile feature flag + LLM backend choice.
11
+ The default row is created lazily by the installer; this migration
12
+ only creates the table so the installer's INSERT can land.
13
+ - ``evolution_llm_cost_log`` — every LLM call the evolution cycle makes
14
+ is logged here so the cost-accounting widget on the dashboard can
15
+ report tokens + USD spend.
16
+
17
+ Target DB: learning.db. Additive only.
18
+
19
+ SEC-L2 — ``cost_usd`` is IEEE-754 REAL (double). This loses precision
20
+ when summing thousands of sub-cent rows (rounding drift < 0.5 ¢ per
21
+ 10k rows in practice). The schema is additive and therefore locked
22
+ for v3.4.21 — dashboards MUST compute aggregate cost as
23
+ ``SUM(cost_usd)`` with explicit ``ROUND(x, 4)`` at display time, and
24
+ MUST NOT branch on sub-cent equality. A follow-on migration is
25
+ scheduled to switch the column to INTEGER millicents (see FINAL
26
+ board). The ``cost_usd >= 0`` non-negativity invariant is enforced
27
+ application-side by ``evolution.llm_dispatch._log_cost``.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import sqlite3
33
+
34
+ NAME = "M010_evolution_config"
35
+ DB_TARGET = "learning"
36
+
37
+ _REQUIRED_TABLES = frozenset({"evolution_config", "evolution_llm_cost_log"})
38
+
39
+
40
+ def verify(conn: sqlite3.Connection) -> bool:
41
+ try:
42
+ names = {
43
+ r[0]
44
+ for r in conn.execute(
45
+ "SELECT name FROM sqlite_master WHERE type='table'"
46
+ ).fetchall()
47
+ }
48
+ except sqlite3.Error:
49
+ return False
50
+ return _REQUIRED_TABLES.issubset(names)
51
+
52
+
53
+ DDL = """
54
+ CREATE TABLE IF NOT EXISTS evolution_config (
55
+ profile_id TEXT PRIMARY KEY,
56
+ enabled INTEGER NOT NULL DEFAULT 0,
57
+ llm_backend TEXT NOT NULL DEFAULT 'haiku',
58
+ llm_model TEXT NOT NULL DEFAULT 'claude-haiku-4-5',
59
+ last_cycle_at TEXT,
60
+ cycles_this_week INTEGER NOT NULL DEFAULT 0,
61
+ disabled_until TEXT
62
+ );
63
+ CREATE TABLE IF NOT EXISTS evolution_llm_cost_log (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ profile_id TEXT NOT NULL,
66
+ ts TEXT NOT NULL,
67
+ model TEXT NOT NULL,
68
+ tokens_in INTEGER NOT NULL DEFAULT 0,
69
+ tokens_out INTEGER NOT NULL DEFAULT 0,
70
+ cost_usd REAL NOT NULL DEFAULT 0.0,
71
+ cycle_id TEXT
72
+ );
73
+ CREATE INDEX IF NOT EXISTS idx_cost_profile_ts
74
+ ON evolution_llm_cost_log(profile_id, ts);
75
+ """
@@ -0,0 +1,87 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-00 §1.4 + LLD-12
4
+
5
+ """M011 — archive + merge log (memory.db, DEFERRED).
6
+
7
+ LLD-12 real consolidation uses hnswlib to find near-duplicate atomic
8
+ facts, then archives or merges them reversibly. This migration:
9
+
10
+ - Extends ``atomic_facts`` with four lifecycle columns:
11
+ * ``archive_status`` — 'live' | 'archived' | 'merged'
12
+ * ``archive_reason`` — tag explaining why (cosine_dup, reward_gate, ...)
13
+ * ``merged_into`` — fact_id of the canonical survivor when merged
14
+ * ``retrieval_prior`` — reward-derived boost factor used by ranker
15
+ - Creates ``memory_archive`` — payload-preserving archive table.
16
+ Consolidation NEVER deletes from atomic_facts; it only flips status
17
+ and writes a payload snapshot here.
18
+ - Creates ``memory_merge_log`` — merge decisions are reversible via
19
+ ``slm memory unmerge <merge_id>``. Records the canonical + duplicate
20
+ fact_ids plus the cosine + jaccard scores that drove the merge.
21
+
22
+ Deferred like M006 — ``atomic_facts`` is bootstrapped at engine init,
23
+ not by the migration runner. DEFERRED_MIGRATIONS runs after
24
+ ``MemoryEngine.initialize()``.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import sqlite3
30
+
31
+ NAME = "M011_archive_and_merge"
32
+ DB_TARGET = "memory"
33
+
34
+ _REQUIRED_ATOMIC = frozenset({
35
+ "archive_status", "archive_reason", "merged_into", "retrieval_prior",
36
+ })
37
+ _REQUIRED_TABLES = frozenset({"memory_archive", "memory_merge_log"})
38
+
39
+
40
+ def verify(conn: sqlite3.Connection) -> bool:
41
+ try:
42
+ atomic_cols = {
43
+ r[1]
44
+ for r in conn.execute(
45
+ "PRAGMA table_info(atomic_facts)"
46
+ ).fetchall()
47
+ }
48
+ names = {
49
+ r[0]
50
+ for r in conn.execute(
51
+ "SELECT name FROM sqlite_master WHERE type='table'"
52
+ ).fetchall()
53
+ }
54
+ except sqlite3.Error:
55
+ return False
56
+ return _REQUIRED_ATOMIC.issubset(atomic_cols) and _REQUIRED_TABLES.issubset(names)
57
+
58
+
59
+ DDL = """
60
+ ALTER TABLE atomic_facts ADD COLUMN archive_status TEXT DEFAULT 'live';
61
+ ALTER TABLE atomic_facts ADD COLUMN archive_reason TEXT;
62
+ ALTER TABLE atomic_facts ADD COLUMN merged_into TEXT;
63
+ ALTER TABLE atomic_facts ADD COLUMN retrieval_prior REAL DEFAULT 0.0;
64
+
65
+ CREATE TABLE IF NOT EXISTS memory_archive (
66
+ archive_id TEXT PRIMARY KEY,
67
+ fact_id TEXT NOT NULL,
68
+ profile_id TEXT NOT NULL,
69
+ payload_json TEXT NOT NULL,
70
+ archived_at TEXT NOT NULL,
71
+ reason TEXT NOT NULL
72
+ );
73
+ CREATE INDEX IF NOT EXISTS idx_archive_profile
74
+ ON memory_archive(profile_id, archived_at);
75
+
76
+ CREATE TABLE IF NOT EXISTS memory_merge_log (
77
+ merge_id TEXT PRIMARY KEY,
78
+ profile_id TEXT NOT NULL,
79
+ canonical_fact_id TEXT NOT NULL,
80
+ merged_fact_id TEXT NOT NULL,
81
+ cosine_sim REAL,
82
+ entity_jaccard REAL,
83
+ merged_at TEXT NOT NULL,
84
+ reversible INTEGER DEFAULT 1
85
+ );
86
+ CREATE INDEX IF NOT EXISTS idx_merge_profile ON memory_merge_log(profile_id);
87
+ """