superlocalmemory 3.8.1 → 3.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +2 -2
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +3 -5
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +2 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +3 -5
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +1 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +360 -2
  35. package/src/superlocalmemory/cli/main.py +62 -3
  36. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  37. package/src/superlocalmemory/core/component_healer.py +144 -0
  38. package/src/superlocalmemory/core/component_registry.py +487 -0
  39. package/src/superlocalmemory/core/config.py +21 -0
  40. package/src/superlocalmemory/core/embeddings.py +14 -1
  41. package/src/superlocalmemory/core/engine.py +9 -5
  42. package/src/superlocalmemory/core/ingestion_command.py +36 -16
  43. package/src/superlocalmemory/core/maintenance.py +43 -0
  44. package/src/superlocalmemory/core/maintenance_scheduler.py +28 -0
  45. package/src/superlocalmemory/core/recall_pipeline.py +39 -3
  46. package/src/superlocalmemory/core/store_pipeline.py +42 -0
  47. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  48. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  49. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  50. package/src/superlocalmemory/mcp/tools_core.py +17 -2
  51. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  52. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  53. package/src/superlocalmemory/server/routes/behavioral.py +6 -2
  54. package/src/superlocalmemory/server/routes/learning.py +13 -3
  55. package/src/superlocalmemory/server/routes/memories.py +80 -26
  56. package/src/superlocalmemory/server/routes/v3_api.py +120 -0
  57. package/src/superlocalmemory/server/unified_daemon.py +349 -10
  58. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  59. package/src/superlocalmemory/ui/index.html +3 -2
  60. package/src/superlocalmemory/ui/js/core.js +6 -1
  61. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  62. package/src/superlocalmemory/ui/js/od-entities.js +43 -0
  63. package/src/superlocalmemory/ui/js/od-graph.js +35 -0
  64. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  65. package/src/superlocalmemory/ui/js/od-memories.js +37 -0
  66. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  67. package/src/superlocalmemory/ui/js/od-settings.js +72 -3
@@ -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-health.js?v=380"></script>
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=380"></script>
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>
@@ -13,7 +13,12 @@
13
13
  // surfaces as a normal rejection and the UI can show a clear error.
14
14
  // ============================================================================
15
15
 
16
- window.SLM_FETCH_TIMEOUT_MS = 15000;
16
+ // v3.8.3: 30s (> the server's 25s recall budget) so the browser waits for a
17
+ // quality recall under heavy multi-agent load instead of aborting it early
18
+ // ("signal is aborted without reason"). The server self-bounds recall and
19
+ // falls back to keyword within its budget, so this only ever waits longer for
20
+ // a genuinely stuck request. Per-call init.timeoutMs still overrides.
21
+ window.SLM_FETCH_TIMEOUT_MS = 30000;
17
22
  window.SLM_INSTALL_TOKEN_KEY = 'slm_install_token';
18
23
 
19
24
  // B2 (3.7.9): the install token is kept in a private closure, never in
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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">&#9671;</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 &amp; 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
  }
@@ -96,6 +96,9 @@
96
96
  '<button class="tab" data-od-tab="trust">Trust</button>' +
97
97
  '<button class="tab" data-od-tab="compliance">Compliance</button>' +
98
98
  '<button class="tab" data-od-tab="loops">Bounded Loops</button>' +
99
+ // Ingestion tab — restored: re-mounts #ingestion-overview + #ingestion-adapters
100
+ // so ng-ingestion.js (loaded in index.html) can populate them on call.
101
+ '<button class="tab" data-od-tab="ingestion">Ingestion</button>' +
99
102
  '</div>' +
100
103
 
101
104
  // ══════ LIFECYCLE TAB ══════════════════════════════════════════════
@@ -427,6 +430,31 @@
427
430
  '</div>' +
428
431
  '</div>' +
429
432
 
433
+ '</section>' +
434
+
435
+ // ══════ INGESTION TAB ═════════════════════════════════════════════
436
+ // Restoration: mounts #ingestion-overview and #ingestion-adapters so that
437
+ // window.loadIngestionStatus() (ng-ingestion.js) can populate them.
438
+ // ng-ingestion.js is loaded in index.html and defines loadIngestionStatus().
439
+ // Adapter action buttons use data-act-click="adapter-action" which is handled
440
+ // by event-delegation.js → adapterAction() defined in ng-ingestion.js.
441
+ // Backend: GET /api/adapters · POST /api/adapters/{enable|disable|start|stop}
442
+ '<section class="tabpane" data-od-pane="ingestion">' +
443
+
444
+ '<div class="page-head" style="margin-bottom:16px">' +
445
+ '<h2 style="font-size:20px;margin-bottom:6px">Connectors &amp; Ingestion</h2>' +
446
+ '<p style="font-size:13.5px">' +
447
+ 'Gmail, Calendar, and Transcript adapters — enable to start building memory ' +
448
+ 'from external sources. All processing is local.' +
449
+ '</p>' +
450
+ '</div>' +
451
+
452
+ // #ingestion-overview — ng-ingestion.js: 4 stat cards (Available/Enabled/Running/Privacy)
453
+ '<div id="ingestion-overview"></div>' +
454
+
455
+ // #ingestion-adapters — ng-ingestion.js: adapter cards (Gmail/Calendar/Transcript)
456
+ '<div id="ingestion-adapters" style="margin-top:16px"></div>' +
457
+
430
458
  '</section>'
431
459
  );
432
460
  }
@@ -1227,6 +1255,14 @@
1227
1255
  window.odRenderTeam(document.getElementById('od-team-mount'));
1228
1256
  }
1229
1257
 
1258
+ // Ingestion (Gmail / Calendar / Transcript) — re-linked to ng-ingestion.js.
1259
+ // #ingestion-overview and #ingestion-adapters are now in the skeleton DOM
1260
+ // (inside the Ingestion tab section); calling loadIngestionStatus() renders them.
1261
+ // The function is null-guarded: no-op if ng-ingestion.js failed to load.
1262
+ if (typeof window.loadIngestionStatus === 'function') {
1263
+ window.loadIngestionStatus();
1264
+ }
1265
+
1230
1266
  }).catch(function (err) {
1231
1267
  container.innerHTML =
1232
1268
  '<div style="color:var(--danger);text-align:center;padding:48px 0">' +
@@ -509,6 +509,75 @@
509
509
  }).catch(function () {});
510
510
  }
511
511
 
512
+ /* ═══════════════════════════════════════════════════════════
513
+ GROUP — Recall & Memory Behaviour (v3.8.2, live-apply, no restart)
514
+ PUT /api/v3/runtime/config {retrieval:{top_k,use_cross_encoder},
515
+ injection:{enabled,core_block_enabled,
516
+ core_block_max_facts}}
517
+ ═══════════════════════════════════════════════════════════ */
518
+ function buildRuntime() {
519
+ var topkWrap = makeRange('rt-topk', 1, 200, 1, 20);
520
+ var topkInp = topkWrap.querySelector('input');
521
+ var xencSw = makeSwitch('rt-xenc', true);
522
+ var injSw = makeSwitch('rt-inj', true);
523
+ var cbSw = makeSwitch('rt-cb', true);
524
+ var cbWrap = makeRange('rt-cbmax', 0, 50, 1, 5);
525
+ var cbInp = cbWrap.querySelector('input');
526
+ var st = makeSt('rt');
527
+ function save() {
528
+ authPut('/api/v3/runtime/config', {
529
+ retrieval: {
530
+ top_k: Number(topkInp.value),
531
+ use_cross_encoder: xencSw.classList.contains('on')
532
+ },
533
+ injection: {
534
+ enabled: injSw.classList.contains('on'),
535
+ core_block_enabled: cbSw.classList.contains('on'),
536
+ core_block_max_facts: Number(cbInp.value)
537
+ }
538
+ }).then(function (r) { return r.json(); })
539
+ .then(function (d) {
540
+ setSt('rt', d.success !== false ? 'Applied' : esc(d.error || 'Error'), d.success !== false);
541
+ if (d.success !== false) loadRuntime();
542
+ })
543
+ .catch(function () { setSt('rt', 'Error', false); toast('Save failed', true); });
544
+ }
545
+ // Ranges save on release ('change'), not each drag tick ('input').
546
+ topkInp.addEventListener('change', save);
547
+ cbInp.addEventListener('change', save);
548
+ xencSw.addEventListener('od-toggle', save);
549
+ injSw.addEventListener('od-toggle', save);
550
+ cbSw.addEventListener('od-toggle', save);
551
+ return makeGrp('Recall & Memory Behaviour (auto-saves, no restart)', 'operations', [
552
+ makeRow('Recall depth', 'Memories each recall considers — higher is more thorough, a little slower', 'retrieval.top_k', topkWrap),
553
+ makeRow('Reranker', 'Re-scores results for quality (uses ~200MB RAM, adds ~200ms)', 'retrieval.use_cross_encoder', xencSw),
554
+ makeRow('Inject memories into prompts', 'Master switch for automatic memory injection', 'injection.enabled', injSw),
555
+ makeRow('Core-memory block', 'Prepend your most important facts to every prompt', 'injection.core_block_enabled', cbSw),
556
+ makeRow('Core-block size', 'Max facts in the core-memory block', 'injection.core_block_max_facts', cbWrap),
557
+ makeRow('Status', '', '', st)
558
+ ]);
559
+ }
560
+ function loadRuntime() {
561
+ fetch('/api/v3/runtime/config').then(function (r) { return r.ok ? r.json() : null; })
562
+ .then(function (d) {
563
+ if (!d || !d.config) return;
564
+ var rt = d.config.retrieval || {}, inj = d.config.injection || {};
565
+ function setRange(id, val) {
566
+ var i = q(id); if (i == null) return;
567
+ i.value = val; var disp = q(id + '-disp'); if (disp) disp.textContent = val;
568
+ }
569
+ function setSw(id, on) {
570
+ var s = q(id); if (!s) return;
571
+ s.classList.toggle('on', !!on); s.setAttribute('aria-checked', String(!!on));
572
+ }
573
+ if (rt.top_k != null) setRange('rt-topk', rt.top_k);
574
+ setSw('rt-xenc', rt.use_cross_encoder);
575
+ setSw('rt-inj', inj.enabled);
576
+ setSw('rt-cb', inj.core_block_enabled);
577
+ if (inj.core_block_max_facts != null) setRange('rt-cbmax', inj.core_block_max_facts);
578
+ }).catch(function () {});
579
+ }
580
+
512
581
  /* ═══════════════════════════════════════════════════════════
513
582
  GROUP 5 — Auto-Capture
514
583
  ═══════════════════════════════════════════════════════════ */
@@ -1026,8 +1095,8 @@
1026
1095
  });
1027
1096
  }
1028
1097
  function loadAll() {
1029
- loadMode(); loadEmb(); loadStorage(); loadScope(); loadCapture();
1030
- loadRecall(); loadInvoke(); loadForgetting(); loadEvolution();
1098
+ loadMode(); loadEmb(); loadStorage(); loadScope(); loadRuntime();
1099
+ loadCapture(); loadRecall(); loadInvoke(); loadForgetting(); loadEvolution();
1031
1100
  loadBackup(); loadMesh(); loadTrust(); loadRateLimit(); loadDaemon();
1032
1101
  }
1033
1102
 
@@ -1062,7 +1131,7 @@
1062
1131
  hub.appendChild(sWrap);
1063
1132
 
1064
1133
  var frag = document.createDocumentFragment();
1065
- [buildMode(), buildEmb(), buildStorage(), buildScope(),
1134
+ [buildMode(), buildEmb(), buildStorage(), buildScope(), buildRuntime(),
1066
1135
  buildCapture(), buildRecall(), buildInvoke(), buildForgetting(),
1067
1136
  buildEvolution(), buildBackup(), buildMesh(), buildTrust(),
1068
1137
  buildRateLimit(), buildDaemon()