superlocalmemory 4.0.0 → 4.0.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 (83) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +11 -11
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +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-governance/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-profile/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  33. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  36. package/pyproject.toml +5 -4
  37. package/src/superlocalmemory/__init__.py +1 -1
  38. package/src/superlocalmemory/cli/commands.py +60 -1
  39. package/src/superlocalmemory/cli/main.py +24 -1
  40. package/src/superlocalmemory/compliance/gdpr.py +104 -73
  41. package/src/superlocalmemory/contracts/__init__.py +1 -0
  42. package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +92 -0
  43. package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +46 -0
  44. package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +59 -0
  45. package/src/superlocalmemory/contracts/v402.py +62 -0
  46. package/src/superlocalmemory/core/engine.py +10 -0
  47. package/src/superlocalmemory/core/recall_pipeline.py +6 -0
  48. package/src/superlocalmemory/core/recall_worker.py +12 -0
  49. package/src/superlocalmemory/core/worker_pool.py +12 -0
  50. package/src/superlocalmemory/hooks/hook_handlers.py +16 -0
  51. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +12 -6
  52. package/src/superlocalmemory/hooks/session_registry.py +136 -3
  53. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -2
  54. package/src/superlocalmemory/integrations/__init__.py +1 -0
  55. package/src/superlocalmemory/integrations/bounded_loops_v051.py +236 -0
  56. package/src/superlocalmemory/learning/database.py +21 -14
  57. package/src/superlocalmemory/mcp/_daemon_proxy.py +9 -0
  58. package/src/superlocalmemory/mcp/server.py +5 -0
  59. package/src/superlocalmemory/mcp/tools_brain.py +132 -0
  60. package/src/superlocalmemory/mcp/tools_core.py +25 -6
  61. package/src/superlocalmemory/mcp/tools_v3.py +16 -2
  62. package/src/superlocalmemory/retrieval/engine.py +43 -1
  63. package/src/superlocalmemory/retrieval/temporal_utils.py +16 -1
  64. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +151 -0
  65. package/src/superlocalmemory/server/routes/brain.py +206 -1
  66. package/src/superlocalmemory/server/routes/helpers.py +53 -35
  67. package/src/superlocalmemory/server/routes/v3_api.py +118 -11
  68. package/src/superlocalmemory/server/ui.py +7 -2
  69. package/src/superlocalmemory/server/unified_daemon.py +37 -4
  70. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  71. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  72. package/src/superlocalmemory/storage/agent_experience.py +490 -0
  73. package/src/superlocalmemory/storage/database.py +189 -34
  74. package/src/superlocalmemory/storage/migration_runner.py +8 -0
  75. package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +18 -0
  76. package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +254 -0
  77. package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
  78. package/src/superlocalmemory/storage/schema.py +4 -0
  79. package/src/superlocalmemory/ui/index.html +2 -2
  80. package/src/superlocalmemory/ui/js/auto-settings.js +18 -14
  81. package/src/superlocalmemory/ui/js/brain.js +57 -1
  82. package/src/superlocalmemory/ui/js/od-brain.js +114 -40
  83. package/src/superlocalmemory/ui/js/od-settings.js +8 -1
@@ -222,19 +222,20 @@ async function loadModeSettings() {
222
222
  // Show provider panel and populate model dropdown
223
223
  updateModeUI();
224
224
 
225
- // v3.6.12 (issue #39/#40): populate the endpoint field from the SAVED
226
- // config endpoint. updateProviderUI() above sets the field to the
227
- // provider's DEFAULT (e.g. https://api.openai.com/v1), which hides the
228
- // user's real custom endpoint (llama.cpp/LM Studio) and makes Test
229
- // Connection probe the wrong URL 401. Override with data.endpoint here.
230
- if (data.endpoint) {
231
- setTimeout(function() {
232
- var epEl = document.getElementById('settings-endpoint');
233
- if (epEl) epEl.value = data.endpoint;
234
- var epRow = document.getElementById('settings-endpoint-row');
235
- if (epRow) epRow.style.display = 'block';
236
- }, 0);
237
- }
225
+ // GET /mode deliberately exposes only a redacted endpoint host.
226
+ // updateProviderUI() sets a provider default as an editable value;
227
+ // clear it after a reload and expose the redacted host as a hint only.
228
+ // Otherwise a user who changes nothing can overwrite a full saved URL
229
+ // with the host-only display value on the next Save.
230
+ setTimeout(function() {
231
+ var epEl = document.getElementById('settings-endpoint');
232
+ if (epEl) {
233
+ epEl.value = '';
234
+ epEl.placeholder = data.endpoint || 'Custom endpoint (optional)';
235
+ }
236
+ var epRow = document.getElementById('settings-endpoint-row');
237
+ if (epRow && data.endpoint) epRow.style.display = 'block';
238
+ }, 0);
238
239
 
239
240
  // After provider UI updates, set the saved model value
240
241
  if (model) {
@@ -432,7 +433,10 @@ async function saveAllSettings() {
432
433
  try {
433
434
  // V3.4.24: Include embedding params in save payload
434
435
  var embParams = getEmbeddingParams();
435
- var payload = Object.assign({mode: mode, provider: provider, model: model, api_key: apiKey}, embParams);
436
+ // Password-style fields reload blank. Omit blanks so the API can
437
+ // distinguish "unchanged" from an explicit clear request.
438
+ var payload = Object.assign({mode: mode, provider: provider, model: model}, embParams);
439
+ if (apiKey) payload.api_key = apiKey;
436
440
  if (endpoint) { payload.base_url = endpoint; payload.endpoint = endpoint; }
437
441
  var modeResp = await fetch('/api/v3/mode/set', {
438
442
  method: 'POST',
@@ -825,7 +825,11 @@
825
825
  function cardCrossPlatform(cp) {
826
826
  const data = cp || {};
827
827
  const wrap = EL('section', {className: 'brain-section'});
828
- wrap.appendChild(EL('h4', {text: 'Connected clients'}));
828
+ wrap.appendChild(EL('h4', {text: 'Configured integrations'}));
829
+ wrap.appendChild(EL('p', {
830
+ className: 'brain-help',
831
+ text: 'Installation and sync availability. Recent client activity is shown separately in Living Brain.',
832
+ }));
829
833
 
830
834
  const grid = EL('div', {className: 'brain-adapter-grid'});
831
835
  const order = [
@@ -1096,6 +1100,7 @@
1096
1100
  function renderAll(brain, behavioral) {
1097
1101
  const b = brain || {};
1098
1102
  const nodes = [
1103
+ cardLivingBrain(b.living_brain),
1099
1104
  cardLearning(b.learning),
1100
1105
  ];
1101
1106
  // Only shows when there are legacy rows pending migration; hidden
@@ -1125,6 +1130,57 @@
1125
1130
  return nodes;
1126
1131
  }
1127
1132
 
1133
+ // --------------------------------------------------------------------
1134
+ // Card: Living Brain — portable evidence, never an automatic control
1135
+ // --------------------------------------------------------------------
1136
+ function cardLivingBrain(snapshot) {
1137
+ const data = snapshot || {};
1138
+ const feedback = data.feedback || {};
1139
+ const clients = (data.connected_clients || {}).clients || [];
1140
+ const quality = data.source_quality || {};
1141
+ const graph = data.graph || {};
1142
+ const wrap = EL('section', {className: 'brain-section'});
1143
+ wrap.appendChild(EL('h4', {text: 'Living Brain'}));
1144
+ wrap.appendChild(EL('p', {
1145
+ className: 'brain-help',
1146
+ text: 'A local evidence view of what SLM has observed. It does not automatically change your model, tools, or memories.',
1147
+ }));
1148
+
1149
+ const clientText = clients.length
1150
+ ? clients.map((client) => {
1151
+ const seconds = Number(client.last_seen_seconds_ago || 0);
1152
+ return String(client.kind || 'other') + ' active ' + seconds + 's ago';
1153
+ }).join(' · ')
1154
+ : 'No client activity in the last 5 minutes';
1155
+ const grid = EL('div', {className: 'brain-stat-grid'});
1156
+ grid.appendChild(statRow('Recent clients', clientText));
1157
+ grid.appendChild(statRow('Explicit feedback', feedback.explicit_signals || 0));
1158
+ grid.appendChild(statRow('Implicit signals', feedback.implicit_signals || 0));
1159
+ grid.appendChild(statRow('Settled outcomes', feedback.settled_outcomes || 0));
1160
+ grid.appendChild(statRow(
1161
+ 'Observed source quality',
1162
+ quality.mean_quality == null ? 'No evidence yet' : Number(quality.mean_quality).toFixed(3),
1163
+ ));
1164
+ grid.appendChild(statRow('Sources with evidence', quality.observed_sources || 0));
1165
+ grid.appendChild(statRow('Memory graph nodes', graph.fact_nodes || 0));
1166
+ grid.appendChild(statRow('Memory graph edges', graph.association_edges || 0));
1167
+ wrap.appendChild(grid);
1168
+
1169
+ const byType = feedback.signals_by_type || {};
1170
+ const signalKinds = Object.keys(byType).sort();
1171
+ wrap.appendChild(EL('p', {
1172
+ className: 'brain-help',
1173
+ text: signalKinds.length
1174
+ ? 'Signals: ' + signalKinds.map((kind) => kind + ' (' + Number(byType[kind] || 0) + ')').join(', ')
1175
+ : 'No feedback signals recorded yet. Report outcomes or use memory feedback to start the loop.',
1176
+ }));
1177
+ wrap.appendChild(badge(
1178
+ data.is_real ? 'real' : 'stub',
1179
+ data.source || 'local evidence',
1180
+ ));
1181
+ return wrap;
1182
+ }
1183
+
1128
1184
  // --------------------------------------------------------------------
1129
1185
  // S9-DASH-05: live closed-loop tiles
1130
1186
  // --------------------------------------------------------------------
@@ -32,12 +32,31 @@
32
32
  });
33
33
  }
34
34
 
35
+ function fetchLivingBrain() {
36
+ // The canonical Living Brain read model is install-token gated. The
37
+ // browser obtains that token only from the local same-origin endpoint;
38
+ // it is never rendered, stored, or sent to another origin.
39
+ return apiFetch('/internal/token').then(function (tokenBody) {
40
+ var token = tokenBody && typeof tokenBody.token === 'string'
41
+ ? tokenBody.token.trim() : '';
42
+ if (!token) throw new Error('/internal/token → missing token');
43
+ return fetch('/api/v3/brain', {
44
+ credentials: 'same-origin',
45
+ headers: { 'X-Install-Token': token },
46
+ }).then(function (r) {
47
+ if (!r.ok) throw new Error('/api/v3/brain → ' + r.status);
48
+ return r.json();
49
+ });
50
+ });
51
+ }
52
+
35
53
  function fetchAll() {
36
54
  return Promise.all([
37
55
  apiFetch('/api/learning/status'),
38
56
  apiFetch('/api/behavioral/status'),
39
57
  apiFetch('/api/behavioral/assertions?category=skill_performance&limit=50'),
40
58
  apiFetch('/api/behavioral/tool-events?limit=500'),
59
+ fetchLivingBrain(),
41
60
  ]);
42
61
  }
43
62
 
@@ -184,7 +203,7 @@
184
203
  // ======================================================================
185
204
  // Tab: OVERVIEW
186
205
  // ======================================================================
187
- function buildOverview(learning, behavioral, dateMap) {
206
+ function buildOverview(learning, behavioral, dateMap, living) {
188
207
  var sec = EL('section', { className: 'tabpane active', 'data-p': 'overview' });
189
208
  var stats = (learning && learning.stats) || {};
190
209
  var eng = (learning && learning.engagement) || {};
@@ -208,6 +227,9 @@
208
227
  var healthColor = healthStatus === 'HEALTHY' ? 'var(--ok)'
209
228
  : healthStatus === 'ACTIVE' ? 'var(--cyan)' : undefined;
210
229
  var pCount = ((beh.patterns) || []).length;
230
+ var feedback = (living && living.feedback) || {};
231
+ var graph = (living && living.graph) || {};
232
+ var experience = (living && living.agent_experience) || {};
211
233
 
212
234
  // KPI strip
213
235
  var strip = EL('div', { className: 'kpi-strip', style: 'margin-bottom:16px' });
@@ -291,6 +313,13 @@
291
313
  ['Models trained', String(stats.models_trained || 0)],
292
314
  ['Verified active models', String(stats.models_active_verified || 0)],
293
315
  ['Sources tracked', String(stats.tracked_sources || 0)],
316
+ ['Explicit feedback', String(feedback.explicit_signals || 0)],
317
+ ['Settled outcomes', String(feedback.settled_outcomes || 0)],
318
+ ['Claimed evidence authority', String(experience.claimed_evidence_experiences || 0)],
319
+ ['Cognitive turns', String(experience.turns_total || 0) +
320
+ ' · ' + String((experience.turns_by_state || {}).finalized || 0) + ' finalized'],
321
+ ['Graph evidence', String(graph.fact_nodes || 0) + ' nodes · ' +
322
+ String(graph.association_edges || 0) + ' edges'],
294
323
  ].forEach(function (row) {
295
324
  var r = EL('div', { className: 'list-row' });
296
325
  r.appendChild(EL('span', { className: 'muted', style: 'flex:1', text: row[0] }));
@@ -308,6 +337,35 @@
308
337
  grid.appendChild(priv);
309
338
  sec.appendChild(grid);
310
339
 
340
+ // Keep evidence distinct from ranking or reward so a non-technical user
341
+ // can see what SLM observed without inferring an automatic behavior change.
342
+ var evc = EL('div', { className: 'card', style: 'margin-top:16px' });
343
+ var evh = EL('div', { className: 'card-head' });
344
+ evh.appendChild(EL('h3', { text: 'Agent evidence' }));
345
+ evh.appendChild(EL('span', { className: 'sub', text: 'local receipts · observation only' }));
346
+ evc.appendChild(evh);
347
+ var evb = EL('div', { className: 'card-pad' });
348
+ evb.appendChild(EL('p', {
349
+ className: 'muted',
350
+ style: 'margin:0 0 14px;font-size:12px;line-height:1.55',
351
+ text: 'SLM records completed work when an integration supplies evidence. These records do not change recall, ranking, or model routing by themselves.',
352
+ }));
353
+ var evGrid = EL('div', { className: 'kpi-strip', style: 'margin:0' });
354
+ evGrid.appendChild(kpiCard('fact_check', 'Recorded experiences',
355
+ fmtNum(experience.experiences_total || 0), 'profile-scoped durable receipts',
356
+ Number(experience.experiences_total || 0) > 0, undefined, true));
357
+ evGrid.appendChild(kpiCard('verified', 'Claimed evidence authority',
358
+ fmtNum(experience.claimed_evidence_experiences || 0), 'declared by the producing host',
359
+ Number(experience.claimed_evidence_experiences || 0) > 0, undefined, true));
360
+ evGrid.appendChild(kpiCard('account_tree', 'Cognitive turns',
361
+ fmtNum(experience.turns_total || 0),
362
+ fmtNum((experience.turns_by_state || {}).open || 0) + ' open · ' +
363
+ fmtNum((experience.turns_by_state || {}).finalized || 0) + ' finalized',
364
+ Number(experience.turns_total || 0) > 0, undefined, true));
365
+ evb.appendChild(evGrid);
366
+ evc.appendChild(evb);
367
+ sec.appendChild(evc);
368
+
311
369
  // Activity heatmap (tool events are activity, never reward labels)
312
370
  var hmc = EL('div', { className: 'card', style: 'margin-top:16px' });
313
371
  var hmh = EL('div', { className: 'card-head' });
@@ -589,54 +647,54 @@
589
647
  // ======================================================================
590
648
  // Tab: CONNECTED CLIENTS
591
649
  // ======================================================================
592
- function buildClients(dateMap) {
650
+ function buildClients(living, configured) {
593
651
  var sec = EL('section', { className: 'tabpane', 'data-p': 'clients' });
652
+ var clients = ((living && living.connected_clients) || {}).clients || [];
653
+ var configuredData = configured || {};
594
654
 
595
- // Sparkline from tool-event activity (last 22 data-points matching design)
655
+ // Activity is presence reported by host lifecycle hooks, not the old
656
+ // tool-event proxy. A configured adapter and a recent client are two
657
+ // different truths, rendered as separate cards.
596
658
  var evc = EL('div', { className: 'card', style: 'margin-bottom:16px' });
597
659
  var evh = EL('div', { className: 'card-head' });
598
- evh.appendChild(EL('h3', { text: 'Connected-client evolution' }));
599
- evh.appendChild(EL('span', { className: 'sub', text: 'tool-event activity over time (proxy metric)' }));
660
+ evh.appendChild(EL('h3', { text: 'Recent client activity' }));
661
+ evh.appendChild(EL('span', { className: 'sub', text: 'host lifecycle presence · last 5 minutes' }));
600
662
  evc.appendChild(evh);
601
663
  var evb = EL('div', { className: 'card-pad' });
602
- var today = new Date(); today.setHours(0, 0, 0, 0);
603
- var cVals = [];
604
- for (var ci = 21; ci >= 0; ci--) {
605
- var cd = new Date(today); cd.setDate(today.getDate() - ci);
606
- cVals.push((dateMap && dateMap[cd.toISOString().slice(0, 10)]) || 0);
607
- }
608
- var evSp = EL('div', { id: 'od-brain-sp-clients' });
609
- var hasData = cVals.some(function (v) { return v > 0; });
610
- if (hasData && typeof window.slmSpark === 'function') {
611
- evSp.innerHTML = window.slmSpark(cVals, { w: 600, h: 150, color: 'var(--cyan)' });
612
- var sv2 = evSp.querySelector('svg'); if (sv2) sv2.style.height = '150px';
664
+ if (clients.length === 0) {
665
+ evb.appendChild(EL('p', { className: 'muted', style: 'padding:16px;text-align:center',
666
+ text: 'No host activity in the last 5 minutes. This does not mean an integration is uninstalled.',
667
+ }));
613
668
  } else {
614
- evSp.appendChild(EL('p', { className: 'muted', style: 'padding:32px;text-align:center',
615
- text: 'No event history in this period.' }));
669
+ clients.forEach(function (client) {
670
+ var row = EL('div', { className: 'list-row' });
671
+ row.appendChild(EL('b', { style: 'flex:1', text: String(client.kind || 'other') }));
672
+ row.appendChild(EL('span', { className: 'muted',
673
+ text: 'active ' + Number(client.last_seen_seconds_ago || 0) + 's ago',
674
+ }));
675
+ evb.appendChild(row);
676
+ });
616
677
  }
617
- evb.appendChild(evSp);
618
- // TODO: GET /api/clients — no live connected-client session data available via these endpoints.
619
- // tool-events captures tool invocations but not distinct client identities or session counts.
620
- evb.appendChild(EL('p', {
621
- className: 'muted',
622
- style: 'margin-top:12px;font-size:13px',
623
- text: 'Bars above represent all tool invocations logged to this daemon. ' +
624
- 'A dedicated client-session endpoint is not yet exposed via the public API.',
625
- }));
626
678
  evc.appendChild(evb);
627
679
  sec.appendChild(evc);
628
680
 
629
- // Empty-state clients table
681
+ // Configured integrations are installation/sync state, not client activity.
630
682
  var tc = EL('div', { className: 'card' });
631
683
  var tch = EL('div', { className: 'card-head' });
632
- tch.appendChild(EL('h3', { text: 'Clients' }));
684
+ tch.appendChild(EL('h3', { text: 'Configured integrations' }));
685
+ tch.appendChild(EL('span', { className: 'sub', text: 'installation and sync availability' }));
633
686
  tc.appendChild(tch);
634
687
  var tcb = EL('div', { className: 'card-pad' });
635
- tcb.appendChild(EL('p', {
636
- className: 'muted',
637
- style: 'padding:16px;text-align:center;font-size:13px',
638
- text: 'Client details are not yet available via API. Coming in a future daemon release.',
639
- }));
688
+ Object.keys(configuredData).sort().forEach(function (kind) {
689
+ var state = configuredData[kind] || {};
690
+ var row = EL('div', { className: 'list-row' });
691
+ row.appendChild(EL('span', { style: 'flex:1', text: kind.replace(/_/g, ' ') }));
692
+ row.appendChild(EL('span', {
693
+ className: 'badge ' + (state.active ? 'ok' : 'warn'),
694
+ text: state.active ? 'available' : (state.reason || 'not available'),
695
+ }));
696
+ tcb.appendChild(row);
697
+ });
640
698
  tc.appendChild(tcb);
641
699
  sec.appendChild(tc);
642
700
  return sec;
@@ -645,7 +703,7 @@
645
703
  // ======================================================================
646
704
  // Tab: SOURCE QUALITY
647
705
  // ======================================================================
648
- function buildSourceQuality(learning) {
706
+ function buildSourceQuality(learning, living) {
649
707
  var sec = EL('section', { className: 'tabpane', 'data-p': 'sources' });
650
708
  var scores = (learning && learning.source_scores) || {};
651
709
  var entries = Object.keys(scores).sort(function (a, b) {
@@ -657,13 +715,27 @@
657
715
  ch.appendChild(EL('span', { className: 'sub', text: 'persisted source-outcome posterior · 0.0–1.0' }));
658
716
  card.appendChild(ch);
659
717
  var cb = EL('div', { className: 'card-pad' });
660
- if (entries.length === 0) {
718
+ var aggregate = (living && living.source_quality) || {};
719
+ var observedSources = Number(aggregate.observed_sources || 0);
720
+ if (observedSources === 0) {
721
+ cb.appendChild(EL('p', {
722
+ className: 'muted',
723
+ style: 'padding:16px;text-align:center;font-size:13px',
724
+ text: 'No source-quality evidence has settled yet. Recall hits alone do not establish source quality.',
725
+ }));
726
+ } else if (entries.length === 0) {
661
727
  cb.appendChild(EL('p', {
662
728
  className: 'muted',
663
729
  style: 'padding:16px;text-align:center;font-size:13px',
664
- text: 'No source-quality observations yet. Recall hits alone do not establish source quality.',
730
+ text: String(observedSources) + ' sources have evidence, but no per-source posterior is available yet.',
665
731
  }));
666
732
  } else {
733
+ cb.appendChild(EL('p', {
734
+ className: 'muted',
735
+ style: 'font-size:13px;margin-bottom:16px',
736
+ text: String(observedSources) + ' sources with observed mean quality ' +
737
+ Number(aggregate.mean_quality).toFixed(3) + '.',
738
+ }));
667
739
  entries.forEach(function (k) {
668
740
  var v = Number(scores[k]);
669
741
  var row = EL('div', { style: 'margin-bottom:14px' });
@@ -765,6 +837,8 @@
765
837
  var learning = results[0] || {};
766
838
  var behavioral = results[1] || {};
767
839
  var events = ((results[3] && results[3].events) || []);
840
+ var brain = results[4] || {};
841
+ var living = brain.living_brain || {};
768
842
  var dateMap = buildDateMap(events);
769
843
  var pCount = ((behavioral.patterns) || []).length;
770
844
 
@@ -778,11 +852,11 @@
778
852
  container.replaceChildren(
779
853
  head,
780
854
  buildTabRow(pCount),
781
- buildOverview(learning, behavioral, dateMap),
855
+ buildOverview(learning, behavioral, dateMap, living),
782
856
  buildReward(behavioral),
783
857
  buildBehaviour(learning, behavioral),
784
- buildClients(dateMap),
785
- buildSourceQuality(learning)
858
+ buildClients(living, brain.cross_platform),
859
+ buildSourceQuality(learning, living)
786
860
  );
787
861
  wireTabs(container);
788
862
  }).catch(function (err) { showError(container, err); });
@@ -302,7 +302,14 @@
302
302
  var mi = q('model'); if (mi) mi.value = d.model || '';
303
303
  var k = q('apikey');
304
304
  if (k) k.placeholder = d.has_key ? '(key saved — enter new to replace)' : 'sk-...';
305
- var ep = q('endpoint'); if (ep) ep.value = d.endpoint || '';
305
+ // GET /mode deliberately returns only a redacted host. Showing that
306
+ // host as the editable value would turn a passive reload + save into a
307
+ // destructive replacement of the real URL, so it is display-only.
308
+ var ep = q('endpoint');
309
+ if (ep) {
310
+ ep.value = '';
311
+ ep.placeholder = d.endpoint || 'http://localhost:11434';
312
+ }
306
313
  }).catch(function () {});
307
314
  }
308
315
  function saveMode() {