superlocalmemory 3.8.13 → 4.0.0
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/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/* od-ops-health.js — Wave-3 Admin Operations & Health panel
|
|
2
|
+
* Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Injects an "Admin: Ops Health" tab into the Governance pane tab bar
|
|
5
|
+
* rendered by od-operations.js. Uses a MutationObserver so it fires after
|
|
6
|
+
* od-operations.js's innerHTML replacement completes. Additive only — never
|
|
7
|
+
* modifies or removes od-operations.js tabs or sections.
|
|
8
|
+
*
|
|
9
|
+
* Surfaces:
|
|
10
|
+
* - Dead-letter DLQ entries (ingestion exhausted)
|
|
11
|
+
* - Degraded completion manifests
|
|
12
|
+
* - Exhausted projection obligations
|
|
13
|
+
* - Writer stall warning
|
|
14
|
+
* - Per-entry action buttons: retry / force_reconcile / cancel
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
/* -----------------------------------------------------------------------
|
|
21
|
+
* Constants
|
|
22
|
+
* --------------------------------------------------------------------- */
|
|
23
|
+
var POLL_MS = 30000; // auto-refresh interval when tab is active
|
|
24
|
+
var TAB_KEY = 'admin-ops-health';
|
|
25
|
+
var TAB_LABEL = 'Admin: Ops Health';
|
|
26
|
+
var PANE_ID = 'od-pane-admin-ops-health';
|
|
27
|
+
var _pollTimer = null;
|
|
28
|
+
var _injected = false;
|
|
29
|
+
|
|
30
|
+
/* -----------------------------------------------------------------------
|
|
31
|
+
* DOM helpers
|
|
32
|
+
* --------------------------------------------------------------------- */
|
|
33
|
+
function $id(id) { return document.getElementById(id); }
|
|
34
|
+
|
|
35
|
+
function _esc(str) {
|
|
36
|
+
return String(str)
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/</g, '<')
|
|
39
|
+
.replace(/>/g, '>')
|
|
40
|
+
.replace(/"/g, '"');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* -----------------------------------------------------------------------
|
|
44
|
+
* Fetch helpers — never throws
|
|
45
|
+
* --------------------------------------------------------------------- */
|
|
46
|
+
function _get(path) {
|
|
47
|
+
return fetch(path, { credentials: 'same-origin' })
|
|
48
|
+
.then(function (r) {
|
|
49
|
+
return r.json().then(function (d) { return { ok: r.ok, status: r.status, data: d }; });
|
|
50
|
+
})
|
|
51
|
+
.catch(function (err) { return { ok: false, status: 0, data: null, err: String(err) }; });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _post(path, body) {
|
|
55
|
+
return fetch(path, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
credentials: 'same-origin',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
})
|
|
61
|
+
.then(function (r) {
|
|
62
|
+
return r.json().then(function (d) { return { ok: r.ok, status: r.status, data: d }; });
|
|
63
|
+
})
|
|
64
|
+
.catch(function (err) { return { ok: false, status: 0, data: null, err: String(err) }; });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* -----------------------------------------------------------------------
|
|
68
|
+
* Tab injection — called once od-operations.js has rendered #od-ops-tabs
|
|
69
|
+
* --------------------------------------------------------------------- */
|
|
70
|
+
function _injectTab() {
|
|
71
|
+
var tabBar = $id('od-ops-tabs');
|
|
72
|
+
if (!tabBar) return;
|
|
73
|
+
|
|
74
|
+
/* Don't inject twice (idempotent) */
|
|
75
|
+
if (_injected || tabBar.querySelector('[data-od-tab="' + TAB_KEY + '"]')) {
|
|
76
|
+
_injected = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Add tab button */
|
|
81
|
+
var tabBtn = document.createElement('button');
|
|
82
|
+
tabBtn.className = 'tab';
|
|
83
|
+
tabBtn.setAttribute('data-od-tab', TAB_KEY);
|
|
84
|
+
tabBtn.style.cssText = 'border-color: var(--bs-warning, #ffc107); color: var(--bs-warning, #ffc107);';
|
|
85
|
+
tabBtn.textContent = TAB_LABEL;
|
|
86
|
+
tabBar.appendChild(tabBtn);
|
|
87
|
+
|
|
88
|
+
/* Add tab pane section */
|
|
89
|
+
var pane = document.createElement('section');
|
|
90
|
+
pane.className = 'tabpane';
|
|
91
|
+
pane.setAttribute('data-od-pane', TAB_KEY);
|
|
92
|
+
pane.id = PANE_ID;
|
|
93
|
+
pane.innerHTML = _buildPaneHtml();
|
|
94
|
+
tabBar.parentNode.appendChild(pane);
|
|
95
|
+
|
|
96
|
+
/* Wire tab click into od-operations.js's existing tab-switching logic
|
|
97
|
+
* by dispatching a click on the same element od-operations uses */
|
|
98
|
+
tabBtn.addEventListener('click', function () { _onTabActivated(); });
|
|
99
|
+
|
|
100
|
+
/* Also patch od-operations.js's tab system if it uses delegated events */
|
|
101
|
+
_patchTabSystem(tabBar);
|
|
102
|
+
|
|
103
|
+
_injected = true;
|
|
104
|
+
_attachActionListeners(pane);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _buildPaneHtml() {
|
|
108
|
+
return (
|
|
109
|
+
'<div style="padding:16px">' +
|
|
110
|
+
|
|
111
|
+
/* Header */
|
|
112
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">' +
|
|
113
|
+
'<div>' +
|
|
114
|
+
'<h2 style="font-size:20px;margin-bottom:4px">Operations Health</h2>' +
|
|
115
|
+
'<p style="font-size:13px;opacity:0.7;margin:0">Stuck ingestion, failed projections, writer stalls — visible here first. OWNER or ADMIN access required.</p>' +
|
|
116
|
+
'</div>' +
|
|
117
|
+
'<button id="oh-refresh-btn" style="padding:6px 14px;border-radius:6px;border:1px solid var(--bs-secondary,#6c757d);background:transparent;color:inherit;cursor:pointer">↻ Refresh</button>' +
|
|
118
|
+
'</div>' +
|
|
119
|
+
|
|
120
|
+
/* Health banner (error state) */
|
|
121
|
+
'<div id="oh-banner-error" style="display:none;padding:10px 14px;border-radius:8px;background:rgba(220,53,69,0.15);border:1px solid rgba(220,53,69,0.4);margin-bottom:14px;font-size:13px">' +
|
|
122
|
+
'⚠ <span id="oh-banner-text">Failures detected.</span>' +
|
|
123
|
+
'<span style="opacity:0.6;margin-left:8px">Resolve below or run <code>slm ops list</code>.</span>' +
|
|
124
|
+
'</div>' +
|
|
125
|
+
|
|
126
|
+
/* Health banner (ok state) */
|
|
127
|
+
'<div id="oh-banner-ok" style="display:none;padding:10px 14px;border-radius:8px;background:rgba(25,135,84,0.15);border:1px solid rgba(25,135,84,0.4);margin-bottom:14px;font-size:13px">' +
|
|
128
|
+
'✓ All operations healthy — no failures detected.' +
|
|
129
|
+
'</div>' +
|
|
130
|
+
|
|
131
|
+
/* Writer stall warning */
|
|
132
|
+
'<div id="oh-stall-banner" style="display:none;padding:10px 14px;border-radius:8px;background:rgba(255,193,7,0.12);border:1px solid rgba(255,193,7,0.5);margin-bottom:14px;font-size:13px">' +
|
|
133
|
+
'⏳ <strong>Writer stalled</strong> — operation <code id="oh-stall-op">?</code> inflight for <span id="oh-stall-age">?</span>s. Other team members may experience slow writes.' +
|
|
134
|
+
'</div>' +
|
|
135
|
+
|
|
136
|
+
/* KPI strip */
|
|
137
|
+
'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px">' +
|
|
138
|
+
'<div style="border:1px solid var(--bs-border-color,rgba(255,255,255,0.12));border-radius:8px;padding:12px;text-align:center">' +
|
|
139
|
+
'<div id="oh-kpi-dlq" style="font-size:22px;font-weight:600">-</div>' +
|
|
140
|
+
'<div style="font-size:12px;opacity:0.6">Dead-letter</div>' +
|
|
141
|
+
'</div>' +
|
|
142
|
+
'<div style="border:1px solid var(--bs-border-color,rgba(255,255,255,0.12));border-radius:8px;padding:12px;text-align:center">' +
|
|
143
|
+
'<div id="oh-kpi-degraded" style="font-size:22px;font-weight:600">-</div>' +
|
|
144
|
+
'<div style="font-size:12px;opacity:0.6">Degraded</div>' +
|
|
145
|
+
'</div>' +
|
|
146
|
+
'<div style="border:1px solid var(--bs-border-color,rgba(255,255,255,0.12));border-radius:8px;padding:12px;text-align:center">' +
|
|
147
|
+
'<div id="oh-kpi-exhausted" style="font-size:22px;font-weight:600">-</div>' +
|
|
148
|
+
'<div style="font-size:12px;opacity:0.6">Exhausted</div>' +
|
|
149
|
+
'</div>' +
|
|
150
|
+
'</div>' +
|
|
151
|
+
|
|
152
|
+
/* Table wrap */
|
|
153
|
+
'<div id="oh-table-wrap"><p style="opacity:0.5;font-size:13px">Click Refresh or navigate to this tab to load.</p></div>' +
|
|
154
|
+
|
|
155
|
+
'</div>'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Patches od-operations.js's internal tab router so our pane hides/shows
|
|
160
|
+
* correctly when other tabs are clicked. Intercepts the tab bar's click
|
|
161
|
+
* events and extends the existing handler. */
|
|
162
|
+
function _patchTabSystem(tabBar) {
|
|
163
|
+
tabBar.addEventListener('click', function (e) {
|
|
164
|
+
var btn = e.target.closest ? e.target.closest('[data-od-tab]') : e.target;
|
|
165
|
+
if (!btn || !btn.getAttribute) return;
|
|
166
|
+
var key = btn.getAttribute('data-od-tab');
|
|
167
|
+
if (!key) return;
|
|
168
|
+
|
|
169
|
+
/* Show our pane only when our tab is clicked */
|
|
170
|
+
var ourPane = $id(PANE_ID);
|
|
171
|
+
if (!ourPane) return;
|
|
172
|
+
|
|
173
|
+
if (key === TAB_KEY) {
|
|
174
|
+
/* Deactivate all od-operations.js panes first */
|
|
175
|
+
var sibs = tabBar.parentNode.querySelectorAll('[data-od-pane]');
|
|
176
|
+
sibs.forEach(function (s) { s.classList.remove('active'); });
|
|
177
|
+
ourPane.classList.add('active');
|
|
178
|
+
_onTabActivated();
|
|
179
|
+
} else {
|
|
180
|
+
ourPane.classList.remove('active');
|
|
181
|
+
}
|
|
182
|
+
}, true /* capture — fires before od-operations.js's handler */);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* -----------------------------------------------------------------------
|
|
186
|
+
* Tab activation — load + auto-refresh
|
|
187
|
+
* --------------------------------------------------------------------- */
|
|
188
|
+
function _onTabActivated() {
|
|
189
|
+
_load();
|
|
190
|
+
_startAutoRefresh();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _startAutoRefresh() {
|
|
194
|
+
_stopAutoRefresh();
|
|
195
|
+
_pollTimer = setInterval(_load, POLL_MS);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _stopAutoRefresh() {
|
|
199
|
+
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* -----------------------------------------------------------------------
|
|
203
|
+
* Data loading
|
|
204
|
+
* --------------------------------------------------------------------- */
|
|
205
|
+
function _load() {
|
|
206
|
+
var wrap = $id('oh-table-wrap');
|
|
207
|
+
if (wrap) wrap.innerHTML = '<p style="opacity:0.5;font-size:13px">Loading…</p>';
|
|
208
|
+
|
|
209
|
+
_get('/operations/failed').then(function (res) {
|
|
210
|
+
if (res.status === 403) { _renderPermissionDenied(); return; }
|
|
211
|
+
if (!res.ok || !res.data) { _renderError(res.err || ('HTTP ' + res.status)); return; }
|
|
212
|
+
_renderData(res.data);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
_get('/status').then(function (res) {
|
|
216
|
+
if (!res.ok || !res.data) return;
|
|
217
|
+
_renderStall(res.data);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* -----------------------------------------------------------------------
|
|
222
|
+
* Rendering
|
|
223
|
+
* --------------------------------------------------------------------- */
|
|
224
|
+
function _set(id, val) { var el = $id(id); if (el) el.textContent = val; }
|
|
225
|
+
function _show(id) { var el = $id(id); if (el) el.style.display = ''; }
|
|
226
|
+
function _hide(id) { var el = $id(id); if (el) el.style.display = 'none'; }
|
|
227
|
+
|
|
228
|
+
function _renderPermissionDenied() {
|
|
229
|
+
_hide('oh-banner-error'); _hide('oh-banner-ok');
|
|
230
|
+
var w = $id('oh-table-wrap');
|
|
231
|
+
if (w) w.innerHTML = '<p style="color:#ffc107;font-size:13px">🔒 Permission denied. This panel requires <strong>OWNER</strong> or <strong>ADMIN</strong> role.</p>';
|
|
232
|
+
_set('oh-kpi-dlq', '-'); _set('oh-kpi-degraded', '-'); _set('oh-kpi-exhausted', '-');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _renderError(msg) {
|
|
236
|
+
_hide('oh-banner-error'); _hide('oh-banner-ok');
|
|
237
|
+
var w = $id('oh-table-wrap');
|
|
238
|
+
if (w) w.innerHTML = '<p style="opacity:0.5;font-size:13px">Could not load operations: ' + _esc(msg) + '</p>';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function _renderData(data) {
|
|
242
|
+
var dlq = data.dead_letter || [];
|
|
243
|
+
var degraded = data.degraded_manifests || [];
|
|
244
|
+
var exhausted = data.exhausted_obligations || [];
|
|
245
|
+
var total = (data.total !== undefined) ? data.total : (dlq.length + degraded.length + exhausted.length);
|
|
246
|
+
|
|
247
|
+
_set('oh-kpi-dlq', dlq.length);
|
|
248
|
+
_set('oh-kpi-degraded', degraded.length);
|
|
249
|
+
_set('oh-kpi-exhausted', exhausted.length);
|
|
250
|
+
|
|
251
|
+
if (total > 0) {
|
|
252
|
+
var parts = [];
|
|
253
|
+
if (dlq.length) parts.push(dlq.length + ' dead-letter');
|
|
254
|
+
if (degraded.length) parts.push(degraded.length + ' degraded');
|
|
255
|
+
if (exhausted.length) parts.push(exhausted.length + ' exhausted');
|
|
256
|
+
_set('oh-banner-text', parts.join(', ') + ' — action required.');
|
|
257
|
+
_show('oh-banner-error'); _hide('oh-banner-ok');
|
|
258
|
+
} else {
|
|
259
|
+
_hide('oh-banner-error'); _show('oh-banner-ok');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var all = dlq.concat(degraded).concat(exhausted);
|
|
263
|
+
var w = $id('oh-table-wrap');
|
|
264
|
+
if (w) {
|
|
265
|
+
w.innerHTML = _renderTable(all);
|
|
266
|
+
_attachActionListeners(w);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function _renderStall(status) {
|
|
271
|
+
if (status.writer_stalled) {
|
|
272
|
+
_set('oh-stall-op', status.writer_stalled_op_id || '?');
|
|
273
|
+
_set('oh-stall-age', status.writer_stalled_age_s != null ? Math.round(status.writer_stalled_age_s) : '?');
|
|
274
|
+
_show('oh-stall-banner');
|
|
275
|
+
} else {
|
|
276
|
+
_hide('oh-stall-banner');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function _catLabel(cat) {
|
|
281
|
+
return { dead_letter: 'Dead-letter', degraded_manifest: 'Degraded', exhausted_obligation: 'Exhausted' }[cat] || cat;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function _renderTable(entries) {
|
|
285
|
+
if (!entries || entries.length === 0) {
|
|
286
|
+
return '<p style="opacity:0.5;font-size:13px">No failed operations.</p>';
|
|
287
|
+
}
|
|
288
|
+
var style = 'width:100%;border-collapse:collapse;font-size:13px';
|
|
289
|
+
var thStyle = 'padding:8px 10px;text-align:left;opacity:0.6;border-bottom:1px solid rgba(255,255,255,0.1)';
|
|
290
|
+
var tdStyle = 'padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.06)';
|
|
291
|
+
var rows = entries.map(function (e) {
|
|
292
|
+
var opId = _esc(e.operation_id || '?');
|
|
293
|
+
var cat = _esc(_catLabel(e.category));
|
|
294
|
+
var profile = _esc(e.profile_id || '-');
|
|
295
|
+
var attempts = e.attempts !== undefined ? e.attempts : '-';
|
|
296
|
+
var err = e.error ? ('<span style="color:#f66;font-size:12px">' + _esc(e.error.slice(0, 60)) + '</span>') : '';
|
|
297
|
+
var btns =
|
|
298
|
+
'<button class="oh-action" data-op="' + opId + '" data-act="retry" style="margin-right:4px;padding:3px 8px;border-radius:4px;border:1px solid;cursor:pointer;font-size:12px">Retry</button>' +
|
|
299
|
+
'<button class="oh-action" data-op="' + opId + '" data-act="force_reconcile" style="margin-right:4px;padding:3px 8px;border-radius:4px;border:1px solid;cursor:pointer;font-size:12px">Reconcile</button>' +
|
|
300
|
+
'<button class="oh-action" data-op="' + opId + '" data-act="cancel" style="padding:3px 8px;border-radius:4px;border:1px solid rgba(220,53,69,0.5);color:#f66;cursor:pointer;font-size:12px">Cancel</button>';
|
|
301
|
+
return '<tr><td style="' + tdStyle + '"><code style="font-size:11px">' + opId + '</code></td>' +
|
|
302
|
+
'<td style="' + tdStyle + '">' + cat + '</td>' +
|
|
303
|
+
'<td style="' + tdStyle + '">' + profile + '</td>' +
|
|
304
|
+
'<td style="' + tdStyle + '">' + attempts + '</td>' +
|
|
305
|
+
'<td style="' + tdStyle + '">' + err + '</td>' +
|
|
306
|
+
'<td style="' + tdStyle + '">' + btns + '</td></tr>';
|
|
307
|
+
}).join('');
|
|
308
|
+
|
|
309
|
+
return '<table style="' + style + '"><thead><tr>' +
|
|
310
|
+
['Operation ID', 'Category', 'Profile', 'Attempts', 'Error', 'Actions'].map(function (h) {
|
|
311
|
+
return '<th style="' + thStyle + '">' + h + '</th>';
|
|
312
|
+
}).join('') +
|
|
313
|
+
'</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* -----------------------------------------------------------------------
|
|
317
|
+
* Action buttons
|
|
318
|
+
* --------------------------------------------------------------------- */
|
|
319
|
+
function _attachActionListeners(container) {
|
|
320
|
+
var btns = container.querySelectorAll('.oh-action');
|
|
321
|
+
for (var i = 0; i < btns.length; i++) {
|
|
322
|
+
(function (btn) {
|
|
323
|
+
btn.addEventListener('click', function () {
|
|
324
|
+
_resolveOp(btn.getAttribute('data-op'), btn.getAttribute('data-act'));
|
|
325
|
+
});
|
|
326
|
+
})(btns[i]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
var refreshBtn = $id('oh-refresh-btn');
|
|
330
|
+
if (refreshBtn && !refreshBtn._wired) {
|
|
331
|
+
refreshBtn._wired = true;
|
|
332
|
+
refreshBtn.addEventListener('click', _load);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _resolveOp(opId, action) {
|
|
337
|
+
if (typeof window.confirmDestructive !== 'function') {
|
|
338
|
+
_toast('error', 'Confirmation control unavailable; operation was not changed.');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
window.confirmDestructive({
|
|
342
|
+
title: 'Resolve operation',
|
|
343
|
+
target: opId,
|
|
344
|
+
consequence: 'Apply the "' + action + '" resolution to this operation.',
|
|
345
|
+
confirmLabel: 'Resolve'
|
|
346
|
+
}).then(function (confirmed) {
|
|
347
|
+
if (confirmed) _resolveOpConfirmed(opId, action);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function _resolveOpConfirmed(opId, action) {
|
|
352
|
+
_post('/operations/' + encodeURIComponent(opId) + '/resolve', { action: action })
|
|
353
|
+
.then(function (res) {
|
|
354
|
+
if (res.status === 403) { _toast('error', 'Permission denied.'); return; }
|
|
355
|
+
var d = res.data || {};
|
|
356
|
+
if (d.success) { _toast('ok', 'Resolved: ' + opId); setTimeout(_load, 800); }
|
|
357
|
+
else { _toast('error', 'Not resolved: ' + (d.reason || d.error || 'unknown')); }
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function _toast(kind, msg) {
|
|
362
|
+
var w = $id('oh-table-wrap');
|
|
363
|
+
if (!w) return;
|
|
364
|
+
var el = document.createElement('div');
|
|
365
|
+
el.style.cssText = 'padding:8px 12px;border-radius:6px;margin-bottom:8px;font-size:13px;' +
|
|
366
|
+
(kind === 'ok' ? 'background:rgba(25,135,84,0.2);' : 'background:rgba(220,53,69,0.2);');
|
|
367
|
+
el.textContent = msg;
|
|
368
|
+
w.insertBefore(el, w.firstChild);
|
|
369
|
+
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 5000);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* -----------------------------------------------------------------------
|
|
373
|
+
* Injection observer — waits for od-operations.js to render #od-ops-tabs
|
|
374
|
+
* --------------------------------------------------------------------- */
|
|
375
|
+
function _waitForTabBar() {
|
|
376
|
+
/* Fast path: already rendered */
|
|
377
|
+
if ($id('od-ops-tabs')) { _injectTab(); return; }
|
|
378
|
+
|
|
379
|
+
if (typeof MutationObserver === 'undefined') {
|
|
380
|
+
/* Fallback: polling (od-operations.js renders lazily on tab click) */
|
|
381
|
+
var t = setInterval(function () {
|
|
382
|
+
if ($id('od-ops-tabs')) { clearInterval(t); _injectTab(); }
|
|
383
|
+
}, 200);
|
|
384
|
+
/* Stop after 5 min — well past any realistic page session */
|
|
385
|
+
setTimeout(function () { clearInterval(t); }, 300000);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* Observe the whole document body so we catch od-operations.js's
|
|
390
|
+
* deferred render regardless of which element it targets. */
|
|
391
|
+
var obs = new MutationObserver(function () {
|
|
392
|
+
if ($id('od-ops-tabs') && !_injected) {
|
|
393
|
+
_injectTab();
|
|
394
|
+
if (_injected) obs.disconnect();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
398
|
+
|
|
399
|
+
/* Safety: stop observing after 5 min */
|
|
400
|
+
setTimeout(function () { obs.disconnect(); }, 300000);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* -----------------------------------------------------------------------
|
|
404
|
+
* Init
|
|
405
|
+
* --------------------------------------------------------------------- */
|
|
406
|
+
function init() {
|
|
407
|
+
_waitForTabBar();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
window.OpsHealth = { load: _load, init: init };
|
|
411
|
+
|
|
412
|
+
if (document.readyState === 'loading') {
|
|
413
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
414
|
+
} else {
|
|
415
|
+
init();
|
|
416
|
+
}
|
|
417
|
+
})();
|
|
@@ -656,12 +656,19 @@
|
|
|
656
656
|
if (!btn) return;
|
|
657
657
|
var mode = btn.getAttribute('data-mode');
|
|
658
658
|
if (mode === 'aggressive') {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
'
|
|
663
|
-
'
|
|
664
|
-
))
|
|
659
|
+
window.confirmDestructive({
|
|
660
|
+
title: 'Enable aggressive compression',
|
|
661
|
+
target: 'Compression mode',
|
|
662
|
+
consequence: 'May reduce output fidelity. Not safe for code, legal text, or math tasks.',
|
|
663
|
+
confirmLabel: 'Enable',
|
|
664
|
+
}).then(function(confirmed) {
|
|
665
|
+
if (!confirmed) return;
|
|
666
|
+
seg.querySelectorAll('button[data-mode]').forEach(function (b) {
|
|
667
|
+
b.classList.toggle('active', b === btn);
|
|
668
|
+
});
|
|
669
|
+
putConfig({ compress_mode: 'aggressive' });
|
|
670
|
+
});
|
|
671
|
+
return;
|
|
665
672
|
}
|
|
666
673
|
seg.querySelectorAll('button[data-mode]').forEach(function (b) {
|
|
667
674
|
b.classList.toggle('active', b === btn);
|
|
@@ -674,21 +681,28 @@
|
|
|
674
681
|
var clearBtn = container.querySelector('#od-opt-clear-cache');
|
|
675
682
|
if (clearBtn) {
|
|
676
683
|
clearBtn.addEventListener('click', function () {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
684
|
+
window.confirmDestructive({
|
|
685
|
+
title: 'Clear LLM cache',
|
|
686
|
+
target: 'All entries in llmcache.db',
|
|
687
|
+
consequence: 'All cached responses will be cleared.',
|
|
688
|
+
confirmLabel: 'Clear',
|
|
689
|
+
}).then(function(confirmed) {
|
|
690
|
+
if (!confirmed) return;
|
|
691
|
+
fetch('/api/optimize/cache/clear', { method: 'DELETE' })
|
|
692
|
+
.then(function (r) { return r.json(); })
|
|
693
|
+
.then(function (d) {
|
|
694
|
+
var msgEl = document.getElementById('od-opt-clear-msg');
|
|
695
|
+
if (msgEl) {
|
|
696
|
+
msgEl.textContent = 'Cleared ' + fmtNum(d.deleted || 0) + ' entries.';
|
|
697
|
+
msgEl.style.display = '';
|
|
698
|
+
setTimeout(function () {
|
|
699
|
+
msgEl.style.display = 'none';
|
|
700
|
+
loadSavings();
|
|
701
|
+
}, 3000);
|
|
702
|
+
}
|
|
703
|
+
})
|
|
704
|
+
.catch(function () {});
|
|
705
|
+
});
|
|
692
706
|
});
|
|
693
707
|
}
|
|
694
708
|
|
|
@@ -232,8 +232,15 @@
|
|
|
232
232
|
btn.addEventListener('click', function () {
|
|
233
233
|
var uid = btn.getAttribute('data-uid');
|
|
234
234
|
var uname = btn.getAttribute('data-uname');
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
window.confirmDestructive({
|
|
236
|
+
title: 'Remove user',
|
|
237
|
+
target: '"' + uname + '"',
|
|
238
|
+
consequence: 'This deletes their account and access.',
|
|
239
|
+
confirmLabel: 'Remove',
|
|
240
|
+
}).then(function(confirmed) {
|
|
241
|
+
if (!confirmed) return;
|
|
242
|
+
postJSON('/api/rbac/users/' + encodeURIComponent(uid), {}, 'DELETE').then(reload);
|
|
243
|
+
});
|
|
237
244
|
});
|
|
238
245
|
});
|
|
239
246
|
|
|
@@ -95,22 +95,19 @@
|
|
|
95
95
|
if (id === 'opt-compress-mode') {
|
|
96
96
|
var mode = e.target.value;
|
|
97
97
|
if (mode === 'aggressive') {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
e.target.value = 'safe';
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
98
|
+
confirmDestructive({
|
|
99
|
+
title: 'Enable aggressive compression',
|
|
100
|
+
target: 'Compression mode',
|
|
101
|
+
consequence: 'May reduce output fidelity. Not safe for code, legal text, or math tasks.',
|
|
102
|
+
confirmLabel: 'Enable',
|
|
103
|
+
}).then(function(confirmed) {
|
|
104
|
+
if (confirmed) {
|
|
105
|
+
_putConfig({compress_mode: 'aggressive'});
|
|
106
|
+
} else {
|
|
107
|
+
e.target.value = 'safe';
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
114
111
|
}
|
|
115
112
|
_putConfig({compress_mode: mode});
|
|
116
113
|
return;
|
|
@@ -211,9 +211,13 @@ async function deleteProfile(name) {
|
|
|
211
211
|
showToast('Cannot delete the default profile');
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
var confirmed = await confirmDestructive({
|
|
215
|
+
title: 'Delete profile',
|
|
216
|
+
target: '"' + name + '"',
|
|
217
|
+
consequence: 'Its memories will be moved to the default profile.',
|
|
218
|
+
confirmLabel: 'Delete',
|
|
219
|
+
});
|
|
220
|
+
if (!confirmed) return;
|
|
217
221
|
try {
|
|
218
222
|
var response = await fetch('/api/profiles/' + encodeURIComponent(name), {
|
|
219
223
|
method: 'DELETE'
|
|
@@ -474,7 +474,13 @@ async function connectGoogleDrive() {
|
|
|
474
474
|
}
|
|
475
475
|
|
|
476
476
|
async function disconnectDestination(destId) {
|
|
477
|
-
|
|
477
|
+
var confirmed = await confirmDestructive({
|
|
478
|
+
title: 'Disconnect backup destination',
|
|
479
|
+
target: destId,
|
|
480
|
+
consequence: 'Your backups on the cloud will remain.',
|
|
481
|
+
confirmLabel: 'Disconnect',
|
|
482
|
+
});
|
|
483
|
+
if (!confirmed) return;
|
|
478
484
|
|
|
479
485
|
try {
|
|
480
486
|
var response = await fetch('/api/backup/disconnect/' + destId, { method: 'DELETE' });
|
|
@@ -14,6 +14,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import importlib.util
|
|
17
18
|
import logging
|
|
18
19
|
import sqlite3
|
|
19
20
|
import struct
|
|
@@ -24,13 +25,18 @@ from superlocalmemory.storage.sqlite_vectors import iter_canonical_vectors
|
|
|
24
25
|
|
|
25
26
|
logger = logging.getLogger(__name__)
|
|
26
27
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
# Availability via find_spec only — never import lancedb at module load.
|
|
29
|
+
# `import lancedb` starts LanceDBBackgroundEventLoop; under Python 3.14 that
|
|
30
|
+
# background native runtime races GC and can segfault the full pytest suite.
|
|
31
|
+
# Real import is deferred to LanceDBVectorBackend construction.
|
|
32
|
+
def _lancedb_spec_present() -> bool:
|
|
33
|
+
try:
|
|
34
|
+
return importlib.util.find_spec("lancedb") is not None
|
|
35
|
+
except Exception:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_LANCEDB_AVAILABLE: bool = _lancedb_spec_present()
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
class LanceDBError(Exception):
|
|
@@ -60,10 +66,14 @@ class LanceDBVectorBackend:
|
|
|
60
66
|
DEFAULT_DIMENSION: int = 768
|
|
61
67
|
|
|
62
68
|
def __init__(self, db_path: str, dimension: int | None = None) -> None:
|
|
63
|
-
|
|
69
|
+
# Re-check at construct time so a late install still works.
|
|
70
|
+
if not _lancedb_spec_present():
|
|
64
71
|
raise LanceDBNotAvailable(
|
|
65
72
|
"LanceDB not installed. Run: pip install superlocalmemory[lancedb]"
|
|
66
73
|
)
|
|
74
|
+
# Lazy import: only construct-time starts the native runtime.
|
|
75
|
+
import lancedb # noqa: PLC0415
|
|
76
|
+
|
|
67
77
|
path = Path(db_path)
|
|
68
78
|
path.mkdir(parents=True, exist_ok=True)
|
|
69
79
|
self._db_path = str(path)
|
|
@@ -73,7 +83,7 @@ class LanceDBVectorBackend:
|
|
|
73
83
|
# wins over the requested value to keep already-materialized stores
|
|
74
84
|
# readable after a config change.
|
|
75
85
|
self._dimension = int(dimension) if dimension else self.DEFAULT_DIMENSION
|
|
76
|
-
self._db = lancedb.connect(self._db_path)
|
|
86
|
+
self._db = lancedb.connect(self._db_path)
|
|
77
87
|
self._table = self._open_or_create_table()
|
|
78
88
|
|
|
79
89
|
@property
|