superlocalmemory 3.8.1 → 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +2 -2
- package/package.json +1 -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 +2 -2
- 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 +3 -5
- 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 +2 -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-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +3 -5
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/scripts/postinstall.js +7 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +360 -2
- package/src/superlocalmemory/cli/main.py +62 -3
- package/src/superlocalmemory/cli/setup_wizard.py +142 -16
- package/src/superlocalmemory/core/component_healer.py +144 -0
- package/src/superlocalmemory/core/component_registry.py +487 -0
- package/src/superlocalmemory/core/config.py +21 -0
- package/src/superlocalmemory/core/embeddings.py +14 -1
- package/src/superlocalmemory/core/engine.py +9 -5
- package/src/superlocalmemory/core/ingestion_command.py +36 -16
- package/src/superlocalmemory/core/maintenance.py +43 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +28 -0
- package/src/superlocalmemory/core/recall_pipeline.py +39 -3
- package/src/superlocalmemory/core/store_pipeline.py +42 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
- package/src/superlocalmemory/mcp/tools_active.py +1 -1
- package/src/superlocalmemory/mcp/tools_core.py +17 -2
- package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
- package/src/superlocalmemory/server/routes/behavioral.py +6 -2
- package/src/superlocalmemory/server/routes/learning.py +13 -3
- package/src/superlocalmemory/server/routes/memories.py +8 -3
- package/src/superlocalmemory/server/routes/v3_api.py +120 -0
- package/src/superlocalmemory/server/unified_daemon.py +266 -2
- package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
- package/src/superlocalmemory/ui/index.html +3 -2
- package/src/superlocalmemory/ui/js/od-components.js +147 -0
- package/src/superlocalmemory/ui/js/od-entities.js +43 -0
- package/src/superlocalmemory/ui/js/od-graph.js +35 -0
- package/src/superlocalmemory/ui/js/od-health.js +18 -0
- package/src/superlocalmemory/ui/js/od-memories.js +37 -0
- package/src/superlocalmemory/ui/js/od-operations.js +36 -0
- package/src/superlocalmemory/ui/js/od-settings.js +72 -3
|
@@ -26,6 +26,23 @@ if TYPE_CHECKING:
|
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Backfill constants
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
#: Default batch size for backfill_missing_embeddings.
|
|
34
|
+
_BACKFILL_BATCH_SIZE = 50
|
|
35
|
+
|
|
36
|
+
#: Max characters embedded per fact during backfill. The embedding model
|
|
37
|
+
#: (nomic-embed-text-v1.5) truncates at ~8192 tokens anyway, but a raw
|
|
38
|
+
#: oversized document (observed up to 107 KB on a real DB) makes the shared
|
|
39
|
+
#: single-worker embedder busy for 15-20s on ONE fact — starving foreground
|
|
40
|
+
#: recall during a self-heal pass. Bounding the input keeps every fact's embed
|
|
41
|
+
#: fast and the worker responsive; the leading slice captures the fact's gist
|
|
42
|
+
#: for semantic recall. Facts this large are documents that were almost
|
|
43
|
+
#: certainly NULL because they failed to embed at ingestion for the same reason.
|
|
44
|
+
_MAX_EMBED_CHARS = 8000
|
|
45
|
+
|
|
29
46
|
# Sentinel stored in config.json when no model has been set yet.
|
|
30
47
|
_NO_MODEL = ""
|
|
31
48
|
|
|
@@ -44,6 +61,24 @@ def _model_signature(config: SLMConfig) -> str:
|
|
|
44
61
|
return f"{emb.model_name}::{emb.dimension}"
|
|
45
62
|
|
|
46
63
|
|
|
64
|
+
def _normalize_signature(signature: str) -> str:
|
|
65
|
+
"""Normalize a signature for equivalence comparison.
|
|
66
|
+
|
|
67
|
+
v3.8.2 self-healing: the SAME embedding model has been recorded under
|
|
68
|
+
different name strings across releases — notably the HuggingFace org
|
|
69
|
+
prefix drifted (``nomic-ai/nomic-embed-text-v1.5`` vs the bare
|
|
70
|
+
``nomic-embed-text-v1.5``). A prefix-only difference does NOT change the
|
|
71
|
+
embedding vector space, so it must not trigger a full multi-hour re-embed
|
|
72
|
+
when a non-technical user upgrades. This collapses the model name to its
|
|
73
|
+
basename (segment after the last ``/``) while keeping the ``::dimension``
|
|
74
|
+
suffix — a genuine model change (different basename OR dimension) still
|
|
75
|
+
differs and still triggers migration.
|
|
76
|
+
"""
|
|
77
|
+
model, sep, dim = signature.partition("::")
|
|
78
|
+
model = model.rsplit("/", 1)[-1].strip()
|
|
79
|
+
return f"{model}{sep}{dim}" if sep else model
|
|
80
|
+
|
|
81
|
+
|
|
47
82
|
def _read_stored_signature(config_dir: Path) -> str:
|
|
48
83
|
"""Read the last-used embedding model signature from config.json."""
|
|
49
84
|
config_path = config_dir / "config.json"
|
|
@@ -88,6 +123,19 @@ def check_embedding_migration(config: SLMConfig) -> bool:
|
|
|
88
123
|
if stored_sig == current_sig:
|
|
89
124
|
return False
|
|
90
125
|
|
|
126
|
+
# v3.8.2 self-healing: a prefix-only model-name drift (e.g. the nomic-ai/
|
|
127
|
+
# org prefix appearing/disappearing between releases) is the SAME vector
|
|
128
|
+
# space — absorb the transition by refreshing the stored signature to the
|
|
129
|
+
# current form, with NO re-embed. This spares non-technical users a
|
|
130
|
+
# multi-hour full re-index on a cosmetic upgrade.
|
|
131
|
+
if _normalize_signature(stored_sig) == _normalize_signature(current_sig):
|
|
132
|
+
_write_stored_signature(config.base_dir, current_sig)
|
|
133
|
+
logger.info(
|
|
134
|
+
"Embedding signature normalized (no re-embed): %s ~= %s",
|
|
135
|
+
stored_sig, current_sig,
|
|
136
|
+
)
|
|
137
|
+
return False
|
|
138
|
+
|
|
91
139
|
logger.warning(
|
|
92
140
|
"Embedding model changed: %s -> %s. Re-indexing required.",
|
|
93
141
|
stored_sig, current_sig,
|
|
@@ -177,3 +225,190 @@ def run_embedding_migration(
|
|
|
177
225
|
reindexed, total,
|
|
178
226
|
)
|
|
179
227
|
return reindexed
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Backfill: embed facts that were NEVER embedded (embedding IS NULL)
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def _count_null_embeddings(
|
|
235
|
+
db: Any,
|
|
236
|
+
profile_id: str,
|
|
237
|
+
all_profiles: bool,
|
|
238
|
+
) -> int:
|
|
239
|
+
"""Return count of atomic_facts rows with NULL embedding."""
|
|
240
|
+
if all_profiles:
|
|
241
|
+
rows = db.execute(
|
|
242
|
+
"SELECT count(*) AS c FROM atomic_facts WHERE embedding IS NULL",
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
rows = db.execute(
|
|
246
|
+
"SELECT count(*) AS c FROM atomic_facts "
|
|
247
|
+
"WHERE embedding IS NULL AND profile_id = ?",
|
|
248
|
+
(profile_id,),
|
|
249
|
+
)
|
|
250
|
+
return int(rows[0]["c"]) if rows else 0
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def backfill_missing_embeddings(
|
|
254
|
+
config: "SLMConfig",
|
|
255
|
+
db: Any,
|
|
256
|
+
embedder: Any,
|
|
257
|
+
batch_size: int = _BACKFILL_BATCH_SIZE,
|
|
258
|
+
limit: int | None = None,
|
|
259
|
+
all_profiles: bool = False,
|
|
260
|
+
) -> dict[str, int]:
|
|
261
|
+
"""Embed atomic_facts rows whose ``embedding`` column is NULL.
|
|
262
|
+
|
|
263
|
+
Unlike :func:`run_embedding_migration` (which re-embeds on model-signature
|
|
264
|
+
change), this function handles facts that were *never* embedded — for
|
|
265
|
+
example facts stored while the embedder was unavailable.
|
|
266
|
+
|
|
267
|
+
Resumable and idempotent: re-running after a partial run only processes
|
|
268
|
+
the remaining NULLs. Fail-open per-fact: a single bad fact logs a warning
|
|
269
|
+
and is skipped; the batch continues.
|
|
270
|
+
|
|
271
|
+
Writes mirror :func:`run_embedding_migration` exactly:
|
|
272
|
+
* ``atomic_facts.embedding`` ← ``json.dumps(vector)``
|
|
273
|
+
* ``embedding_metadata`` ← upserted row with current model name + dimension
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
config: Active SLMConfig (provides profile_id, model name, dimension).
|
|
277
|
+
db: DatabaseManager (or duck-compatible object with ``.execute()``).
|
|
278
|
+
embedder: Object implementing ``embed_batch(texts) -> list[vec|None]``
|
|
279
|
+
and (optionally) ``embed(text) -> vec|None``. Pass ``None`` to
|
|
280
|
+
make this a no-op (returns zero counts).
|
|
281
|
+
batch_size: Facts per embed_batch() call. Defaults to 50.
|
|
282
|
+
limit: Maximum facts to embed in this call. ``None`` means no cap —
|
|
283
|
+
all NULL-embedding facts are processed. Use a bounded limit for
|
|
284
|
+
the maintenance self-healing path so each pass is quick.
|
|
285
|
+
all_profiles: When ``True``, processes facts from every profile in the
|
|
286
|
+
database. When ``False`` (default), scopes to
|
|
287
|
+
``config.active_profile``.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
``{"scanned": int, "embedded": int, "remaining_null": int}``
|
|
291
|
+
|
|
292
|
+
*scanned*: total NULL-embedding facts found before applying *limit*.
|
|
293
|
+
*embedded*: facts successfully written in this call.
|
|
294
|
+
*remaining_null*: NULL count after the call (includes facts not yet
|
|
295
|
+
reached because of *limit*).
|
|
296
|
+
"""
|
|
297
|
+
profile_id = config.active_profile
|
|
298
|
+
|
|
299
|
+
if embedder is None:
|
|
300
|
+
logger.warning(
|
|
301
|
+
"backfill_missing_embeddings: no embedder available — skipping."
|
|
302
|
+
)
|
|
303
|
+
return {"scanned": 0, "embedded": 0, "remaining_null": 0}
|
|
304
|
+
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
# 1. Fetch all NULL-embedding facts (cheap query; only reads IDs + content)
|
|
307
|
+
# ------------------------------------------------------------------
|
|
308
|
+
if all_profiles:
|
|
309
|
+
rows = db.execute(
|
|
310
|
+
"SELECT fact_id, content, profile_id FROM atomic_facts "
|
|
311
|
+
"WHERE embedding IS NULL ORDER BY created_at",
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
rows = db.execute(
|
|
315
|
+
"SELECT fact_id, content, profile_id FROM atomic_facts "
|
|
316
|
+
"WHERE embedding IS NULL AND profile_id = ? ORDER BY created_at",
|
|
317
|
+
(profile_id,),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
facts: list[tuple[str, str, str]] = [
|
|
321
|
+
(dict(r)["fact_id"], dict(r)["content"], dict(r)["profile_id"])
|
|
322
|
+
for r in rows
|
|
323
|
+
]
|
|
324
|
+
scanned = len(facts)
|
|
325
|
+
|
|
326
|
+
if scanned == 0:
|
|
327
|
+
return {"scanned": 0, "embedded": 0, "remaining_null": 0}
|
|
328
|
+
|
|
329
|
+
# Apply call-level limit (resumability: next call picks up where this left off)
|
|
330
|
+
if limit is not None:
|
|
331
|
+
facts = facts[:limit]
|
|
332
|
+
|
|
333
|
+
current_model = config.embedding.model_name
|
|
334
|
+
current_dim = config.embedding.dimension
|
|
335
|
+
embedded = 0
|
|
336
|
+
|
|
337
|
+
# ------------------------------------------------------------------
|
|
338
|
+
# 2. Batch embed and write back
|
|
339
|
+
# ------------------------------------------------------------------
|
|
340
|
+
for batch_start in range(0, len(facts), batch_size):
|
|
341
|
+
batch = facts[batch_start : batch_start + batch_size]
|
|
342
|
+
# Bound per-fact input so an oversized document doesn't monopolize the
|
|
343
|
+
# shared embedding worker (starving foreground recall during self-heal).
|
|
344
|
+
texts = [(content or "")[:_MAX_EMBED_CHARS] for _, content, _ in batch]
|
|
345
|
+
fact_ids = [fid for fid, _, _ in batch]
|
|
346
|
+
prof_ids = [pid for _, _, pid in batch]
|
|
347
|
+
|
|
348
|
+
# Attempt batch embed; fall back to per-fact on batch failure.
|
|
349
|
+
try:
|
|
350
|
+
vectors: list[Any] = embedder.embed_batch(texts)
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
logger.warning(
|
|
353
|
+
"backfill: batch embed failed for facts %d-%d: %s — "
|
|
354
|
+
"retrying per-fact.",
|
|
355
|
+
batch_start,
|
|
356
|
+
batch_start + len(batch),
|
|
357
|
+
exc,
|
|
358
|
+
)
|
|
359
|
+
vectors = []
|
|
360
|
+
for text in texts:
|
|
361
|
+
try:
|
|
362
|
+
vec = embedder.embed(text)
|
|
363
|
+
vectors.append(vec)
|
|
364
|
+
except Exception as per_fact_exc:
|
|
365
|
+
logger.warning(
|
|
366
|
+
"backfill: per-fact embed failed for '%s...': %s",
|
|
367
|
+
text[:40],
|
|
368
|
+
per_fact_exc,
|
|
369
|
+
)
|
|
370
|
+
vectors.append(None)
|
|
371
|
+
|
|
372
|
+
# Write each successfully-embedded fact back to the DB.
|
|
373
|
+
for fid, vec, pid in zip(fact_ids, vectors, prof_ids):
|
|
374
|
+
if vec is None:
|
|
375
|
+
logger.warning(
|
|
376
|
+
"backfill: null vector for fact %s — skipping.", fid[:16]
|
|
377
|
+
)
|
|
378
|
+
continue
|
|
379
|
+
try:
|
|
380
|
+
embedding_json = json.dumps(vec)
|
|
381
|
+
# Mirror run_embedding_migration's write path exactly.
|
|
382
|
+
db.execute(
|
|
383
|
+
"UPDATE atomic_facts SET embedding = ? WHERE fact_id = ?",
|
|
384
|
+
(embedding_json, fid),
|
|
385
|
+
)
|
|
386
|
+
# Upsert embedding_metadata. NULL-embedding facts have no row
|
|
387
|
+
# here yet, so we INSERT; if a row somehow exists, update it.
|
|
388
|
+
db.execute(
|
|
389
|
+
"INSERT INTO embedding_metadata"
|
|
390
|
+
" (fact_id, profile_id, model_name, dimension)"
|
|
391
|
+
" VALUES (?, ?, ?, ?)"
|
|
392
|
+
" ON CONFLICT(fact_id) DO UPDATE SET"
|
|
393
|
+
" model_name = excluded.model_name",
|
|
394
|
+
(fid, pid, current_model, current_dim),
|
|
395
|
+
)
|
|
396
|
+
embedded += 1
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
logger.warning(
|
|
399
|
+
"backfill: failed to write fact %s: %s", fid[:16], exc
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# ------------------------------------------------------------------
|
|
403
|
+
# 3. Count remaining NULLs (accounts for the limit; tells caller how
|
|
404
|
+
# many passes remain before full convergence).
|
|
405
|
+
# ------------------------------------------------------------------
|
|
406
|
+
remaining = _count_null_embeddings(db, profile_id, all_profiles)
|
|
407
|
+
|
|
408
|
+
logger.info(
|
|
409
|
+
"Embedding backfill: %d/%d facts embedded, %d remaining NULL.",
|
|
410
|
+
embedded,
|
|
411
|
+
scanned,
|
|
412
|
+
remaining,
|
|
413
|
+
)
|
|
414
|
+
return {"scanned": scanned, "embedded": embedded, "remaining_null": remaining}
|
|
@@ -1566,7 +1566,8 @@
|
|
|
1566
1566
|
<!-- OD screen modules (v3.7.9): approved-design panes wired to live data.
|
|
1567
1567
|
Load LAST so their window.load<Screen> overrides win over the legacy loaders. -->
|
|
1568
1568
|
<script src="static/js/od-brain.js?v=379"></script>
|
|
1569
|
-
<script src="static/js/od-
|
|
1569
|
+
<script src="static/js/od-components.js?v=382"></script>
|
|
1570
|
+
<script src="static/js/od-health.js?v=382"></script>
|
|
1570
1571
|
<script src="static/js/od-operations.js?v=380"></script>
|
|
1571
1572
|
<script src="static/js/od-team.js?v=379"></script>
|
|
1572
1573
|
<script src="static/js/od-graph.js?v=379"></script>
|
|
@@ -1577,7 +1578,7 @@
|
|
|
1577
1578
|
<script src="static/js/od-skills.js?v=379"></script>
|
|
1578
1579
|
<script src="static/js/od-mesh.js?v=379"></script>
|
|
1579
1580
|
<script src="static/js/od-optimize.js?v=379"></script>
|
|
1580
|
-
<script src="static/js/od-settings.js?v=
|
|
1581
|
+
<script src="static/js/od-settings.js?v=382"></script>
|
|
1581
1582
|
<script src="static/js/od-backup.js?v=379"></script>
|
|
1582
1583
|
<!-- MCP & Integrations pane (v3.8.0): shows exposed MCP tool profile + counts -->
|
|
1583
1584
|
<script src="static/js/od-mcp.js?v=380"></script>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// od-components.js — System Health "What's Missing" report (v3.8.2 UX-6)
|
|
2
|
+
//
|
|
3
|
+
// window.odRenderComponents(el) renders the component-registry snapshot from
|
|
4
|
+
// GET /api/v3/components → {components:[...], summary:{...}}
|
|
5
|
+
// into `el`, showing per-component status, a copy-paste fix command for the
|
|
6
|
+
// items SLM can't repair on its own, and a "Retry now" button that triggers
|
|
7
|
+
// POST /api/v3/components/heal (install token auto-attached by core.js)
|
|
8
|
+
// The daemon auto-heals on start; this panel is transparency + a manual
|
|
9
|
+
// fallback for the non-technical user.
|
|
10
|
+
//
|
|
11
|
+
// Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
|
|
12
|
+
|
|
13
|
+
/* global window, document, fetch, navigator, setTimeout */
|
|
14
|
+
(function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
function esc(s) {
|
|
18
|
+
return String(s == null ? '' : s)
|
|
19
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Severity/glyph/colour for one component, honouring its category so an
|
|
24
|
+
// absent optional backend is not shown as an error.
|
|
25
|
+
function view(c) {
|
|
26
|
+
var st = String(c.status || '').toLowerCase();
|
|
27
|
+
if (st === 'ok') return { g: '✓', col: 'var(--ok)', tip: 'ready' };
|
|
28
|
+
if (st === 'retrying') return { g: '⟳', col: 'var(--accent, #4a9)', tip: 'repairing' };
|
|
29
|
+
if (st === 'degraded') return { g: '⚠', col: 'var(--warn)', tip: 'degraded' };
|
|
30
|
+
// missing — severity depends on how much SLM needs it.
|
|
31
|
+
if (c.category === 'required') return { g: '✗', col: 'var(--danger)', tip: 'missing' };
|
|
32
|
+
if (c.category === 'recommended') return { g: '⚠', col: 'var(--warn)', tip: 'missing' };
|
|
33
|
+
return { g: '○', col: 'var(--fg-2)', tip: 'optional' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rowHtml(c) {
|
|
37
|
+
var v = view(c);
|
|
38
|
+
var fix = '';
|
|
39
|
+
if (c.status !== 'ok' && c.fix_cmd) {
|
|
40
|
+
if (c.auto_fixable) {
|
|
41
|
+
fix = '<div class="muted" style="margin-top:2px;font-size:12px">'
|
|
42
|
+
+ 'SLM repairs this automatically.</div>';
|
|
43
|
+
} else {
|
|
44
|
+
fix = '<div style="margin-top:4px;display:flex;gap:6px;align-items:center">'
|
|
45
|
+
+ '<code style="font-size:12px;background:var(--bg-2,#0002);'
|
|
46
|
+
+ 'padding:2px 6px;border-radius:4px">' + esc(c.fix_cmd) + '</code>'
|
|
47
|
+
+ '<button class="od-copy-fix" data-cmd="' + esc(c.fix_cmd) + '" '
|
|
48
|
+
+ 'style="font-size:11px;padding:1px 6px;cursor:pointer">Copy</button>'
|
|
49
|
+
+ '</div>';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return '<div style="padding:8px 0;border-bottom:1px solid var(--border,#8881)">'
|
|
53
|
+
+ '<div style="display:flex;gap:8px;align-items:baseline">'
|
|
54
|
+
+ '<span style="color:' + v.col + ';font-weight:700;width:16px;text-align:center">'
|
|
55
|
+
+ v.g + '</span>'
|
|
56
|
+
+ '<div style="flex:1">'
|
|
57
|
+
+ '<span style="font-weight:600">' + esc(c.label) + '</span>'
|
|
58
|
+
+ ' <span class="muted" style="font-size:12px">' + esc(c.detail || v.tip) + '</span>'
|
|
59
|
+
+ fix
|
|
60
|
+
+ '</div></div></div>';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function render(el, data) {
|
|
64
|
+
var comps = (data && data.components) || [];
|
|
65
|
+
var s = (data && data.summary) || {};
|
|
66
|
+
var healthy = !!s.healthy;
|
|
67
|
+
var hasAutoFix = (s.auto_fixable_missing || 0) > 0;
|
|
68
|
+
|
|
69
|
+
var head = '<div style="display:flex;justify-content:space-between;'
|
|
70
|
+
+ 'align-items:center;margin-bottom:6px">'
|
|
71
|
+
+ '<h3 style="margin:0">System Health — Components</h3>'
|
|
72
|
+
+ '<span style="padding:2px 10px;border-radius:12px;font-size:12px;'
|
|
73
|
+
+ 'background:' + (healthy ? 'var(--ok)' : 'var(--warn)') + ';color:#000">'
|
|
74
|
+
+ (healthy ? 'All good' : 'Needs attention') + '</span></div>';
|
|
75
|
+
|
|
76
|
+
var summary = '<div class="muted" style="font-size:12px;margin-bottom:8px">'
|
|
77
|
+
+ (s.ok || 0) + ' ready · ' + (s.missing || 0) + ' missing · '
|
|
78
|
+
+ (s.degraded || 0) + ' degraded'
|
|
79
|
+
+ (s.retrying ? ' · ' + s.retrying + ' repairing' : '') + '</div>';
|
|
80
|
+
|
|
81
|
+
var actions = '<div style="margin-top:10px;display:flex;gap:8px">'
|
|
82
|
+
+ '<button id="od-comp-recheck" style="padding:4px 12px;cursor:pointer">Recheck</button>'
|
|
83
|
+
+ (hasAutoFix
|
|
84
|
+
? '<button id="od-comp-retry" style="padding:4px 12px;cursor:pointer;'
|
|
85
|
+
+ 'background:var(--accent,#4a9);color:#000;border:none;border-radius:4px">'
|
|
86
|
+
+ 'Retry now</button>'
|
|
87
|
+
: '')
|
|
88
|
+
+ '</div>'
|
|
89
|
+
+ '<div class="muted" style="font-size:11px;margin-top:6px">'
|
|
90
|
+
+ 'SLM auto-repairs fixable items on start. Copy a command for anything '
|
|
91
|
+
+ 'it can’t install for you.</div>';
|
|
92
|
+
|
|
93
|
+
el.innerHTML = head + summary
|
|
94
|
+
+ '<div>' + comps.map(rowHtml).join('') + '</div>' + actions;
|
|
95
|
+
|
|
96
|
+
// Copy buttons.
|
|
97
|
+
el.querySelectorAll('.od-copy-fix').forEach(function (b) {
|
|
98
|
+
b.addEventListener('click', function () {
|
|
99
|
+
var cmd = b.getAttribute('data-cmd') || '';
|
|
100
|
+
if (navigator.clipboard) navigator.clipboard.writeText(cmd);
|
|
101
|
+
var old = b.textContent; b.textContent = 'Copied';
|
|
102
|
+
setTimeout(function () { b.textContent = old; }, 1200);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
var recheck = el.querySelector('#od-comp-recheck');
|
|
106
|
+
if (recheck) recheck.addEventListener('click', function () { load(el); });
|
|
107
|
+
var retry = el.querySelector('#od-comp-retry');
|
|
108
|
+
if (retry) retry.addEventListener('click', function () { doRetry(el, retry); });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function load(el) {
|
|
112
|
+
if (!el) return;
|
|
113
|
+
el.innerHTML = '<p class="muted" style="padding:8px 0">Checking components…</p>';
|
|
114
|
+
fetch('/api/v3/components')
|
|
115
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
116
|
+
.then(function (data) {
|
|
117
|
+
if (!data) {
|
|
118
|
+
el.innerHTML = '<p class="muted" style="padding:8px 0">'
|
|
119
|
+
+ 'Component health unavailable (is the daemon running?).</p>';
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
render(el, data);
|
|
123
|
+
})
|
|
124
|
+
.catch(function () {
|
|
125
|
+
el.innerHTML = '<p class="muted" style="padding:8px 0">'
|
|
126
|
+
+ 'Could not load component health.</p>';
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Trigger a background heal, then poll a few times so the panel reflects
|
|
131
|
+
// the retrying → ok transition without the user refreshing.
|
|
132
|
+
function doRetry(el, btn) {
|
|
133
|
+
btn.disabled = true; btn.textContent = 'Repairing…';
|
|
134
|
+
fetch('/api/v3/components/heal', { method: 'POST', credentials: 'same-origin' })
|
|
135
|
+
.then(function () {
|
|
136
|
+
var tries = 0;
|
|
137
|
+
(function poll() {
|
|
138
|
+
tries += 1;
|
|
139
|
+
load(el);
|
|
140
|
+
if (tries < 4) setTimeout(poll, 2500);
|
|
141
|
+
})();
|
|
142
|
+
})
|
|
143
|
+
.catch(function () { load(el); });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
window.odRenderComponents = load;
|
|
147
|
+
})();
|
|
@@ -396,6 +396,17 @@
|
|
|
396
396
|
_dateField('Last seen', listEnt.last_seen) +
|
|
397
397
|
_dateField('Compiled', d.last_compiled_at) +
|
|
398
398
|
'</div>' +
|
|
399
|
+
// Recompile action — restored: POST /api/entity/{name}/recompile
|
|
400
|
+
// The disabled guard was removed; data-ename carries the name to _wire() handler.
|
|
401
|
+
'<div style="margin-top:16px;display:flex;align-items:center;gap:10px">' +
|
|
402
|
+
'<button data-od-act="recompile-entity" ' +
|
|
403
|
+
'data-ename="' + _esc(d.entity_name || '') + '" ' +
|
|
404
|
+
'style="padding:5px 14px;border:1px solid var(--border);border-radius:5px;' +
|
|
405
|
+
'font-size:12px;cursor:pointer;background:var(--bg-2);color:var(--fg)">' +
|
|
406
|
+
'Recompile</button>' +
|
|
407
|
+
'<span id="' + id + '-recompile-status" ' +
|
|
408
|
+
'style="font-size:12px;color:var(--fg-2)"></span>' +
|
|
409
|
+
'</div>' +
|
|
399
410
|
'</div>' +
|
|
400
411
|
'</div>' +
|
|
401
412
|
|
|
@@ -514,12 +525,44 @@
|
|
|
514
525
|
|
|
515
526
|
// ── Event delegation ──────────────────────────────────────────────────────
|
|
516
527
|
|
|
528
|
+
// Recompile entity — POST /api/entity/{name}/recompile
|
|
529
|
+
// Updates button text and status span to give feedback without a page reload.
|
|
530
|
+
function _recompileEntity(id, name, btn) {
|
|
531
|
+
if (!name) return;
|
|
532
|
+
var statusEl = document.getElementById(id + '-recompile-status');
|
|
533
|
+
btn.disabled = true;
|
|
534
|
+
btn.textContent = 'Recompiling…';
|
|
535
|
+
if (statusEl) { statusEl.textContent = ''; }
|
|
536
|
+
fetch('/api/entity/' + encodeURIComponent(name) + '/recompile', { method: 'POST' })
|
|
537
|
+
.then(function (r) { return r.json(); })
|
|
538
|
+
.then(function (data) {
|
|
539
|
+
btn.disabled = false;
|
|
540
|
+
btn.textContent = 'Recompile';
|
|
541
|
+
if (statusEl) {
|
|
542
|
+
statusEl.style.color = 'var(--accent)';
|
|
543
|
+
statusEl.textContent = data.message || 'Recompiled successfully';
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
.catch(function (err) {
|
|
547
|
+
btn.disabled = false;
|
|
548
|
+
btn.textContent = 'Recompile';
|
|
549
|
+
if (statusEl) {
|
|
550
|
+
statusEl.style.color = 'var(--danger, #e05)';
|
|
551
|
+
statusEl.textContent = 'Recompile failed: ' + (err.message || 'unknown error');
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
517
556
|
function _wire(container, id) {
|
|
518
557
|
container.addEventListener('click', function (e) {
|
|
519
558
|
var el = e.target.closest('[data-od-act]');
|
|
520
559
|
if (!el) return;
|
|
521
560
|
var act = el.dataset.odAct;
|
|
522
561
|
|
|
562
|
+
if (act === 'recompile-entity') {
|
|
563
|
+
_recompileEntity(id, el.dataset.ename, el);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
523
566
|
if (act === 'type-filter') {
|
|
524
567
|
_st = Object.assign({}, _st, { typeFilter: el.dataset.type, page: 0 });
|
|
525
568
|
_refreshTypeChips(id, el.dataset.type);
|
|
@@ -93,6 +93,31 @@
|
|
|
93
93
|
'<div class="inspector-empty"><div style="font-size:34px;margin-bottom:8px">◇</div>' +
|
|
94
94
|
'Loading your knowledge graph…</div>' +
|
|
95
95
|
'</div>' +
|
|
96
|
+
// Quick Insight Actions — restored: 5 data-insight-action buttons + #insight-results.
|
|
97
|
+
// quick-actions.js (global, non-IIFE) defines fetchInsight(action) which calls
|
|
98
|
+
// GET /api/v3/insights/{action} and writes into #insight-results.
|
|
99
|
+
// Delegation wired in wireControls() so buttons survive re-renders.
|
|
100
|
+
'<div id="odg-insights-panel" style="border-top:1px solid var(--border);' +
|
|
101
|
+
'padding:10px 12px 8px">' +
|
|
102
|
+
'<div style="font-size:10.5px;font-weight:600;color:var(--fg-3);' +
|
|
103
|
+
'text-transform:uppercase;letter-spacing:0.06em;margin-bottom:7px">' +
|
|
104
|
+
'Quick Insights</div>' +
|
|
105
|
+
'<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:7px">' +
|
|
106
|
+
'<button class="chip" style="font-size:11px;padding:3px 9px;cursor:pointer" ' +
|
|
107
|
+
'data-insight-action="changed_this_week">Changed This Week</button>' +
|
|
108
|
+
'<button class="chip" style="font-size:11px;padding:3px 9px;cursor:pointer" ' +
|
|
109
|
+
'data-insight-action="opinions">Opinions</button>' +
|
|
110
|
+
'<button class="chip" style="font-size:11px;padding:3px 9px;cursor:pointer" ' +
|
|
111
|
+
'data-insight-action="contradictions">Contradictions</button>' +
|
|
112
|
+
'<button class="chip" style="font-size:11px;padding:3px 9px;cursor:pointer" ' +
|
|
113
|
+
'data-insight-action="health">Memory Health</button>' +
|
|
114
|
+
'<button class="chip" style="font-size:11px;padding:3px 9px;cursor:pointer" ' +
|
|
115
|
+
'data-insight-action="cross_project">Cross-Project</button>' +
|
|
116
|
+
'</div>' +
|
|
117
|
+
// #insight-results — quick-actions.js writes here via document.getElementById
|
|
118
|
+
'<div id="insight-results" ' +
|
|
119
|
+
'style="max-height:200px;overflow-y:auto;font-size:12px"></div>' +
|
|
120
|
+
'</div>' +
|
|
96
121
|
'<div class="ask">' +
|
|
97
122
|
'<div class="ask-head"><span data-ic="brain"></span> Ask your memory</div>' +
|
|
98
123
|
'<div class="ask-log" id="odg-log">' +
|
|
@@ -502,6 +527,16 @@
|
|
|
502
527
|
q('#odg-send').onclick = sendAsk;
|
|
503
528
|
q('#odg-ask').addEventListener('keydown', function (e) { if (e.key === 'Enter') sendAsk(); });
|
|
504
529
|
|
|
530
|
+
// Quick Insight Actions — delegated on mount so buttons survive od-graph.js re-renders.
|
|
531
|
+
// quick-actions.js:fetchInsight() is global (non-IIFE); it writes into #insight-results
|
|
532
|
+
// which is mounted inside odg-insights-panel above.
|
|
533
|
+
mount.addEventListener('click', function (e) {
|
|
534
|
+
var btn = e.target.closest('[data-insight-action]');
|
|
535
|
+
if (btn && typeof fetchInsight === 'function') {
|
|
536
|
+
fetchInsight(btn.getAttribute('data-insight-action'));
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
505
540
|
cv.addEventListener('mousedown', function (e) {
|
|
506
541
|
var r = cv.getBoundingClientRect(), n = nodeAt(e.clientX - r.left, e.clientY - r.top);
|
|
507
542
|
if (n) { dragNode = n; selected = n; renderInsp(n); wake(); } else { panning = true; }
|
|
@@ -183,6 +183,20 @@
|
|
|
183
183
|
'</tbody>' +
|
|
184
184
|
'</table>' +
|
|
185
185
|
'</div>' +
|
|
186
|
+
'</div>' +
|
|
187
|
+
|
|
188
|
+
// System Health — Components (v3.8.2 UX-6): what's installed vs missing,
|
|
189
|
+
// with copy-paste fix commands + a Retry-now button. The daemon
|
|
190
|
+
// auto-heals fixable items on start; this is transparency + fallback.
|
|
191
|
+
// Rendered by od-components.js → window.odRenderComponents.
|
|
192
|
+
'<div class="card" style="margin-top:16px">' +
|
|
193
|
+
'<div class="card-head">' +
|
|
194
|
+
'<h3>System Health — Components</h3>' +
|
|
195
|
+
'<span class="sub">models & dependencies · self-healing</span>' +
|
|
196
|
+
'</div>' +
|
|
197
|
+
'<div class="card-pad" id="od-h-components">' +
|
|
198
|
+
'<div class="dim" style="text-align:center;padding:20px">Loading…</div>' +
|
|
199
|
+
'</div>' +
|
|
186
200
|
'</div>'
|
|
187
201
|
);
|
|
188
202
|
}
|
|
@@ -515,6 +529,10 @@
|
|
|
515
529
|
populateTrustSignals(trustStats);
|
|
516
530
|
populateAgentSummary(agentsData, eventsStats);
|
|
517
531
|
populateAgentTable(agentsData);
|
|
532
|
+
// v3.8.2 UX-6: component "what's missing" report (od-components.js).
|
|
533
|
+
if (typeof window.odRenderComponents === 'function') {
|
|
534
|
+
window.odRenderComponents(document.getElementById('od-h-components'));
|
|
535
|
+
}
|
|
518
536
|
|
|
519
537
|
}).catch(function (err) {
|
|
520
538
|
container.innerHTML =
|
|
@@ -147,6 +147,9 @@
|
|
|
147
147
|
'<button class="tab" data-od-act="tab" data-tab="timeline">Creation timeline</button>' +
|
|
148
148
|
'<button class="tab" data-od-act="tab" data-tab="clusters">' +
|
|
149
149
|
'Knowledge clusters <span class="cnt" id="' + id + '-cnt-clusters">…</span></button>' +
|
|
150
|
+
// Recall Lab tab — restored: recall-lab.js is already loaded in index.html and uses
|
|
151
|
+
// document-level click/keydown delegation keyed on IDs below, so it survives re-renders.
|
|
152
|
+
'<button class="tab" data-od-act="tab" data-tab="recall">Recall Lab</button>' +
|
|
150
153
|
'</div>' +
|
|
151
154
|
'<div class="tabpane active" id="' + id + '-pane-all">' + _allScaffold(id) + '</div>' +
|
|
152
155
|
'<div class="tabpane" id="' + id + '-pane-timeline">' + _tlScaffold(id) + '</div>' +
|
|
@@ -155,6 +158,40 @@
|
|
|
155
158
|
_loading('Loading clusters…') +
|
|
156
159
|
'</div>' +
|
|
157
160
|
'</div>' +
|
|
161
|
+
// Recall Lab pane — exact IDs required by recall-lab.js:
|
|
162
|
+
// #recall-lab-query (input), #recall-lab-search (button — click check),
|
|
163
|
+
// #recall-lab-per-page (select, optional), #recall-lab-meta, #recall-lab-results.
|
|
164
|
+
// Backend: POST /api/v3/recall/trace
|
|
165
|
+
'<div class="tabpane" id="' + id + '-pane-recall" style="padding-top:12px">' +
|
|
166
|
+
'<div style="margin-bottom:14px">' +
|
|
167
|
+
'<p style="font-size:13px;color:var(--fg-2);margin-bottom:10px">' +
|
|
168
|
+
'Trace how a recall query is resolved — matching algorithm, scoring, and final result set.' +
|
|
169
|
+
'</p>' +
|
|
170
|
+
'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
|
|
171
|
+
'<input id="recall-lab-query" placeholder="Enter recall query…" autocomplete="off" ' +
|
|
172
|
+
'style="flex:1;min-width:200px;padding:8px 12px;' +
|
|
173
|
+
'border:1px solid var(--border);border-radius:6px;font-size:13px;' +
|
|
174
|
+
'background:var(--bg-2);color:var(--fg)">' +
|
|
175
|
+
'<select id="recall-lab-per-page" ' +
|
|
176
|
+
'style="padding:7px 10px;border:1px solid var(--border);border-radius:6px;' +
|
|
177
|
+
'font-size:13px;background:var(--bg-2);color:var(--fg)">' +
|
|
178
|
+
'<option value="5">5</option>' +
|
|
179
|
+
'<option value="10" selected>10</option>' +
|
|
180
|
+
'<option value="20">20</option>' +
|
|
181
|
+
'<option value="50">50</option>' +
|
|
182
|
+
'</select>' +
|
|
183
|
+
'<button id="recall-lab-search" ' +
|
|
184
|
+
'style="padding:8px 18px;background:var(--accent);color:#fff;' +
|
|
185
|
+
'border:none;border-radius:6px;font-size:13px;cursor:pointer;' +
|
|
186
|
+
'white-space:nowrap">Run Trace</button>' +
|
|
187
|
+
'</div>' +
|
|
188
|
+
'</div>' +
|
|
189
|
+
// #recall-lab-meta — written by recall-lab.js before results (timing, count, etc.)
|
|
190
|
+
'<div id="recall-lab-meta" ' +
|
|
191
|
+
'style="font-size:12px;color:var(--fg-2);margin-bottom:8px"></div>' +
|
|
192
|
+
// #recall-lab-results — written by recall-lab.js (result cards, pagination)
|
|
193
|
+
'<div id="recall-lab-results"></div>' +
|
|
194
|
+
'</div>' +
|
|
158
195
|
'</div>'
|
|
159
196
|
);
|
|
160
197
|
}
|