superlocalmemory 3.4.18 → 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.
- package/CHANGELOG.md +35 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +3 -2
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +219 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/embeddings.py +8 -2
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/retrieval/reranker.py +4 -2
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- 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
|
+
"""
|