superlocalmemory 3.8.0 → 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.
Files changed (134) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  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 +2 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +494 -9
  35. package/src/superlocalmemory/cli/daemon.py +7 -0
  36. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  37. package/src/superlocalmemory/cli/main.py +72 -7
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  39. package/src/superlocalmemory/cli/version_banner.py +17 -3
  40. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  41. package/src/superlocalmemory/core/component_healer.py +144 -0
  42. package/src/superlocalmemory/core/component_registry.py +487 -0
  43. package/src/superlocalmemory/core/config.py +21 -0
  44. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  45. package/src/superlocalmemory/core/embeddings.py +132 -45
  46. package/src/superlocalmemory/core/engine.py +29 -22
  47. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  48. package/src/superlocalmemory/core/ingestion_command.py +154 -25
  49. package/src/superlocalmemory/core/injection.py +12 -7
  50. package/src/superlocalmemory/core/maintenance.py +43 -0
  51. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
  52. package/src/superlocalmemory/core/recall_pipeline.py +42 -4
  53. package/src/superlocalmemory/core/store_pipeline.py +195 -20
  54. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  55. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  56. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  57. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  58. package/src/superlocalmemory/learning/reward.py +50 -0
  59. package/src/superlocalmemory/learning/source_quality.py +523 -1
  60. package/src/superlocalmemory/loops/ledger.py +25 -5
  61. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  62. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  63. package/src/superlocalmemory/mcp/server.py +11 -30
  64. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  65. package/src/superlocalmemory/mcp/tools_core.py +21 -5
  66. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  67. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  68. package/src/superlocalmemory/retrieval/engine.py +53 -21
  69. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  70. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  71. package/src/superlocalmemory/server/config_file.py +90 -0
  72. package/src/superlocalmemory/server/origin.py +50 -0
  73. package/src/superlocalmemory/server/routes/backup.py +293 -70
  74. package/src/superlocalmemory/server/routes/behavioral.py +342 -61
  75. package/src/superlocalmemory/server/routes/brain.py +57 -16
  76. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  77. package/src/superlocalmemory/server/routes/entity.py +100 -23
  78. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  79. package/src/superlocalmemory/server/routes/learning.py +286 -105
  80. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  81. package/src/superlocalmemory/server/routes/memories.py +8 -3
  82. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  83. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  84. package/src/superlocalmemory/server/routes/stats.py +93 -155
  85. package/src/superlocalmemory/server/routes/token.py +3 -13
  86. package/src/superlocalmemory/server/routes/v3_api.py +184 -20
  87. package/src/superlocalmemory/server/unified_daemon.py +732 -41
  88. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  89. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  90. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  91. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  92. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  93. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  94. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  95. package/src/superlocalmemory/storage/schema.py +49 -1
  96. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  97. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  98. package/src/superlocalmemory/ui/index.html +6 -8
  99. package/src/superlocalmemory/ui/js/core.js +52 -9
  100. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  101. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  102. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  103. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  104. package/src/superlocalmemory/ui/js/od-entities.js +65 -22
  105. package/src/superlocalmemory/ui/js/od-graph.js +46 -4
  106. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  107. package/src/superlocalmemory/ui/js/od-memories.js +84 -5
  108. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  109. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  110. package/src/superlocalmemory/ui/js/od-settings.js +186 -63
  111. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  112. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  113. package/src/superlocalmemory/ui/js/settings.js +15 -1
  114. package/plugin-src/.mcp.json +0 -12
  115. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  116. package/plugin-src/agents/slm-loop-runner.md +0 -71
  117. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  118. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  119. package/plugin-src/commands/slm-loop.md +0 -31
  120. package/plugin-src/hooks/.gitkeep +0 -0
  121. package/plugin-src/hooks/hooks.json +0 -102
  122. package/plugin-src/manifest.json +0 -30
  123. package/plugin-src/requirements.txt +0 -1
  124. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  125. package/plugin-src/scripts/ensure-venv.bat +0 -122
  126. package/plugin-src/scripts/ensure-venv.sh +0 -105
  127. package/plugin-src/scripts/slm-launch +0 -62
  128. package/plugin-src/scripts/slm-launch.bat +0 -23
  129. package/plugin-src/settings.json +0 -25
  130. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  131. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  132. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  133. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  134. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -16,6 +16,9 @@
16
16
  var GRAPH_URL = '/api/graph';
17
17
  var RECALL_URL = '/api/search'; /* POST {query, limit} -> {results:[...]} */
18
18
  var MAX_NODES = 120; /* default budget; slider range 20-2000 */
19
+ /* Keep force-layout work bounded even when a user asks to render "All". */
20
+ var PHYSICS_MAX_NODES = 160;
21
+ var PRE_SETTLE_TICKS = 24;
19
22
 
20
23
  /* ---- 16-colour categorical palette (Tableau-inspired, accessible) ---- */
21
24
  var CAT_PALETTE = [
@@ -41,7 +44,7 @@
41
44
  var raf = null, running = false, fitFrames = 0;
42
45
  /* settle-freeze: stop the rAF loop once kinetic energy drops below SETTLE_KE */
43
46
  var frames = 0, _ke = 0;
44
- var SETTLE_MIN = 60, SETTLE_KE = 0.2, SETTLE_MAX_FRAMES = 600;
47
+ var SETTLE_MIN = 30, SETTLE_KE = 0.2, SETTLE_MAX_FRAMES = 180;
45
48
  var NODES = [], LINKS = [], idx = {};
46
49
  var tierVisible = { 1: true, 2: true, 3: true }; /* independent tier toggles */
47
50
  var scale = 1, ox = 0, oy = 0, dpr = Math.max(1, window.devicePixelRatio || 1);
@@ -90,6 +93,31 @@
90
93
  '<div class="inspector-empty"><div style="font-size:34px;margin-bottom:8px">&#9671;</div>' +
91
94
  'Loading your knowledge graph…</div>' +
92
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>' +
93
121
  '<div class="ask">' +
94
122
  '<div class="ask-head"><span data-ic="brain"></span> Ask your memory</div>' +
95
123
  '<div class="ask-log" id="odg-log">' +
@@ -223,7 +251,11 @@
223
251
 
224
252
  /* ---------------- physics (ported from design) ---------------- */
225
253
  function tick() {
226
- var vis = NODES.filter(visible), i, j;
254
+ /* Force repulsion is O(n^2). Render every requested node, but only
255
+ simulate a stable bounded subset so the dashboard remains interactive
256
+ for existing installs with very large graphs. */
257
+ var vis = NODES.filter(visible).slice(0, PHYSICS_MAX_NODES), simulated = {}, i, j;
258
+ vis.forEach(function (n) { simulated[n.id] = true; });
227
259
  for (i = 0; i < vis.length; i++) {
228
260
  var a = vis[i];
229
261
  for (j = i + 1; j < vis.length; j++) {
@@ -234,7 +266,7 @@
234
266
  }
235
267
  }
236
268
  LINKS.forEach(function (l) {
237
- if (!visLink(l)) return;
269
+ if (!visLink(l) || !simulated[l.s] || !simulated[l.t]) return;
238
270
  var a = idx[l.s], b = idx[l.t];
239
271
  var dx = b.x - a.x, dy = b.y - a.y, d = Math.sqrt(dx * dx + dy * dy) + 0.01;
240
272
  var rest = (a.type === 'community' || b.type === 'community') ? 150 : 100;
@@ -495,6 +527,16 @@
495
527
  q('#odg-send').onclick = sendAsk;
496
528
  q('#odg-ask').addEventListener('keydown', function (e) { if (e.key === 'Enter') sendAsk(); });
497
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
+
498
540
  cv.addEventListener('mousedown', function (e) {
499
541
  var r = cv.getBoundingClientRect(), n = nodeAt(e.clientX - r.left, e.clientY - r.top);
500
542
  if (n) { dragNode = n; selected = n; renderInsp(n); wake(); } else { panning = true; }
@@ -557,7 +599,7 @@
557
599
  running = false; draw(); updateCount(); updateLegend(); return;
558
600
  }
559
601
  readPalette(); resize(); updateCount(); updateLegend(); selected = null; setInspectorEmpty();
560
- for (var i = 0; i < 120; i++) tick(); /* pre-settle so it doesn't open as a clump */
602
+ for (var i = 0; i < PRE_SETTLE_TICKS; i++) tick(); /* bounded pre-settle keeps navigation responsive */
561
603
  fitFrames = 0; frames = 0;
562
604
  if (!running) { running = true; loop(); } else { wake(); }
563
605
  }).catch(function (err) {
@@ -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 =
@@ -106,6 +106,8 @@
106
106
  catCounts: {}, // pre-fetched per-category totals
107
107
  tlRange: '30d',
108
108
  cluLoaded: false, // reset in render() to avoid stale state on re-entry
109
+ searchQ: null,
110
+ requestSeq: 0,
109
111
  };
110
112
 
111
113
  // Known categories from the daemon (pre-fetched at render time)
@@ -119,7 +121,7 @@
119
121
  var id = 'od-mem-' + Math.random().toString(36).slice(2, 8);
120
122
  _st = Object.assign({}, _st, {
121
123
  rootId: id, page: 0, category: null, sort: 'created',
122
- memories: [], catCounts: {}, cluLoaded: false,
124
+ memories: [], catCounts: {}, cluLoaded: false, searchQ: null, requestSeq: 0,
123
125
  });
124
126
  // Clear per-id timeline cache for this render instance
125
127
  _tlLoaded = {};
@@ -145,6 +147,9 @@
145
147
  '<button class="tab" data-od-act="tab" data-tab="timeline">Creation timeline</button>' +
146
148
  '<button class="tab" data-od-act="tab" data-tab="clusters">' +
147
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>' +
148
153
  '</div>' +
149
154
  '<div class="tabpane active" id="' + id + '-pane-all">' + _allScaffold(id) + '</div>' +
150
155
  '<div class="tabpane" id="' + id + '-pane-timeline">' + _tlScaffold(id) + '</div>' +
@@ -153,6 +158,40 @@
153
158
  _loading('Loading clusters…') +
154
159
  '</div>' +
155
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>' +
156
195
  '</div>'
157
196
  );
158
197
  }
@@ -306,6 +345,8 @@
306
345
  if (_st.scopeView && _st.scopeView !== 'mine') {
307
346
  url += '&scope=' + encodeURIComponent(_st.scopeView);
308
347
  }
348
+ var requestSeq = _st.requestSeq + 1;
349
+ _st = Object.assign({}, _st, { requestSeq: requestSeq });
309
350
  var wrap = document.getElementById(id + '-tbl-wrap');
310
351
  if (wrap) wrap.innerHTML = _loading('Loading memories…');
311
352
 
@@ -315,10 +356,12 @@
315
356
  return r.json();
316
357
  })
317
358
  .then(function (d) {
359
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
318
360
  _st = Object.assign({}, _st, { memories: d.memories || [], total: d.total || 0 });
319
361
  _renderTable(id, d);
320
362
  })
321
363
  .catch(function (err) {
364
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
322
365
  var w = document.getElementById(id + '-tbl-wrap');
323
366
  if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center">' +
324
367
  'Failed to load memories: ' + _esc(err.message) + '</div>';
@@ -442,22 +485,38 @@
442
485
  var _searchTimer = null;
443
486
  function _doSearch(id, q) {
444
487
  clearTimeout(_searchTimer);
488
+ var requestSeq = _st.requestSeq + 1;
489
+ // /api/search is a full-corpus semantic query. It cannot truthfully retain
490
+ // the list endpoint's category/scope selection, so clear those controls
491
+ // before the request rather than leaving the table and chips disagreeing.
492
+ _st = Object.assign({}, _st, {
493
+ searchQ: q || null,
494
+ category: q ? null : _st.category,
495
+ scopeView: q ? 'mine' : _st.scopeView,
496
+ page: 0,
497
+ requestSeq: requestSeq,
498
+ });
499
+ if (q) _rebuildCatBar(id);
445
500
  _searchTimer = setTimeout(function () {
446
- _st = Object.assign({}, _st, { searchQ: q || null });
501
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
447
502
  if (!q) { _loadMem(id); return; }
448
503
  var wrap = document.getElementById(id + '-tbl-wrap');
449
504
  if (wrap) wrap.innerHTML = _loading('Searching…');
450
505
  fetch('/api/search', { method: 'POST', credentials: 'same-origin',
451
506
  headers: { 'Content-Type': 'application/json' },
507
+ slmInvalidatesCache: false,
508
+ slmRequiresWriteAuth: false,
452
509
  body: JSON.stringify({ query: q, limit: 50, window: _st.window || '' })
453
510
  }).then(function (r) {
454
511
  if (!r.ok) throw new Error('HTTP ' + r.status);
455
512
  return r.json();
456
513
  }).then(function (d) {
514
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
457
515
  var mems = (d.results || []).map(_normSearch);
458
516
  _st = Object.assign({}, _st, { memories: mems, total: mems.length });
459
517
  _renderTable(id, { memories: mems, total: mems.length, limit: mems.length, offset: 0 });
460
518
  }).catch(function (err) {
519
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
461
520
  var w = document.getElementById(id + '-tbl-wrap');
462
521
  if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center;' +
463
522
  'padding:24px">Search failed: ' + _esc(err.message) + '</div>';
@@ -760,7 +819,13 @@
760
819
  if (bar) bar.querySelectorAll('[data-od-act="cat"]').forEach(function (c) {
761
820
  c.classList.toggle('on', c.dataset.cat === el.dataset.cat);
762
821
  });
763
- _st = Object.assign({}, _st, { category: el.dataset.cat || null, page: 0 });
822
+ clearTimeout(_searchTimer);
823
+ _st = Object.assign({}, _st, {
824
+ category: el.dataset.cat || null, page: 0, searchQ: null,
825
+ requestSeq: _st.requestSeq + 1,
826
+ });
827
+ var searchInput = document.querySelector('#' + id + ' [data-od-act="search"]');
828
+ if (searchInput) searchInput.value = '';
764
829
  _loadMem(id);
765
830
  return;
766
831
  }
@@ -778,7 +843,13 @@
778
843
  if (sroot) sroot.querySelectorAll('[data-od-act="scope-view"]').forEach(function (b) {
779
844
  b.classList.toggle('active', b.dataset.scope === el.dataset.scope);
780
845
  });
781
- _st = Object.assign({}, _st, { scopeView: el.dataset.scope, page: 0 });
846
+ clearTimeout(_searchTimer);
847
+ _st = Object.assign({}, _st, {
848
+ scopeView: el.dataset.scope, page: 0, searchQ: null,
849
+ requestSeq: _st.requestSeq + 1,
850
+ });
851
+ var scopedSearchInput = document.querySelector('#' + id + ' [data-od-act="search"]');
852
+ if (scopedSearchInput) scopedSearchInput.value = '';
782
853
  _loadMem(id);
783
854
  return;
784
855
  }
@@ -791,7 +862,15 @@
791
862
  if (act === 'close-drawer') { _closeDrawer(); return; }
792
863
  if (act === 'pg') {
793
864
  var pg = parseInt(el.dataset.page, 10);
794
- if (!isNaN(pg)) { _st = Object.assign({}, _st, { page: pg }); _loadMem(id); }
865
+ if (!isNaN(pg)) {
866
+ clearTimeout(_searchTimer);
867
+ _st = Object.assign({}, _st, {
868
+ page: pg, searchQ: null, requestSeq: _st.requestSeq + 1,
869
+ });
870
+ var pagedSearchInput = document.querySelector('#' + id + ' [data-od-act="search"]');
871
+ if (pagedSearchInput) pagedSearchInput.value = '';
872
+ _loadMem(id);
873
+ }
795
874
  return;
796
875
  }
797
876
  if (act === 'expand-cluster') { _expandCluster(id, el.dataset.cid); return; }
@@ -7,8 +7,8 @@
7
7
  // Design-system CSS classes used throughout — no invented inline colours.
8
8
  //
9
9
  // Endpoints (all via meshFetch → X-Install-Token):
10
- // GET /mesh/status → {broker_up, peer_count, uptime_s}
11
- // GET /mesh/peers → {peers:[{peer_id, session_id, status, last_heartbeat,
10
+ // GET /mesh/status → {broker_up, remote_peer_count, local_session_count, uptime_s}
11
+ // GET /mesh/peers?view=all → {peers:[{peer_id, session_id, status, last_heartbeat,
12
12
  // summary, host, port, agent_type, project_path}]}
13
13
  // POST /mesh/send → body:{from_peer, to_peer, content, type} → {ok:true}
14
14
  //
@@ -76,6 +76,7 @@
76
76
  var _container = null;
77
77
  var _timer = null;
78
78
  var _observer = null; // MutationObserver — re-starts refresh on tab activate
79
+ var _requestSeq = 0;
79
80
 
80
81
  // ── HTML helpers ──────────────────────────────────────────────────────────
81
82
  // CRIT C3: every API-derived string MUST pass through this.
@@ -198,12 +199,15 @@
198
199
  return;
199
200
  }
200
201
  var up = data.broker_up === true ? 'Broker up' : 'Broker offline';
201
- var cnt = data.peer_count != null
202
- ? data.peer_count + ' peer' + (data.peer_count === 1 ? '' : 's')
202
+ var remoteCount = data.remote_peer_count != null ? data.remote_peer_count : data.peer_count;
203
+ var cnt = remoteCount != null
204
+ ? remoteCount + ' remote peer' + (remoteCount === 1 ? '' : 's')
203
205
  : '';
206
+ var localCount = data.local_session_count || 0;
204
207
  var upt = data.uptime_s != null ? 'uptime ' + fmtUptime(data.uptime_s) : '';
205
208
  var parts = [up];
206
209
  if (cnt) parts.push(cnt);
210
+ if (localCount) parts.push(localCount + ' local session' + (localCount === 1 ? '' : 's'));
207
211
  if (upt) parts.push(upt);
208
212
  el.textContent = 'Agents sharing this memory mesh on your machine and LAN. ' +
209
213
  parts.join(' · ') + '. Peers go stale after 5 min without a heartbeat, dead after 30.';
@@ -236,10 +240,10 @@
236
240
  'justify-content:center;margin-bottom:16px;color:var(--fg-3)">' +
237
241
  meshEmptyIcon() +
238
242
  '</div>' +
239
- '<h3 style="font-size:15px;margin-bottom:6px">No peers connected</h3>' +
243
+ '<h3 style="font-size:15px;margin-bottom:6px">No mesh sessions connected</h3>' +
240
244
  '<p style="font-size:12.5px;color:var(--fg-2);' +
241
245
  'max-width:30ch;line-height:1.5">' +
242
- 'Start another agent session. Peers register via the' +
246
+ 'Start another agent session or connect a trusted LAN peer. Sessions register via the' +
243
247
  ' <code>mesh_summary</code> MCP tool.' +
244
248
  '</p>' +
245
249
  '</div>';
@@ -253,6 +257,9 @@
253
257
  var pidFrag = escapeHtml((p.peer_id || '').slice(0, 12));
254
258
  var summary = escapeHtml(p.summary || 'No summary');
255
259
  var atype = escapeHtml(p.agent_type || 'unknown');
260
+ var isLocal = String(p.host || '').toLowerCase() === '127.0.0.1' ||
261
+ String(p.host || '').toLowerCase() === 'localhost' || String(p.host || '') === '::1';
262
+ var scopeLabel = isLocal ? 'local session' : 'remote peer';
256
263
  var ago = escapeHtml(timeAgo(p.last_heartbeat));
257
264
  var proj = escapeHtml(p.project_path || '~');
258
265
  var port = p.port ? ' · :' + p.port : '';
@@ -274,7 +281,7 @@
274
281
  '<div style="flex:1;min-width:0">' +
275
282
  '<h3 style="font-size:15px;white-space:nowrap;overflow:hidden;' +
276
283
  'text-overflow:ellipsis">' + name + '</h3>' +
277
- '<span class="mono dim" style="font-size:11px">' + pidFrag + '</span>' +
284
+ '<span class="mono dim" style="font-size:11px">' + pidFrag + ' · ' + scopeLabel + '</span>' +
278
285
  '</div>' +
279
286
  '<span class="badge ' + badgeCls(st) + '">' +
280
287
  '<span class="dot"></span>' + escapeHtml(st) +
@@ -424,15 +431,22 @@
424
431
 
425
432
  // ── Data loaders ──────────────────────────────────────────────────────────
426
433
  function loadAll() {
434
+ var requestSeq = _requestSeq + 1;
435
+ _requestSeq = requestSeq;
427
436
  meshFetch('/mesh/status')
428
437
  .then(function (r) { return r.ok ? r.json() : null; })
429
438
  .catch(function () { return null; })
430
- .then(renderPageDesc);
439
+ .then(function (data) {
440
+ if (_requestSeq === requestSeq) renderPageDesc(data);
441
+ });
431
442
 
432
- meshFetch('/mesh/peers')
443
+ // The broker distinguishes remote LAN peers from local agent sessions.
444
+ // Both are real mesh participants and must be visible to operators.
445
+ meshFetch('/mesh/peers?view=all')
433
446
  .then(function (r) { return r.ok ? r.json() : { peers: [] }; })
434
447
  .catch(function () { return { peers: [] }; })
435
448
  .then(function (d) {
449
+ if (_requestSeq !== requestSeq) return;
436
450
  _peers = Array.isArray(d && d.peers) ? d.peers : [];
437
451
  renderPeers(_peers);
438
452
  // If the currently selected peer has expired, deselect
@@ -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">' +