superlocalmemory 3.7.6 → 3.7.7
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 +14 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin-src/manifest.json +1 -1
- package/plugin-src/requirements.txt +1 -1
- package/pyproject.toml +6 -6
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +169 -9
- package/src/superlocalmemory/cli/setup_wizard.py +18 -1
- package/src/superlocalmemory/infra/auth_middleware.py +5 -5
- package/src/superlocalmemory/mcp/_daemon_proxy.py +8 -10
- package/src/superlocalmemory/mcp/server.py +1 -0
- package/src/superlocalmemory/mcp/tools_core.py +178 -20
- package/src/superlocalmemory/optimize/cache/centroid_store.py +21 -3
- package/src/superlocalmemory/optimize/cache/manager.py +7 -0
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -10
- package/src/superlocalmemory/server/profile_runtime.py +384 -0
- package/src/superlocalmemory/server/recall_health.py +12 -6
- package/src/superlocalmemory/server/routes/helpers.py +9 -16
- package/src/superlocalmemory/server/routes/profiles.py +24 -14
- package/src/superlocalmemory/server/routes/v3_api.py +97 -20
- package/src/superlocalmemory/server/unified_daemon.py +243 -55
- package/src/superlocalmemory/storage/migration_runner.py +17 -3
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +32 -3
- package/src/superlocalmemory/ui/index.html +32 -1
- package/src/superlocalmemory/ui/js/auto-settings.js +48 -0
- package/src/superlocalmemory/ui/js/profiles.js +11 -2
|
@@ -120,6 +120,17 @@ _MODULES = {
|
|
|
120
120
|
|
|
121
121
|
logger = logging.getLogger(__name__)
|
|
122
122
|
|
|
123
|
+
# Exact historical DDL fingerprints whose resulting schema is intentionally
|
|
124
|
+
# accepted by the current migration. Unknown hashes are never reconciled.
|
|
125
|
+
_KNOWN_EQUIVALENT_DDL_HASHES: dict[str, frozenset[str]] = {
|
|
126
|
+
_M002.NAME: frozenset({
|
|
127
|
+
# v3.4.21 hardened copy-forward variant.
|
|
128
|
+
"347eeb2ec8aac89f7cbf373da49ac9446be9ed150e6105c382c656cd22426d4b",
|
|
129
|
+
# v3.4.22 model_version-default variant shipped through 3.6.x.
|
|
130
|
+
"d28666fa1dfa66e6514efd288e6748363513da2255a4cee95d80f233e6728ae7",
|
|
131
|
+
}),
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
|
|
124
135
|
@dataclass(frozen=True, slots=True)
|
|
125
136
|
class Migration:
|
|
@@ -307,11 +318,14 @@ def _apply_single(
|
|
|
307
318
|
# in place, reconcile the log to the current hash and treat as
|
|
308
319
|
# already-applied instead of failing the daemon into permanent
|
|
309
320
|
# not_ready. Absent/failing verify keeps the hard failure.
|
|
321
|
+
allowed_hashes = _KNOWN_EQUIVALENT_DDL_HASHES.get(
|
|
322
|
+
migration.name, frozenset(),
|
|
323
|
+
)
|
|
310
324
|
mod = _MODULES.get(migration.name)
|
|
311
325
|
verify_fn = (
|
|
312
326
|
getattr(mod, "verify", None) if mod is not None else None
|
|
313
327
|
)
|
|
314
|
-
if verify_fn is not None:
|
|
328
|
+
if logged_hash in allowed_hashes and verify_fn is not None:
|
|
315
329
|
try:
|
|
316
330
|
if verify_fn(conn):
|
|
317
331
|
if not dry_run:
|
|
@@ -323,8 +337,8 @@ def _apply_single(
|
|
|
323
337
|
pass
|
|
324
338
|
return (
|
|
325
339
|
"skipped",
|
|
326
|
-
"
|
|
327
|
-
"
|
|
340
|
+
"allowlisted historical DDL reconciled after "
|
|
341
|
+
"full schema verification",
|
|
328
342
|
)
|
|
329
343
|
except sqlite3.Error: # pragma: no cover
|
|
330
344
|
pass
|
|
@@ -29,14 +29,43 @@ _REQUIRED_COLS = frozenset({
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def verify(conn: sqlite3.Connection) -> bool:
|
|
32
|
-
"""
|
|
32
|
+
"""Verify columns plus both indexes promised by this migration."""
|
|
33
33
|
try:
|
|
34
|
-
cols = {r[1] for r in conn.execute(
|
|
34
|
+
cols = {r[1]: r for r in conn.execute(
|
|
35
35
|
"PRAGMA table_info(learning_model_state)"
|
|
36
36
|
).fetchall()}
|
|
37
|
+
index_rows = conn.execute(
|
|
38
|
+
"PRAGMA index_list(learning_model_state)"
|
|
39
|
+
).fetchall()
|
|
37
40
|
except sqlite3.Error:
|
|
38
41
|
return False
|
|
39
|
-
|
|
42
|
+
if not _REQUIRED_COLS <= set(cols):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
indexes = {row[1]: row for row in index_rows}
|
|
46
|
+
active = indexes.get("idx_model_active")
|
|
47
|
+
profile_time = indexes.get("idx_model_profile_time")
|
|
48
|
+
if active is None or profile_time is None:
|
|
49
|
+
return False
|
|
50
|
+
# idx_model_active must remain a UNIQUE partial index.
|
|
51
|
+
if int(active[2]) != 1 or int(active[4]) != 1:
|
|
52
|
+
return False
|
|
53
|
+
active_cols = [row[2] for row in conn.execute(
|
|
54
|
+
"PRAGMA index_info(idx_model_active)"
|
|
55
|
+
).fetchall()]
|
|
56
|
+
time_cols = [row[2] for row in conn.execute(
|
|
57
|
+
"PRAGMA index_info(idx_model_profile_time)"
|
|
58
|
+
).fetchall()]
|
|
59
|
+
if active_cols != ["profile_id"]:
|
|
60
|
+
return False
|
|
61
|
+
if time_cols != ["profile_id", "trained_at"]:
|
|
62
|
+
return False
|
|
63
|
+
sql_row = conn.execute(
|
|
64
|
+
"SELECT sql FROM sqlite_master WHERE type='index' AND name=?",
|
|
65
|
+
("idx_model_active",),
|
|
66
|
+
).fetchone()
|
|
67
|
+
normalized = " ".join(str(sql_row[0] if sql_row else "").lower().split())
|
|
68
|
+
return "where is_active = 1" in normalized
|
|
40
69
|
|
|
41
70
|
|
|
42
71
|
# IMPORTANT: this DDL shipped in V3.4.21. Migration hashes are immutable
|
|
@@ -1058,11 +1058,42 @@
|
|
|
1058
1058
|
</button>
|
|
1059
1059
|
<span id="settings-emb-test-result" class="ms-2 small"></span>
|
|
1060
1060
|
</div>
|
|
1061
|
-
|
|
1061
|
+
<div id="settings-emb-info" class="small text-muted mt-1">
|
|
1062
1062
|
Using local <strong>nomic-embed-text-v1.5</strong> (768d)
|
|
1063
1063
|
</div>
|
|
1064
1064
|
</div>
|
|
1065
1065
|
|
|
1066
|
+
<!-- Step 4: Scope defaults (shared/global remain opt-in) -->
|
|
1067
|
+
<div class="mt-3 pt-3 border-top" id="settings-scope-panel">
|
|
1068
|
+
<h6 class="text-muted"><i class="bi bi-shield-lock"></i> Step 4: Memory Visibility</h6>
|
|
1069
|
+
<p class="small text-muted mb-2">
|
|
1070
|
+
Personal-only is the privacy-safe default. Enable broader visibility deliberately.
|
|
1071
|
+
</p>
|
|
1072
|
+
<div class="row g-2 align-items-end">
|
|
1073
|
+
<div class="col-md-4">
|
|
1074
|
+
<label class="form-label small" for="settings-default-scope">Default write scope</label>
|
|
1075
|
+
<select class="form-select form-select-sm" id="settings-default-scope">
|
|
1076
|
+
<option value="personal">Personal (recommended)</option>
|
|
1077
|
+
<option value="shared">Shared</option>
|
|
1078
|
+
<option value="global">Global</option>
|
|
1079
|
+
</select>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div class="col-md-4">
|
|
1082
|
+
<div class="form-check form-switch">
|
|
1083
|
+
<input class="form-check-input" type="checkbox" id="settings-recall-shared">
|
|
1084
|
+
<label class="form-check-label small" for="settings-recall-shared">Include shared memories by default</label>
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
<div class="col-md-4">
|
|
1088
|
+
<div class="form-check form-switch">
|
|
1089
|
+
<input class="form-check-input" type="checkbox" id="settings-recall-global">
|
|
1090
|
+
<label class="form-check-label small" for="settings-recall-global">Include global memories by default</label>
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
<div id="settings-scope-status" class="small text-muted mt-2"></div>
|
|
1095
|
+
</div>
|
|
1096
|
+
|
|
1066
1097
|
<!-- Save button -->
|
|
1067
1098
|
<div class="mt-3">
|
|
1068
1099
|
<button class="btn btn-primary" id="settings-save-all">
|
|
@@ -27,6 +27,50 @@ async function loadAutoSettings() {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
async function loadScopeSettings() {
|
|
31
|
+
try {
|
|
32
|
+
var response = await fetch('/api/v3/scope/config');
|
|
33
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
34
|
+
var data = await response.json();
|
|
35
|
+
var defaultScope = document.getElementById('settings-default-scope');
|
|
36
|
+
var shared = document.getElementById('settings-recall-shared');
|
|
37
|
+
var globalScope = document.getElementById('settings-recall-global');
|
|
38
|
+
if (defaultScope) defaultScope.value = data.default_scope || 'personal';
|
|
39
|
+
if (shared) shared.checked = data.recall_include_shared === true;
|
|
40
|
+
if (globalScope) globalScope.checked = data.recall_include_global === true;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
var status = document.getElementById('settings-scope-status');
|
|
43
|
+
if (status) status.textContent = 'Could not load runtime visibility settings.';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function saveScopeSettings() {
|
|
48
|
+
var status = document.getElementById('settings-scope-status');
|
|
49
|
+
var payload = {
|
|
50
|
+
default_scope: document.getElementById('settings-default-scope')?.value || 'personal',
|
|
51
|
+
recall_include_shared: document.getElementById('settings-recall-shared')?.checked === true,
|
|
52
|
+
recall_include_global: document.getElementById('settings-recall-global')?.checked === true,
|
|
53
|
+
};
|
|
54
|
+
if (status) status.textContent = 'Applying to daemon...';
|
|
55
|
+
try {
|
|
56
|
+
var response = await fetch('/api/v3/scope/config', {
|
|
57
|
+
method: 'PUT',
|
|
58
|
+
headers: {'Content-Type': 'application/json'},
|
|
59
|
+
body: JSON.stringify(payload),
|
|
60
|
+
});
|
|
61
|
+
var data = await response.json();
|
|
62
|
+
if (!response.ok || data.success !== true) {
|
|
63
|
+
throw new Error(data.error || 'daemon rejected visibility settings');
|
|
64
|
+
}
|
|
65
|
+
if (status) status.textContent = 'Applied to the resident daemon.';
|
|
66
|
+
return true;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (status) status.textContent = 'Not applied: ' + error.message;
|
|
69
|
+
await loadScopeSettings();
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
30
74
|
function saveAutoCaptureConfig() {
|
|
31
75
|
var payload = {
|
|
32
76
|
enabled: document.getElementById('auto-capture-toggle')?.checked,
|
|
@@ -533,6 +577,9 @@ document.getElementById('settings-save-all')?.addEventListener('click', saveAllS
|
|
|
533
577
|
document.getElementById('settings-test-btn')?.addEventListener('click', testConnection);
|
|
534
578
|
document.getElementById('settings-emb-provider')?.addEventListener('change', updateEmbeddingUI);
|
|
535
579
|
document.getElementById('settings-emb-test-btn')?.addEventListener('click', testEmbeddingEndpoint);
|
|
580
|
+
['settings-default-scope', 'settings-recall-shared', 'settings-recall-global'].forEach(function(id) {
|
|
581
|
+
document.getElementById(id)?.addEventListener('change', saveScopeSettings);
|
|
582
|
+
});
|
|
536
583
|
|
|
537
584
|
// Mode radio buttons
|
|
538
585
|
document.querySelectorAll('input[name="settings-mode-radio"]').forEach(function(radio) {
|
|
@@ -542,6 +589,7 @@ document.querySelectorAll('input[name="settings-mode-radio"]').forEach(function(
|
|
|
542
589
|
// Load settings when the settings tab is shown
|
|
543
590
|
document.getElementById('settings-tab')?.addEventListener('shown.bs.tab', function() {
|
|
544
591
|
loadAutoSettings();
|
|
592
|
+
loadScopeSettings();
|
|
545
593
|
loadModeSettings();
|
|
546
594
|
loadEmbeddingSettings();
|
|
547
595
|
updateModeUI();
|
|
@@ -206,7 +206,9 @@ async function switchProfile(profileName) {
|
|
|
206
206
|
method: 'POST'
|
|
207
207
|
});
|
|
208
208
|
var data = await response.json();
|
|
209
|
-
|
|
209
|
+
var acknowledged = response.ok && data.success === true &&
|
|
210
|
+
data.active_profile === profileName && Number.isInteger(data.generation);
|
|
211
|
+
if (acknowledged) {
|
|
210
212
|
showToast('Switched to profile: ' + profileName);
|
|
211
213
|
loadProfiles();
|
|
212
214
|
loadStats();
|
|
@@ -226,11 +228,18 @@ async function switchProfile(profileName) {
|
|
|
226
228
|
if (typeof loadCompliance === 'function') loadCompliance();
|
|
227
229
|
var activeTab = document.querySelector('#mainTabs .nav-link.active');
|
|
228
230
|
if (activeTab) activeTab.click();
|
|
231
|
+
return true;
|
|
229
232
|
} else {
|
|
230
|
-
showToast('
|
|
233
|
+
showToast(data.detail || 'Daemon did not acknowledge the requested profile');
|
|
234
|
+
// Restore the selector from daemon runtime truth after any failure
|
|
235
|
+
// or mismatched acknowledgement.
|
|
236
|
+
loadProfiles();
|
|
237
|
+
return false;
|
|
231
238
|
}
|
|
232
239
|
} catch (error) {
|
|
233
240
|
console.error('Error switching profile:', error);
|
|
234
241
|
showToast('Error switching profile');
|
|
242
|
+
loadProfiles();
|
|
243
|
+
return false;
|
|
235
244
|
}
|
|
236
245
|
}
|