superlocalmemory 3.8.0 → 3.8.1

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 (118) hide show
  1. package/CHANGELOG.md +60 -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 +1 -1
  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 +1 -1
  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 +1 -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 +1 -1
  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/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/cli/commands.py +134 -7
  34. package/src/superlocalmemory/cli/daemon.py +7 -0
  35. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  36. package/src/superlocalmemory/cli/main.py +10 -4
  37. package/src/superlocalmemory/cli/version_banner.py +17 -3
  38. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  39. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  40. package/src/superlocalmemory/core/embeddings.py +119 -45
  41. package/src/superlocalmemory/core/engine.py +24 -21
  42. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  43. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  44. package/src/superlocalmemory/core/injection.py +12 -7
  45. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  46. package/src/superlocalmemory/core/recall_pipeline.py +5 -3
  47. package/src/superlocalmemory/core/store_pipeline.py +153 -20
  48. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  49. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  50. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  51. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  52. package/src/superlocalmemory/learning/reward.py +50 -0
  53. package/src/superlocalmemory/learning/source_quality.py +523 -1
  54. package/src/superlocalmemory/loops/ledger.py +25 -5
  55. package/src/superlocalmemory/mcp/server.py +11 -30
  56. package/src/superlocalmemory/mcp/tools_core.py +4 -3
  57. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  58. package/src/superlocalmemory/retrieval/engine.py +53 -21
  59. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  60. package/src/superlocalmemory/server/config_file.py +90 -0
  61. package/src/superlocalmemory/server/origin.py +50 -0
  62. package/src/superlocalmemory/server/routes/backup.py +293 -70
  63. package/src/superlocalmemory/server/routes/behavioral.py +336 -59
  64. package/src/superlocalmemory/server/routes/brain.py +57 -16
  65. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  66. package/src/superlocalmemory/server/routes/entity.py +100 -23
  67. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  68. package/src/superlocalmemory/server/routes/learning.py +276 -105
  69. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  70. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  71. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  72. package/src/superlocalmemory/server/routes/stats.py +93 -155
  73. package/src/superlocalmemory/server/routes/token.py +3 -13
  74. package/src/superlocalmemory/server/routes/v3_api.py +64 -20
  75. package/src/superlocalmemory/server/unified_daemon.py +467 -40
  76. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  77. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  78. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  79. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  80. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  81. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  82. package/src/superlocalmemory/storage/schema.py +49 -1
  83. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  84. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  85. package/src/superlocalmemory/ui/index.html +3 -6
  86. package/src/superlocalmemory/ui/js/core.js +52 -9
  87. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  88. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  89. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  90. package/src/superlocalmemory/ui/js/od-entities.js +22 -22
  91. package/src/superlocalmemory/ui/js/od-graph.js +11 -4
  92. package/src/superlocalmemory/ui/js/od-memories.js +47 -5
  93. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  94. package/src/superlocalmemory/ui/js/od-settings.js +113 -59
  95. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  96. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  97. package/src/superlocalmemory/ui/js/settings.js +15 -1
  98. package/plugin-src/.mcp.json +0 -12
  99. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  100. package/plugin-src/agents/slm-loop-runner.md +0 -71
  101. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  102. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  103. package/plugin-src/commands/slm-loop.md +0 -31
  104. package/plugin-src/hooks/.gitkeep +0 -0
  105. package/plugin-src/hooks/hooks.json +0 -102
  106. package/plugin-src/manifest.json +0 -30
  107. package/plugin-src/requirements.txt +0 -1
  108. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  109. package/plugin-src/scripts/ensure-venv.bat +0 -122
  110. package/plugin-src/scripts/ensure-venv.sh +0 -105
  111. package/plugin-src/scripts/slm-launch +0 -62
  112. package/plugin-src/scripts/slm-launch.bat +0 -23
  113. package/plugin-src/settings.json +0 -25
  114. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  115. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  116. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  117. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  118. 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)
@@ -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;
@@ -521,12 +521,9 @@
521
521
  var act = el.dataset.odAct;
522
522
 
523
523
  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 = '';
524
+ _st = Object.assign({}, _st, { typeFilter: el.dataset.type, page: 0 });
528
525
  _refreshTypeChips(id, el.dataset.type);
529
- _renderList(id);
526
+ _loadList(id);
530
527
  return;
531
528
  }
532
529
  if (act === 'select-entity') {
@@ -546,8 +543,11 @@
546
543
 
547
544
  container.addEventListener('input', function (e) {
548
545
  if (e.target.dataset.odAct === 'ent-search') {
549
- _st = Object.assign({}, _st, { search: e.target.value.trim() });
550
- _renderList(id);
546
+ _st = Object.assign({}, _st, { search: e.target.value.trim(), page: 0 });
547
+ clearTimeout(_searchTimer);
548
+ _searchTimer = setTimeout(function () {
549
+ if (_st.rootId === id) _loadList(id);
550
+ }, 250);
551
551
  }
552
552
  });
553
553
  }
@@ -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);
@@ -223,7 +226,11 @@
223
226
 
224
227
  /* ---------------- physics (ported from design) ---------------- */
225
228
  function tick() {
226
- var vis = NODES.filter(visible), i, j;
229
+ /* Force repulsion is O(n^2). Render every requested node, but only
230
+ simulate a stable bounded subset so the dashboard remains interactive
231
+ for existing installs with very large graphs. */
232
+ var vis = NODES.filter(visible).slice(0, PHYSICS_MAX_NODES), simulated = {}, i, j;
233
+ vis.forEach(function (n) { simulated[n.id] = true; });
227
234
  for (i = 0; i < vis.length; i++) {
228
235
  var a = vis[i];
229
236
  for (j = i + 1; j < vis.length; j++) {
@@ -234,7 +241,7 @@
234
241
  }
235
242
  }
236
243
  LINKS.forEach(function (l) {
237
- if (!visLink(l)) return;
244
+ if (!visLink(l) || !simulated[l.s] || !simulated[l.t]) return;
238
245
  var a = idx[l.s], b = idx[l.t];
239
246
  var dx = b.x - a.x, dy = b.y - a.y, d = Math.sqrt(dx * dx + dy * dy) + 0.01;
240
247
  var rest = (a.type === 'community' || b.type === 'community') ? 150 : 100;
@@ -557,7 +564,7 @@
557
564
  running = false; draw(); updateCount(); updateLegend(); return;
558
565
  }
559
566
  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 */
567
+ for (var i = 0; i < PRE_SETTLE_TICKS; i++) tick(); /* bounded pre-settle keeps navigation responsive */
561
568
  fitFrames = 0; frames = 0;
562
569
  if (!running) { running = true; loop(); } else { wake(); }
563
570
  }).catch(function (err) {
@@ -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 = {};
@@ -306,6 +308,8 @@
306
308
  if (_st.scopeView && _st.scopeView !== 'mine') {
307
309
  url += '&scope=' + encodeURIComponent(_st.scopeView);
308
310
  }
311
+ var requestSeq = _st.requestSeq + 1;
312
+ _st = Object.assign({}, _st, { requestSeq: requestSeq });
309
313
  var wrap = document.getElementById(id + '-tbl-wrap');
310
314
  if (wrap) wrap.innerHTML = _loading('Loading memories…');
311
315
 
@@ -315,10 +319,12 @@
315
319
  return r.json();
316
320
  })
317
321
  .then(function (d) {
322
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
318
323
  _st = Object.assign({}, _st, { memories: d.memories || [], total: d.total || 0 });
319
324
  _renderTable(id, d);
320
325
  })
321
326
  .catch(function (err) {
327
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
322
328
  var w = document.getElementById(id + '-tbl-wrap');
323
329
  if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center">' +
324
330
  'Failed to load memories: ' + _esc(err.message) + '</div>';
@@ -442,22 +448,38 @@
442
448
  var _searchTimer = null;
443
449
  function _doSearch(id, q) {
444
450
  clearTimeout(_searchTimer);
451
+ var requestSeq = _st.requestSeq + 1;
452
+ // /api/search is a full-corpus semantic query. It cannot truthfully retain
453
+ // the list endpoint's category/scope selection, so clear those controls
454
+ // before the request rather than leaving the table and chips disagreeing.
455
+ _st = Object.assign({}, _st, {
456
+ searchQ: q || null,
457
+ category: q ? null : _st.category,
458
+ scopeView: q ? 'mine' : _st.scopeView,
459
+ page: 0,
460
+ requestSeq: requestSeq,
461
+ });
462
+ if (q) _rebuildCatBar(id);
445
463
  _searchTimer = setTimeout(function () {
446
- _st = Object.assign({}, _st, { searchQ: q || null });
464
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
447
465
  if (!q) { _loadMem(id); return; }
448
466
  var wrap = document.getElementById(id + '-tbl-wrap');
449
467
  if (wrap) wrap.innerHTML = _loading('Searching…');
450
468
  fetch('/api/search', { method: 'POST', credentials: 'same-origin',
451
469
  headers: { 'Content-Type': 'application/json' },
470
+ slmInvalidatesCache: false,
471
+ slmRequiresWriteAuth: false,
452
472
  body: JSON.stringify({ query: q, limit: 50, window: _st.window || '' })
453
473
  }).then(function (r) {
454
474
  if (!r.ok) throw new Error('HTTP ' + r.status);
455
475
  return r.json();
456
476
  }).then(function (d) {
477
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
457
478
  var mems = (d.results || []).map(_normSearch);
458
479
  _st = Object.assign({}, _st, { memories: mems, total: mems.length });
459
480
  _renderTable(id, { memories: mems, total: mems.length, limit: mems.length, offset: 0 });
460
481
  }).catch(function (err) {
482
+ if (_st.rootId !== id || _st.requestSeq !== requestSeq) return;
461
483
  var w = document.getElementById(id + '-tbl-wrap');
462
484
  if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center;' +
463
485
  'padding:24px">Search failed: ' + _esc(err.message) + '</div>';
@@ -760,7 +782,13 @@
760
782
  if (bar) bar.querySelectorAll('[data-od-act="cat"]').forEach(function (c) {
761
783
  c.classList.toggle('on', c.dataset.cat === el.dataset.cat);
762
784
  });
763
- _st = Object.assign({}, _st, { category: el.dataset.cat || null, page: 0 });
785
+ clearTimeout(_searchTimer);
786
+ _st = Object.assign({}, _st, {
787
+ category: el.dataset.cat || null, page: 0, searchQ: null,
788
+ requestSeq: _st.requestSeq + 1,
789
+ });
790
+ var searchInput = document.querySelector('#' + id + ' [data-od-act="search"]');
791
+ if (searchInput) searchInput.value = '';
764
792
  _loadMem(id);
765
793
  return;
766
794
  }
@@ -778,7 +806,13 @@
778
806
  if (sroot) sroot.querySelectorAll('[data-od-act="scope-view"]').forEach(function (b) {
779
807
  b.classList.toggle('active', b.dataset.scope === el.dataset.scope);
780
808
  });
781
- _st = Object.assign({}, _st, { scopeView: el.dataset.scope, page: 0 });
809
+ clearTimeout(_searchTimer);
810
+ _st = Object.assign({}, _st, {
811
+ scopeView: el.dataset.scope, page: 0, searchQ: null,
812
+ requestSeq: _st.requestSeq + 1,
813
+ });
814
+ var scopedSearchInput = document.querySelector('#' + id + ' [data-od-act="search"]');
815
+ if (scopedSearchInput) scopedSearchInput.value = '';
782
816
  _loadMem(id);
783
817
  return;
784
818
  }
@@ -791,7 +825,15 @@
791
825
  if (act === 'close-drawer') { _closeDrawer(); return; }
792
826
  if (act === 'pg') {
793
827
  var pg = parseInt(el.dataset.page, 10);
794
- if (!isNaN(pg)) { _st = Object.assign({}, _st, { page: pg }); _loadMem(id); }
828
+ if (!isNaN(pg)) {
829
+ clearTimeout(_searchTimer);
830
+ _st = Object.assign({}, _st, {
831
+ page: pg, searchQ: null, requestSeq: _st.requestSeq + 1,
832
+ });
833
+ var pagedSearchInput = document.querySelector('#' + id + ' [data-od-act="search"]');
834
+ if (pagedSearchInput) pagedSearchInput.value = '';
835
+ _loadMem(id);
836
+ }
795
837
  return;
796
838
  }
797
839
  if (act === 'expand-cluster') { _expandCluster(id, el.dataset.cid); return; }