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
@@ -4,9 +4,9 @@
4
4
  // Renders the approved design into window.odRenderBrain(container).
5
5
  // ALL data from these confirmed live endpoints (no seed / no invented data):
6
6
  // GET /api/learning/status → ranking_phase, stats, engagement, tech_preferences,
7
- // workflow_patterns, source_scores
8
- // GET /api/behavioral/status → total_outcomes, outcome_breakdown, patterns[],
9
- // cross_project_transfers, recent_outcomes[]
7
+ // workflow_patterns, persisted source-quality posteriors
8
+ // GET /api/behavioral/status → reward_telemetry, explicit outcome counts,
9
+ // patterns[], transfers, recent_outcomes[]
10
10
  // GET /api/behavioral/assertions?category=skill_performance&limit=50 → assertions[]
11
11
  // GET /api/behavioral/tool-events?limit=500 → events[].{created_at,tool_name,…}
12
12
  //
@@ -189,11 +189,21 @@
189
189
  var stats = (learning && learning.stats) || {};
190
190
  var eng = (learning && learning.engagement) || {};
191
191
  var beh = behavioral || {};
192
- var ML_GATE = 2000;
193
- var signals = Number(stats.feedback_count || stats.total || 0);
194
- var pct = Math.min(100, Math.round(signals / ML_GATE * 100));
195
- var phase = (learning && learning.ranking_phase) || 'cold_start';
196
- var modelActive = phase === 'ml_model';
192
+ var ranker = (learning && learning.ranker_phase) || {};
193
+ var gates = ranker.gates || {};
194
+ var ruleGate = Math.max(1, Number(gates.rule_based_min_signals || 1));
195
+ var mlGate = Math.max(ruleGate, Number(gates.ml_model_min_signals || ruleGate));
196
+ var signals = Number(ranker.signals != null
197
+ ? ranker.signals : stats.ranker_signal_count || 0);
198
+ var pct = Math.min(100, Math.round(signals / mlGate * 100));
199
+ var phase = ranker.key || (learning && learning.ranking_phase) || 'baseline';
200
+ var phaseNumber = Number(ranker.phase || 1);
201
+ var modelActive = Boolean(ranker.model_active);
202
+ var phaseDelta = modelActive
203
+ ? 'Verified active model'
204
+ : signals < mlGate
205
+ ? fmtNum(mlGate - signals) + ' to ML data gate'
206
+ : 'ML data gate met · verified model required';
197
207
  var healthStatus = (eng.health_status || 'INACTIVE').toUpperCase();
198
208
  var healthColor = healthStatus === 'HEALTHY' ? 'var(--ok)'
199
209
  : healthStatus === 'ACTIVE' ? 'var(--cyan)' : undefined;
@@ -203,7 +213,7 @@
203
213
  var strip = EL('div', { className: 'kpi-strip', style: 'margin-bottom:16px' });
204
214
  // Ranking phase: text label → isNumeric=false (font-size:24px to match design)
205
215
  strip.appendChild(kpiCard('skill', 'Ranking phase', phaseLabel(phase),
206
- '▲ ' + fmtNum(ML_GATE - signals) + ' to ML Model phase', !modelActive, undefined, false));
216
+ phaseDelta, modelActive, undefined, false));
207
217
  // Feedback signals: numeric → isNumeric=true
208
218
  strip.appendChild(kpiCard('optimize', 'Feedback signals', fmtNum(signals),
209
219
  '▲ ' + fmtNum(stats.unique_queries || 0) + ' unique queries', true, undefined, true));
@@ -230,7 +240,7 @@
230
240
  var pmeta = EL('div', {
231
241
  style: 'display:flex;justify-content:space-between;font-size:12px;color:var(--fg-2);margin-bottom:8px',
232
242
  });
233
- pmeta.appendChild(EL('span', { text: fmtNum(signals) + ' / ' + fmtNum(ML_GATE) + ' signals' }));
243
+ pmeta.appendChild(EL('span', { text: fmtNum(signals) + ' / ' + fmtNum(mlGate) + ' signals' }));
234
244
  pmeta.appendChild(EL('span', { className: 'num', text: pct + '%' }));
235
245
  pb.appendChild(pmeta);
236
246
  pb.appendChild(meter(pct));
@@ -238,9 +248,24 @@
238
248
  style: 'display:flex;justify-content:space-between;margin-top:18px;gap:12px',
239
249
  });
240
250
  [
241
- { t: 'Baseline', d: '0–20 signals', done: true, now: false },
242
- { t: 'Rule-Based', d: '20–200+ · active', done: false, now: !modelActive },
243
- { t: 'ML Model', d: '2000+ · ' + (modelActive ? 'active' : 'training set'), done: false, now: modelActive },
251
+ {
252
+ t: 'Baseline',
253
+ d: '0–' + Math.max(0, ruleGate - 1) + ' signals',
254
+ done: phaseNumber > 1,
255
+ now: phaseNumber === 1,
256
+ },
257
+ {
258
+ t: 'Rule-Based',
259
+ d: fmtNum(ruleGate) + '–' + fmtNum(Math.max(ruleGate, mlGate - 1)) + ' signals',
260
+ done: phaseNumber > 2,
261
+ now: phaseNumber === 2,
262
+ },
263
+ {
264
+ t: 'ML Model',
265
+ d: fmtNum(mlGate) + '+ · ' + (modelActive ? 'verified active' : 'verified model required'),
266
+ done: false,
267
+ now: phaseNumber === 3 && modelActive,
268
+ },
244
269
  ].forEach(function (phI) {
245
270
  var el = EL('div', {
246
271
  className: 'phase' + (phI.done ? ' done' : '') + (phI.now ? ' now' : ''),
@@ -264,6 +289,7 @@
264
289
  ['Learning DB size', fmtKB(stats.db_size_kb)],
265
290
  ['Patterns learned', String(pCount)],
266
291
  ['Models trained', String(stats.models_trained || 0)],
292
+ ['Verified active models', String(stats.models_active_verified || 0)],
267
293
  ['Sources tracked', String(stats.tracked_sources || 0)],
268
294
  ].forEach(function (row) {
269
295
  var r = EL('div', { className: 'list-row' });
@@ -282,11 +308,11 @@
282
308
  grid.appendChild(priv);
283
309
  sec.appendChild(grid);
284
310
 
285
- // Heatmap card (real data from tool-events)
311
+ // Activity heatmap (tool events are activity, never reward labels)
286
312
  var hmc = EL('div', { className: 'card', style: 'margin-top:16px' });
287
313
  var hmh = EL('div', { className: 'card-head' });
288
- hmh.appendChild(EL('h3', { text: "How I'm getting smarter" }));
289
- hmh.appendChild(EL('span', { className: 'sub', text: 'reward signal · last 26 weeks' }));
314
+ hmh.appendChild(EL('h3', { text: 'Memory activity' }));
315
+ hmh.appendChild(EL('span', { className: 'sub', text: 'tool events · last 26 weeks' }));
290
316
  hmh.appendChild(EL('div', { className: 'spacer' }));
291
317
  hmh.appendChild(heatLegend());
292
318
  hmc.appendChild(hmh);
@@ -302,21 +328,30 @@
302
328
  // ======================================================================
303
329
  // Tab: REWARD SIGNAL
304
330
  // ======================================================================
305
- function buildReward(behavioral, dateMap) {
331
+ function buildReward(behavioral) {
306
332
  var sec = EL('section', { className: 'tabpane', 'data-p': 'reward' });
307
333
  var beh = behavioral || {};
334
+ var reward = beh.reward_telemetry || {};
335
+ var timeline = reward.timeline || [];
336
+ var rewardDateMap = {};
337
+ timeline.forEach(function (point) {
338
+ if (point && point.date) rewardDateMap[String(point.date)] = Number(point.count || 0);
339
+ });
308
340
 
309
341
  // Density heatmap
310
342
  var hmcr = EL('div', { className: 'card', style: 'margin-bottom:16px' });
311
343
  var hmhr = EL('div', { className: 'card-head' });
312
344
  hmhr.appendChild(EL('h3', { text: 'Reward signal density' }));
313
- hmhr.appendChild(EL('span', { className: 'sub', text: 'positive engagement per day · last 26 weeks' }));
345
+ hmhr.appendChild(EL('span', {
346
+ className: 'sub',
347
+ text: 'settled numeric labels per day · last ' + Number(reward.window_days || 182) + ' days',
348
+ }));
314
349
  hmhr.appendChild(EL('div', { className: 'spacer' }));
315
350
  hmhr.appendChild(heatLegend());
316
351
  hmcr.appendChild(hmhr);
317
352
  var hmbr = EL('div', { className: 'card-pad', style: 'overflow-x:auto' });
318
353
  var hmElR = EL('div', { className: 'heatmap', id: 'od-brain-heat2' });
319
- buildHeatmap(hmElR, dateMap, 26);
354
+ buildHeatmap(hmElR, rewardDateMap, 26);
320
355
  hmbr.appendChild(hmElR);
321
356
  hmcr.appendChild(hmbr);
322
357
  sec.appendChild(hmcr);
@@ -324,21 +359,28 @@
324
359
  // 2-column: sparkline + outcome mix
325
360
  var grid = EL('div', { className: 'grid', style: 'grid-template-columns:1fr 1fr;align-items:start' });
326
361
 
327
- // Feedback sparkline (last 30 days from dateMap)
362
+ // Average settled reward and real daily series
328
363
  var fbCard = EL('div', { className: 'card' });
329
364
  var fbH = EL('div', { className: 'card-head' });
330
- fbH.appendChild(EL('h3', { text: 'Feedback signals over time' }));
331
- fbH.appendChild(EL('span', { className: 'sub', text: 'cumulative' }));
365
+ fbH.appendChild(EL('h3', { text: 'Average settled reward' }));
366
+ fbH.appendChild(EL('span', {
367
+ className: 'sub',
368
+ text: fmtNum(reward.count || 0) + ' finalized labels',
369
+ }));
332
370
  fbCard.appendChild(fbH);
333
371
  var fbB = EL('div', { className: 'card-pad' });
334
- var fbSp = EL('div', { id: 'od-brain-sp-fb' });
335
- var today = new Date(); today.setHours(0, 0, 0, 0);
336
- var sparkVals = [];
337
- for (var si = 29; si >= 0; si--) {
338
- var sd = new Date(today); sd.setDate(today.getDate() - si);
339
- sparkVals.push((dateMap && dateMap[sd.toISOString().slice(0, 10)]) || 0);
372
+ if (reward.average != null) {
373
+ fbB.appendChild(EL('div', {
374
+ className: 'value num',
375
+ style: 'font-size:30px;margin-bottom:12px',
376
+ text: Number(reward.average).toFixed(3),
377
+ }));
340
378
  }
341
- var hasSparkData = sparkVals.some(function (v) { return v > 0; });
379
+ var fbSp = EL('div', { id: 'od-brain-sp-fb' });
380
+ var sparkVals = timeline.slice(-30).map(function (point) {
381
+ return Number(point.average || 0);
382
+ });
383
+ var hasSparkData = sparkVals.length > 0;
342
384
  if (hasSparkData && typeof window.slmSpark === 'function') {
343
385
  fbSp.innerHTML = window.slmSpark(sparkVals, { w: 600, h: 150, color: 'var(--violet)' });
344
386
  var sv1 = fbSp.querySelector('svg'); if (sv1) sv1.style.height = '150px';
@@ -346,33 +388,33 @@
346
388
  fbSp.appendChild(EL('p', {
347
389
  className: 'muted',
348
390
  style: 'padding:32px;text-align:center;font-size:13px',
349
- text: 'No signal history in the last 30 days.',
391
+ text: 'No settled reward history is available yet.',
350
392
  }));
351
393
  }
352
394
  fbB.appendChild(fbSp);
353
395
  fbCard.appendChild(fbB);
354
396
  grid.appendChild(fbCard);
355
397
 
356
- // Outcome mix
398
+ // Settled reward distribution
357
399
  var outCard = EL('div', { className: 'card' });
358
400
  var outH = EL('div', { className: 'card-head' });
359
- outH.appendChild(EL('h3', { text: 'Outcome mix' }));
360
- outH.appendChild(EL('span', { className: 'sub', text: 'heuristic reward attribution' }));
401
+ outH.appendChild(EL('h3', { text: 'Reward distribution' }));
402
+ outH.appendChild(EL('span', { className: 'sub', text: 'engagement-derived settled labels' }));
361
403
  outCard.appendChild(outH);
362
404
  var outB = EL('div', { className: 'card-pad', id: 'od-brain-outcomes' });
363
- var total = beh.total_outcomes || 0;
364
- var bd = beh.outcome_breakdown || {};
405
+ var total = Number(reward.count || 0);
406
+ var bd = reward.distribution || {};
365
407
  if (total === 0) {
366
408
  outB.appendChild(EL('p', {
367
409
  className: 'muted',
368
410
  style: 'padding:16px;text-align:center;font-size:13px',
369
- text: 'No outcome data yet. Report outcomes in your sessions to train the ranker.',
411
+ text: 'No settled reward labels yet. Recall engagement will populate this view.',
370
412
  }));
371
413
  } else {
372
414
  [
373
- ['Productive (success)', bd.success || 0, 'var(--ok)'],
374
- ['Session continued (partial)', bd.partial || 0, 'var(--cyan)'],
375
- ['Failure signal', bd.failure || 0, 'var(--danger)'],
415
+ ['Positive (> 0.6)', bd.positive || 0, 'var(--ok)'],
416
+ ['Neutral (0.4–0.6)', bd.neutral || 0, 'var(--cyan)'],
417
+ ['Negative (< 0.4)', bd.negative || 0, 'var(--danger)'],
376
418
  ].forEach(function (r) {
377
419
  var p2 = Math.round(r[1] / total * 100);
378
420
  var rw = EL('div', { style: 'margin-bottom:13px' });
@@ -454,7 +496,7 @@
454
496
  wcb.appendChild(EL('p', {
455
497
  className: 'muted',
456
498
  style: 'padding:16px;text-align:center;font-size:13px',
457
- text: 'Workflow patterns emerge after 50+ recall sessions.',
499
+ text: 'No sequence or temporal patterns recorded yet. Interest signals are not shown as workflows.',
458
500
  }));
459
501
  } else {
460
502
  wfPats.slice(0, 3).forEach(function (wf) {
@@ -482,9 +524,7 @@
482
524
  xch.appendChild(EL('span', { className: 'sub', text: 'patterns reused across projects' }));
483
525
  xc.appendChild(xch);
484
526
  var xcb = EL('div', { className: 'card-pad' });
485
- var xPats = (beh.patterns || []).filter(function (p) {
486
- return p.pattern_type === 'cross_project' || p.is_transferable;
487
- });
527
+ var xPats = beh.cross_project_patterns || [];
488
528
  if (xPats.length === 0) {
489
529
  xcb.appendChild(EL('p', {
490
530
  className: 'muted',
@@ -535,9 +575,9 @@
535
575
  item.appendChild(bdg);
536
576
  item.appendChild(EL('span', {
537
577
  style: 'flex:1;font-size:13px',
538
- text: String(r.context || r.description || ''),
578
+ text: String(r.action_type || 'other'),
539
579
  }));
540
- item.appendChild(EL('time', { text: String(r.time_ago || r.created_at || '') }));
580
+ item.appendChild(EL('time', { text: String(r.timestamp || '') }));
541
581
  rcb.appendChild(item);
542
582
  });
543
583
  }
@@ -614,17 +654,14 @@
614
654
  var card = EL('div', { className: 'card' });
615
655
  var ch = EL('div', { className: 'card-head' });
616
656
  ch.appendChild(EL('h3', { text: 'Source quality' }));
617
- ch.appendChild(EL('span', { className: 'sub', text: 'per-tool retrieval scoring · 0.0–1.0' }));
657
+ ch.appendChild(EL('span', { className: 'sub', text: 'persisted source-outcome posterior · 0.0–1.0' }));
618
658
  card.appendChild(ch);
619
659
  var cb = EL('div', { className: 'card-pad' });
620
660
  if (entries.length === 0) {
621
- // TODO: GET /api/learning/status → source_scores is empty until ML ranking pipeline
622
- // processes sufficient interactions and runs consolidation.
623
661
  cb.appendChild(EL('p', {
624
662
  className: 'muted',
625
663
  style: 'padding:16px;text-align:center;font-size:13px',
626
- text: 'Source quality scores are computed by the ML ranking pipeline. ' +
627
- 'No data yet — scores appear after consolidation runs.',
664
+ text: 'No source-quality observations yet. Recall hits alone do not establish source quality.',
628
665
  }));
629
666
  } else {
630
667
  entries.forEach(function (k) {
@@ -742,7 +779,7 @@
742
779
  head,
743
780
  buildTabRow(pCount),
744
781
  buildOverview(learning, behavioral, dateMap),
745
- buildReward(behavioral, dateMap),
782
+ buildReward(behavioral),
746
783
  buildBehaviour(learning, behavioral),
747
784
  buildClients(dateMap),
748
785
  buildSourceQuality(learning)
@@ -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
+ })();
@@ -127,15 +127,20 @@
127
127
  typeFilter: 'all',
128
128
  search: '',
129
129
  selected: null, // entity name
130
+ requestSeq: 0,
130
131
  };
132
+ var _searchTimer = null;
131
133
 
132
134
  // ── Main entry ─────────────────────────────────────────────────────────────
133
135
 
134
136
  function render(container) {
135
137
  if (!container) return;
138
+ clearTimeout(_searchTimer);
136
139
  _injectCSS();
137
140
  var id = 'od-ent-' + Math.random().toString(36).slice(2, 8);
138
- _st = Object.assign({}, _st, { rootId: id, page: 0, typeFilter: 'all', selected: null });
141
+ _st = Object.assign({}, _st, {
142
+ rootId: id, page: 0, typeFilter: 'all', search: '', selected: null, requestSeq: 0,
143
+ });
139
144
  container.innerHTML = _scaffold(id);
140
145
  _wire(container, id);
141
146
  _loadList(id);
@@ -231,6 +236,10 @@
231
236
  function _loadList(id) {
232
237
  var offset = _st.page * _st.pageSize;
233
238
  var url = '/api/entity/list?limit=' + _st.pageSize + '&offset=' + offset;
239
+ if (_st.typeFilter !== 'all') url += '&type=' + encodeURIComponent(_st.typeFilter);
240
+ if (_st.search) url += '&search=' + encodeURIComponent(_st.search);
241
+ var requestSeq = _st.requestSeq + 1;
242
+ _st = Object.assign({}, _st, { requestSeq: requestSeq });
234
243
  var listEl = document.getElementById(id + '-list');
235
244
  if (listEl) {
236
245
  listEl.innerHTML = '<div style="padding:40px;text-align:center;' +
@@ -243,6 +252,7 @@
243
252
  return r.json();
244
253
  })
245
254
  .then(function (d) {
255
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
246
256
  _st = Object.assign({}, _st, {
247
257
  entities: d.entities || [],
248
258
  total: d.total || 0,
@@ -255,6 +265,7 @@
255
265
  }
256
266
  })
257
267
  .catch(function (err) {
268
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
258
269
  var el = document.getElementById(id + '-list');
259
270
  if (el) {
260
271
  el.innerHTML = '<div style="padding:24px;text-align:center;' +
@@ -264,21 +275,10 @@
264
275
  }
265
276
 
266
277
  function _renderList(id) {
267
- var q = _st.search.toLowerCase();
268
- var tf = _st.typeFilter;
269
- var all = _st.entities;
270
-
271
- var filtered = all.filter(function (e) {
272
- var typeOk = tf === 'all' || e.type === tf;
273
- var searchOk = !q ||
274
- (e.name || '').toLowerCase().indexOf(q) >= 0 ||
275
- (e.type || '').toLowerCase().indexOf(q) >= 0 ||
276
- (e.summary_preview || '').toLowerCase().indexOf(q) >= 0;
277
- return typeOk && searchOk;
278
- });
278
+ var filtered = _st.entities;
279
279
 
280
280
  var cntEl = document.getElementById(id + '-count');
281
- if (cntEl) cntEl.textContent = filtered.length + ' of ' + _st.total.toLocaleString();
281
+ if (cntEl) cntEl.textContent = _st.total.toLocaleString();
282
282
 
283
283
  var listEl = document.getElementById(id + '-list');
284
284
  if (!listEl) return;
@@ -286,7 +286,7 @@
286
286
  if (filtered.length === 0) {
287
287
  listEl.innerHTML = '<div style="padding:40px;text-align:center;' +
288
288
  'color:var(--fg-2);font-size:13px">' +
289
- (q || tf !== 'all' ? 'No entities match this filter.' :
289
+ (_st.search || _st.typeFilter !== 'all' ? 'No entities match this filter.' :
290
290
  'No entities found. Entity extraction runs during consolidation.') +
291
291
  '</div>';
292
292
  return;
@@ -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,19 +525,48 @@
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
- _st = Object.assign({}, _st, { typeFilter: el.dataset.type, search: '' });
525
- // Clear the search input DOM value so it stays in sync with state
526
- var searchEl = document.getElementById(id + '-search');
527
- if (searchEl) searchEl.value = '';
567
+ _st = Object.assign({}, _st, { typeFilter: el.dataset.type, page: 0 });
528
568
  _refreshTypeChips(id, el.dataset.type);
529
- _renderList(id);
569
+ _loadList(id);
530
570
  return;
531
571
  }
532
572
  if (act === 'select-entity') {
@@ -546,8 +586,11 @@
546
586
 
547
587
  container.addEventListener('input', function (e) {
548
588
  if (e.target.dataset.odAct === 'ent-search') {
549
- _st = Object.assign({}, _st, { search: e.target.value.trim() });
550
- _renderList(id);
589
+ _st = Object.assign({}, _st, { search: e.target.value.trim(), page: 0 });
590
+ clearTimeout(_searchTimer);
591
+ _searchTimer = setTimeout(function () {
592
+ if (_st.rootId === id) _loadList(id);
593
+ }, 250);
551
594
  }
552
595
  });
553
596
  }