superlocalmemory 4.0.5 → 4.0.7

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 (71) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +8 -9
  3. package/package.json +3 -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/pyproject.toml +1 -1
  25. package/src/superlocalmemory/__init__.py +1 -1
  26. package/src/superlocalmemory/access/rbac.py +106 -0
  27. package/src/superlocalmemory/brain/truth.py +80 -10
  28. package/src/superlocalmemory/cli/__main__.py +17 -0
  29. package/src/superlocalmemory/cli/commands.py +28 -3
  30. package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
  31. package/src/superlocalmemory/cli/gdpr_io.py +109 -0
  32. package/src/superlocalmemory/cli/main.py +85 -0
  33. package/src/superlocalmemory/cli/summary_cmd.py +195 -0
  34. package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +26 -0
  35. package/src/superlocalmemory/code_graph/bridge/event_listeners.py +14 -3
  36. package/src/superlocalmemory/code_graph/bridge/maintenance.py +206 -0
  37. package/src/superlocalmemory/code_graph/config.py +65 -1
  38. package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
  39. package/src/superlocalmemory/code_graph/graph_store.py +180 -3
  40. package/src/superlocalmemory/code_graph/parser.py +280 -100
  41. package/src/superlocalmemory/compliance/gdpr.py +358 -0
  42. package/src/superlocalmemory/core/config.py +44 -1
  43. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  44. package/src/superlocalmemory/core/fact_consolidator.py +24 -1
  45. package/src/superlocalmemory/core/maintenance.py +93 -1
  46. package/src/superlocalmemory/core/recall_worker.py +33 -12
  47. package/src/superlocalmemory/infra/backup.py +138 -0
  48. package/src/superlocalmemory/infra/backup_obligations.py +423 -0
  49. package/src/superlocalmemory/learning/engagement.py +165 -0
  50. package/src/superlocalmemory/mcp/tools_code_graph.py +78 -7
  51. package/src/superlocalmemory/mcp/tools_v3.py +20 -6
  52. package/src/superlocalmemory/retrieval/engine.py +21 -0
  53. package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
  54. package/src/superlocalmemory/server/routes/brain.py +283 -15
  55. package/src/superlocalmemory/server/routes/learning.py +13 -25
  56. package/src/superlocalmemory/server/routes/memories.py +61 -0
  57. package/src/superlocalmemory/server/routes/v3_api.py +171 -60
  58. package/src/superlocalmemory/storage/database.py +36 -0
  59. package/src/superlocalmemory/storage/models.py +12 -4
  60. package/src/superlocalmemory/storage/schema_code_graph.py +44 -1
  61. package/src/superlocalmemory/summaries/__init__.py +37 -0
  62. package/src/superlocalmemory/summaries/base.py +108 -0
  63. package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
  64. package/src/superlocalmemory/summaries/project_work_log.py +424 -0
  65. package/src/superlocalmemory/summaries/session_summary.py +307 -0
  66. package/src/superlocalmemory/ui/css/design-system.css +76 -1
  67. package/src/superlocalmemory/ui/index.html +29 -12
  68. package/src/superlocalmemory/ui/js/fact-detail.js +61 -0
  69. package/src/superlocalmemory/ui/js/od-agents.js +49 -5
  70. package/src/superlocalmemory/ui/js/od-brain.js +257 -77
  71. package/src/superlocalmemory/ui/js/od-graph.js +147 -6
@@ -87,9 +87,16 @@
87
87
  }
88
88
 
89
89
  function phaseLabel(raw) {
90
- return (raw || 'cold_start')
91
- .replace(/_/g, '-')
92
- .replace(/\b([a-z])/g, function (c) { return c.toUpperCase(); });
90
+ // Map known phase keys to human labels. Never string-mangle an internal
91
+ // identifier — unknown values fall back to a plain-English generic.
92
+ var LABELS = {
93
+ baseline: 'Baseline',
94
+ rule_based: 'Rule-based',
95
+ ml_model: 'ML model',
96
+ cold_start: 'Starting out',
97
+ };
98
+ var key = (raw || 'cold_start').toLowerCase().replace(/-/g, '_').replace(/\s+/g, '_');
99
+ return LABELS[key] || (raw ? String(raw).replace(/[-_]/g, ' ') : 'Starting out');
93
100
  }
94
101
 
95
102
  // ======================================================================
@@ -153,6 +160,35 @@
153
160
  return wrap;
154
161
  }
155
162
 
163
+ function humanizeSource(raw) {
164
+ // Map raw internal capability IDs to plain names a non-technical user can read.
165
+ // IDs follow patterns such as http:daemon-capability:<hex> or
166
+ // dashboard:local-capability:http-route:uid:<uid>:<hex>.
167
+ // Strip trailing hex hashes, then map the leading protocol/type prefix.
168
+ var s = String(raw || '')
169
+ .replace(/:?[0-9a-f]{40,}$/i, '')
170
+ .replace(/:?[0-9a-f]{32,}$/i, '')
171
+ .replace(/:$/, '')
172
+ .trim();
173
+ var prefix = (s.split(':')[0] || '').toLowerCase();
174
+ var MAP = {
175
+ http: 'Background service',
176
+ https: 'Background service',
177
+ dashboard: 'Dashboard',
178
+ cli: 'Command line',
179
+ 'claude-code': 'Claude Code',
180
+ claude_code: 'Claude Code',
181
+ copilot: 'Copilot',
182
+ cursor: 'Cursor',
183
+ mcp: 'MCP server',
184
+ daemon: 'Background service',
185
+ };
186
+ if (MAP[prefix]) return MAP[prefix];
187
+ // Fall back: take the first segment, replace underscores/dashes, capitalise.
188
+ var label = prefix.replace(/[-_]/g, ' ').trim();
189
+ return label ? label.charAt(0).toUpperCase() + label.slice(1) : String(raw);
190
+ }
191
+
156
192
  function heatLegend() {
157
193
  var w = EL('div', { className: 'heat-legend' });
158
194
  w.appendChild(document.createTextNode('less '));
@@ -219,10 +255,10 @@
219
255
  var phaseNumber = Number(ranker.phase || 1);
220
256
  var modelActive = Boolean(ranker.model_active);
221
257
  var phaseDelta = modelActive
222
- ? 'Verified active model'
258
+ ? 'Personalised model active'
223
259
  : signals < mlGate
224
- ? fmtNum(mlGate - signals) + ' to ML data gate'
225
- : 'ML data gate met · verified model required';
260
+ ? fmtNum(mlGate - signals) + ' more interactions to unlock personalisation'
261
+ : 'Ready to personalise awaiting model verification';
226
262
  var healthStatus = (eng.health_status || 'INACTIVE').toUpperCase();
227
263
  var healthColor = healthStatus === 'HEALTHY' ? 'var(--ok)'
228
264
  : healthStatus === 'ACTIVE' ? 'var(--cyan)' : undefined;
@@ -250,17 +286,22 @@
250
286
  // Ranking phase: text label → isNumeric=false (font-size:24px to match design)
251
287
  strip.appendChild(kpiCard('skill', 'Ranking phase', phaseLabel(phase),
252
288
  phaseDelta, modelActive, undefined, false));
253
- // Feedback signals: numeric isNumeric=true
254
- strip.appendChild(kpiCard('optimize', 'Feedback signals', fmtNum(signals),
255
- '▲ ' + fmtNum(stats.unique_queries || 0) + ' unique queries', true, undefined, true));
289
+ // Questions answered: unique queries is the meaningful unit — total signals is context.
290
+ // 5,339 signals across 3 unique queries means one query repeated, not broad learning.
291
+ var uniqueQ = Number(stats.unique_queries || 0);
292
+ strip.appendChild(kpiCard('optimize', 'Questions answered', fmtNum(uniqueQ),
293
+ uniqueQ > 0
294
+ ? fmtNum(signals) + ' total interactions'
295
+ : 'no questions asked yet',
296
+ uniqueQ > 0, undefined, true));
256
297
  // Engagement health: text label → isNumeric=false
257
298
  strip.appendChild(kpiCard('health', 'Engagement health',
258
299
  healthStatus.charAt(0) + healthStatus.slice(1).toLowerCase(),
259
- (eng.days_active || 0) + ' days active · ' + Number(eng.memories_per_day || 0).toFixed(1) + ' mem/day',
300
+ (eng.days_active || 0) + ' days active · ' + Number(eng.memories_per_day || 0).toFixed(1) + ' saves/day',
260
301
  healthStatus === 'HEALTHY', healthColor, false));
261
302
  // Patterns: numeric → isNumeric=true
262
303
  strip.appendChild(kpiCard('brain', 'Patterns learned', String(pCount),
263
- '▲ ' + (beh.cross_project_transfers || 0) + ' transferable', pCount > 0, undefined, true));
304
+ (beh.cross_project_transfers || 0) + ' used across projects', pCount > 0, undefined, true));
264
305
  sec.appendChild(strip);
265
306
 
266
307
  // 2-column grid
@@ -276,8 +317,17 @@
276
317
  var pmeta = EL('div', {
277
318
  style: 'display:flex;justify-content:space-between;font-size:12px;color:var(--fg-2);margin-bottom:8px',
278
319
  });
279
- pmeta.appendChild(EL('span', { text: fmtNum(signals) + ' / ' + fmtNum(mlGate) + ' signals' }));
280
- pmeta.appendChild(EL('span', { className: 'num', text: pct + '%' }));
320
+ if (signals >= mlGate) {
321
+ // Gate already passed showing the overrun fraction (e.g. "5,339 / 200") is
322
+ // misleading. Show the done state and what comes next instead.
323
+ pmeta.appendChild(EL('span', { text: fmtNum(signals) + ' interactions · gate passed' }));
324
+ var gdBadge = EL('span', { className: 'badge ' + (modelActive ? 'ok' : 'warn') });
325
+ gdBadge.appendChild(document.createTextNode(modelActive ? 'model active' : 'awaiting verification'));
326
+ pmeta.appendChild(gdBadge);
327
+ } else {
328
+ pmeta.appendChild(EL('span', { text: fmtNum(signals) + ' of ' + fmtNum(mlGate) + ' interactions' }));
329
+ pmeta.appendChild(EL('span', { className: 'num', text: pct + '%' }));
330
+ }
281
331
  pb.appendChild(pmeta);
282
332
  pb.appendChild(meter(pct));
283
333
  var phasesRow = EL('div', {
@@ -329,12 +379,12 @@
329
379
  ['Sources tracked', String(stats.tracked_sources || 0)],
330
380
  ['Memory activity', truthCount(memoryActivity, 'facts_total', 'facts')],
331
381
  ['Feedback signals', truthCount(feedback, 'signals_total', 'signals')],
332
- ['Claimed evidence', truthCount(experience, 'claimed_experiences_total', 'receipts')],
382
+ ['Claimed evidence', truthCount(experience, 'claimed_experiences_total', '')],
333
383
  ['Independently verified evidence', truthCount(
334
- experience, 'independently_verified_experiences_total', 'receipts',
384
+ experience, 'independently_verified_experiences_total', '',
335
385
  )],
336
- ['External observations', truthCount(externalEvidence, 'receipts_total', 'receipts')],
337
- ['Correction quality', truthCount(correctionQuality, 'cases_total', 'review cases')],
386
+ ['External observations', truthCount(externalEvidence, 'receipts_total', '')],
387
+ ['Correction quality', truthCount(correctionQuality, 'cases_total', '')],
338
388
  ['Graph evidence', String(graph.fact_nodes || 0) + ' nodes · ' +
339
389
  String(graph.association_edges || 0) + ' edges'],
340
390
  ].forEach(function (row) {
@@ -367,26 +417,29 @@
367
417
  style: 'margin:0 0 14px;font-size:12px;line-height:1.55',
368
418
  text: 'SLM records completed work when an integration supplies evidence. These records do not change recall, ranking, or model routing by themselves.',
369
419
  }));
420
+ // Three plain-language KPI cards.
421
+ // Technical labels (Claimed evidence, Independently verified evidence,
422
+ // External observations, Correction quality) are preserved in the
423
+ // Privacy card rows above for test and audit traceability.
424
+ var claimedCount = Number(experience.claimed_experiences_total || 0);
425
+ var corrCount = Number(correctionQuality.cases_total || 0);
426
+ var obsCount = Number(externalEvidence.receipts_total || 0);
370
427
  var evGrid = EL('div', { className: 'kpi-strip', style: 'margin:0' });
371
- evGrid.appendChild(kpiCard('fact_check', 'Claimed evidence',
372
- truthCount(experience, 'claimed_experiences_total', ''), 'profile-scoped durable receipts',
373
- Number(experience.claimed_experiences_total || 0) > 0, undefined, true));
374
- evGrid.appendChild(kpiCard('verified', 'Independently verified evidence',
375
- truthCount(experience, 'independently_verified_experiences_total', ''),
376
- experience.verification_availability === 'not_supported_by_read_model'
377
- ? 'not supported by this read model' : 'independent verifier result',
378
- Number(experience.independently_verified_experiences_total || 0) > 0, undefined, true));
379
- evGrid.appendChild(kpiCard('account_tree', 'Cognitive turns',
380
- truthCount(experience, 'cognitive_turns_total', ''),
381
- String((experience.cognitive_turns_by_state || {}).open || 0) + ' open · ' +
382
- String((experience.cognitive_turns_by_state || {}).finalized || 0) + ' finalized',
383
- Number(experience.cognitive_turns_total || 0) > 0, undefined, true));
384
- evGrid.appendChild(kpiCard('account_tree', 'External observations',
428
+ evGrid.appendChild(kpiCard('fact_check', 'Tasks completed',
429
+ truthCount(experience, 'claimed_experiences_total', ''),
430
+ claimedCount > 0 ? 'work recorded from connected tools' : 'no tasks recorded yet',
431
+ claimedCount > 0, undefined, true));
432
+ evGrid.appendChild(kpiCard('tune', 'Corrections applied',
433
+ truthCount(correctionQuality, 'cases_total', ''),
434
+ corrCount > 0 ? 'improvements applied to results' : 'no corrections recorded yet',
435
+ corrCount > 0, undefined, true));
436
+ evGrid.appendChild(kpiCard('science', 'Observations logged',
385
437
  truthCount(externalEvidence, 'receipts_total', ''),
386
- externalEvidence.availability === 'available'
387
- ? String(externalEvidence.demonstrations_total || 0) + ' demonstrations · no automatic learning'
388
- : 'external evidence unavailable',
389
- Number(externalEvidence.receipts_total || 0) > 0, undefined, true));
438
+ externalEvidence.availability === 'available' &&
439
+ Number(externalEvidence.demonstrations_total || 0) > 0
440
+ ? String(externalEvidence.demonstrations_total) + ' demonstrations'
441
+ : 'no external demonstrations yet',
442
+ obsCount > 0, undefined, true));
390
443
  evb.appendChild(evGrid);
391
444
  evc.appendChild(evb);
392
445
  sec.appendChild(evc);
@@ -427,7 +480,7 @@
427
480
  hmhr.appendChild(EL('h3', { text: 'Reward signal density' }));
428
481
  hmhr.appendChild(EL('span', {
429
482
  className: 'sub',
430
- text: 'settled numeric labels per day · last ' + Number(reward.window_days || 182) + ' days',
483
+ text: 'feedback events per day · last ' + Number(reward.window_days || 182) + ' days',
431
484
  }));
432
485
  hmhr.appendChild(EL('div', { className: 'spacer' }));
433
486
  hmhr.appendChild(heatLegend());
@@ -439,16 +492,26 @@
439
492
  hmcr.appendChild(hmbr);
440
493
  sec.appendChild(hmcr);
441
494
 
495
+ // Compute distribution state once so both cards use the same values.
496
+ var total = Number(reward.count || 0);
497
+ var bd = reward.distribution || {};
498
+ // isUnmeasuredPrior: when positive=0 and negative=0, every label sits on the
499
+ // initialization value (0.5). That is the absence of differentiation, not a
500
+ // finding — render it honestly rather than as a measured result.
501
+ var isUnmeasuredPrior = total > 0
502
+ && Number(bd.positive || 0) === 0
503
+ && Number(bd.negative || 0) === 0;
504
+
442
505
  // 2-column: sparkline + outcome mix
443
506
  var grid = EL('div', { className: 'grid', style: 'grid-template-columns:1fr 1fr;align-items:start' });
444
507
 
445
- // Average settled reward and real daily series
508
+ // Recall quality card: average reward score + real daily sparkline
446
509
  var fbCard = EL('div', { className: 'card' });
447
510
  var fbH = EL('div', { className: 'card-head' });
448
- fbH.appendChild(EL('h3', { text: 'Average settled reward' }));
511
+ fbH.appendChild(EL('h3', { text: 'Recall quality' }));
449
512
  fbH.appendChild(EL('span', {
450
513
  className: 'sub',
451
- text: fmtNum(reward.count || 0) + ' finalized labels',
514
+ text: fmtNum(reward.count || 0) + (isUnmeasuredPrior ? ' interactions · default score' : ' interactions'),
452
515
  }));
453
516
  fbCard.appendChild(fbH);
454
517
  var fbB = EL('div', { className: 'card-pad' });
@@ -458,6 +521,13 @@
458
521
  style: 'font-size:30px;margin-bottom:12px',
459
522
  text: Number(reward.average).toFixed(3),
460
523
  }));
524
+ if (isUnmeasuredPrior) {
525
+ fbB.appendChild(EL('p', {
526
+ className: 'muted',
527
+ style: 'font-size:12px;margin-top:4px;margin-bottom:0',
528
+ text: 'Starting value — no differentiated engagement yet.',
529
+ }));
530
+ }
461
531
  }
462
532
  var fbSp = EL('div', { id: 'od-brain-sp-fb' });
463
533
  var sparkVals = timeline.slice(-30).map(function (point) {
@@ -471,7 +541,7 @@
471
541
  fbSp.appendChild(EL('p', {
472
542
  className: 'muted',
473
543
  style: 'padding:32px;text-align:center;font-size:13px',
474
- text: 'No settled reward history is available yet.',
544
+ text: 'No recall history is available yet.',
475
545
  }));
476
546
  }
477
547
  fbB.appendChild(fbSp);
@@ -482,16 +552,17 @@
482
552
  var outCard = EL('div', { className: 'card' });
483
553
  var outH = EL('div', { className: 'card-head' });
484
554
  outH.appendChild(EL('h3', { text: 'Reward distribution' }));
485
- outH.appendChild(EL('span', { className: 'sub', text: 'engagement-derived settled labels' }));
555
+ outH.appendChild(EL('span', { className: 'sub', text: 'based on how you engage with recalled results' }));
486
556
  outCard.appendChild(outH);
487
557
  var outB = EL('div', { className: 'card-pad', id: 'od-brain-outcomes' });
488
- var total = Number(reward.count || 0);
489
- var bd = reward.distribution || {};
490
- if (total === 0) {
558
+ if (total === 0 || isUnmeasuredPrior) {
491
559
  outB.appendChild(EL('p', {
492
560
  className: 'muted',
493
561
  style: 'padding:16px;text-align:center;font-size:13px',
494
- text: 'No settled reward labels yet. Recall engagement will populate this view.',
562
+ text: total === 0
563
+ ? 'No reward labels yet. Recall engagement will populate this view.'
564
+ : 'All ' + fmtNum(total) + ' labels carry the default score. ' +
565
+ 'Differentiated results appear after consistent recall use.',
495
566
  }));
496
567
  } else {
497
568
  [
@@ -530,7 +601,7 @@
530
601
  var tc = EL('div', { className: 'card' });
531
602
  var tch = EL('div', { className: 'card-head' });
532
603
  tch.appendChild(EL('h3', { text: 'Tech preferences' }));
533
- tch.appendChild(EL('span', { className: 'sub', text: 'Layer 1 · confidence-weighted' }));
604
+ tch.appendChild(EL('span', { className: 'sub', text: 'your tools and technology preferences' }));
534
605
  tc.appendChild(tch);
535
606
  var tcb = EL('div', { className: 'card-pad' });
536
607
  var techItems = l.tech_preferences || [];
@@ -571,7 +642,7 @@
571
642
  var wc = EL('div', { className: 'card' });
572
643
  var wch = EL('div', { className: 'card-head' });
573
644
  wch.appendChild(EL('h3', { text: 'Workflow patterns' }));
574
- wch.appendChild(EL('span', { className: 'sub', text: 'Layer 3 · sequence & temporal' }));
645
+ wch.appendChild(EL('span', { className: 'sub', text: 'how you sequence your work over time' }));
575
646
  wc.appendChild(wch);
576
647
  var wcb = EL('div', { className: 'card-pad' });
577
648
  var wfPats = l.workflow_patterns || [];
@@ -672,11 +743,26 @@
672
743
  // ======================================================================
673
744
  // Tab: CONNECTED CLIENTS
674
745
  // ======================================================================
675
- function buildClients(living, configured) {
746
+ function fmtSecondsAgo(s) {
747
+ if (s == null) return 'unknown';
748
+ var n = Number(s);
749
+ if (n < 120) return n + 's ago';
750
+ if (n < 7200) return Math.round(n / 60) + 'm ago';
751
+ if (n < 172800) return Math.round(n / 3600) + 'h ago';
752
+ return Math.round(n / 86400) + 'd ago';
753
+ }
754
+
755
+ // buildClients accepts an optional third argument `boundedLoops` (brain.bounded_loops).
756
+ // Absent / undefined is safe: the BL section is silently omitted.
757
+ function buildClients(living, configured, boundedLoops) {
676
758
  var sec = EL('section', { className: 'tabpane', 'data-p': 'clients' });
677
- var clients = ((living && living.connected_clients) || {}).clients || [];
759
+ var connectedData = (living && living.connected_clients) || {};
760
+ var clients = connectedData.clients || [];
761
+ var registryStatus = String(connectedData.registry_status || '');
762
+ var newestAgo = connectedData.newest_entry_seconds_ago;
678
763
  var configuredData = configured || {};
679
764
 
765
+ // ── Recent client activity ────────────────────────────────────────────
680
766
  // Activity is presence reported by host lifecycle hooks, not the old
681
767
  // tool-event proxy. A configured adapter and a recent client are two
682
768
  // different truths, rendered as separate cards.
@@ -686,8 +772,46 @@
686
772
  evh.appendChild(EL('span', { className: 'sub', text: 'host lifecycle presence · last 5 minutes' }));
687
773
  evc.appendChild(evh);
688
774
  var evb = EL('div', { className: 'card-pad' });
689
- if (clients.length === 0) {
690
- evb.appendChild(EL('p', { className: 'muted', style: 'padding:16px;text-align:center',
775
+
776
+ if (registryStatus === 'error' || registryStatus === 'unknown') {
777
+ // Wave 4 honesty rule: failure and emptiness must not return the same value.
778
+ // 'unknown' belongs HERE, not in the healthy-empty branch below. The
779
+ // registry reader is fail-soft — a corrupt or unreadable file makes
780
+ // _load() return {} and active_client_summary() return [], so the read
781
+ // model reports status 'unknown' with an empty client list and is_real
782
+ // true. Falling through to "No host activity in the last 5 minutes" would
783
+ // render a BROKEN registry exactly like a healthy quiet machine, which is
784
+ // precisely the silent failure this section was rewritten to eliminate.
785
+ evb.appendChild(EL('p', {
786
+ className: 'muted',
787
+ style: 'padding:16px;text-align:center;font-size:13px',
788
+ text: registryStatus === 'unknown'
789
+ ? 'Presence records could not be read, so recent activity cannot be '
790
+ + 'confirmed. This is not the same as no agents being active.'
791
+ : 'Presence registry unavailable. Check daemon logs for details.',
792
+ }));
793
+ } else if (registryStatus === 'stale') {
794
+ // TELEMETRY GAP — entries exist but all are > 10 min old.
795
+ // "No activity" here would be indistinguishable from "hooks not firing".
796
+ evb.appendChild(EL('p', {
797
+ className: 'muted',
798
+ style: 'padding:16px;text-align:center;font-size:13px',
799
+ text: 'Presence recording gap — last hook event was ' + fmtSecondsAgo(newestAgo) + '. ' +
800
+ 'Hooks are installed but presence has not been written recently. ' +
801
+ 'Recent activity from integrations cannot be confirmed.',
802
+ }));
803
+ } else if (registryStatus === 'absent' || registryStatus === 'empty') {
804
+ // No records at all — first run or hooks have not fired yet.
805
+ evb.appendChild(EL('p', {
806
+ className: 'muted',
807
+ style: 'padding:16px;text-align:center;font-size:13px',
808
+ text: 'No presence records found. Hook events may not have fired yet on this install.',
809
+ }));
810
+ } else if (clients.length === 0) {
811
+ // Registry is live (recent writes) but no clients in the 5-min window.
812
+ evb.appendChild(EL('p', {
813
+ className: 'muted',
814
+ style: 'padding:16px;text-align:center;font-size:13px',
691
815
  text: 'No host activity in the last 5 minutes. This does not mean an integration is uninstalled.',
692
816
  }));
693
817
  } else {
@@ -695,7 +819,7 @@
695
819
  var row = EL('div', { className: 'list-row' });
696
820
  row.appendChild(EL('b', { style: 'flex:1', text: String(client.kind || 'other') }));
697
821
  row.appendChild(EL('span', { className: 'muted',
698
- text: 'active ' + Number(client.last_seen_seconds_ago || 0) + 's ago',
822
+ text: 'active ' + fmtSecondsAgo(client.last_seen_seconds_ago),
699
823
  }));
700
824
  evb.appendChild(row);
701
825
  });
@@ -703,8 +827,11 @@
703
827
  evc.appendChild(evb);
704
828
  sec.appendChild(evc);
705
829
 
706
- // Configured integrations are installation/sync state, not client activity.
707
- var tc = EL('div', { className: 'card' });
830
+ // ── Configured integrations ───────────────────────────────────────────
831
+ // Installation / sync state, not client activity.
832
+ // For Codex, evidence_tier='configured' means config-file evidence;
833
+ // badge reads 'configured' rather than 'available' to be explicit.
834
+ var tc = EL('div', { className: 'card', style: 'margin-bottom:16px' });
708
835
  var tch = EL('div', { className: 'card-head' });
709
836
  tch.appendChild(EL('h3', { text: 'Configured integrations' }));
710
837
  tch.appendChild(EL('span', { className: 'sub', text: 'installation and sync availability' }));
@@ -714,14 +841,50 @@
714
841
  var state = configuredData[kind] || {};
715
842
  var row = EL('div', { className: 'list-row' });
716
843
  row.appendChild(EL('span', { style: 'flex:1', text: kind.replace(/_/g, ' ') }));
717
- row.appendChild(EL('span', {
844
+ var badgeText = state.active
845
+ ? (state.evidence_tier === 'configured' ? 'configured' : 'available')
846
+ : (state.reason || 'not available');
847
+ var badgeEl = EL('span', {
718
848
  className: 'badge ' + (state.active ? 'ok' : 'warn'),
719
- text: state.active ? 'available' : (state.reason || 'not available'),
720
- }));
849
+ text: badgeText,
850
+ });
851
+ if (state.evidence) badgeEl.setAttribute('title', String(state.evidence));
852
+ row.appendChild(badgeEl);
721
853
  tcb.appendChild(row);
722
854
  });
723
855
  tc.appendChild(tcb);
724
856
  sec.appendChild(tc);
857
+
858
+ // ── Bounded Loops ─────────────────────────────────────────────────────
859
+ // Section is on when bounded_loops.section_enabled === true.
860
+ // When absent (older API or not installed) the section is silently omitted.
861
+ var bl = boundedLoops || {};
862
+ if (bl.section_enabled) {
863
+ var blc = EL('div', { className: 'card' });
864
+ var blh = EL('div', { className: 'card-head' });
865
+ blh.appendChild(EL('h3', { text: 'Bounded Loops' }));
866
+ blh.appendChild(EL('span', {
867
+ className: 'sub',
868
+ text: 'local installation' + (bl.version ? ' · v' + bl.version : ''),
869
+ }));
870
+ blc.appendChild(blh);
871
+ var blb = EL('div', { className: 'card-pad', style: 'display:flex;flex-direction:column;gap:2px' });
872
+ [
873
+ ['Status', 'installed'],
874
+ ['Version', bl.version || 'unknown'],
875
+ ['Bridge contract', bl.bridge_contract || '—'],
876
+ ['Evidence', bl.evidence || bl.evidence_tier || '—'],
877
+ ['Note', bl.note || '—'],
878
+ ].forEach(function (r) {
879
+ var row = EL('div', { className: 'list-row' });
880
+ row.appendChild(EL('span', { className: 'muted', style: 'flex:1', text: r[0] }));
881
+ row.appendChild(EL('b', { text: String(r[1]) }));
882
+ blb.appendChild(row);
883
+ });
884
+ blc.appendChild(blb);
885
+ sec.appendChild(blc);
886
+ }
887
+
725
888
  return sec;
726
889
  }
727
890
 
@@ -737,7 +900,7 @@
737
900
  var card = EL('div', { className: 'card' });
738
901
  var ch = EL('div', { className: 'card-head' });
739
902
  ch.appendChild(EL('h3', { text: 'Source quality' }));
740
- ch.appendChild(EL('span', { className: 'sub', text: 'persisted source-outcome posterior · 0.0–1.0' }));
903
+ ch.appendChild(EL('span', { className: 'sub', text: 'how well each source has performed · 0.0–1.0' }));
741
904
  card.appendChild(ch);
742
905
  var cb = EL('div', { className: 'card-pad' });
743
906
  var aggregate = (living && living.source_quality) || {};
@@ -752,27 +915,44 @@
752
915
  cb.appendChild(EL('p', {
753
916
  className: 'muted',
754
917
  style: 'padding:16px;text-align:center;font-size:13px',
755
- text: String(observedSources) + ' sources have evidence, but no per-source posterior is available yet.',
918
+ text: String(observedSources) + ' sources have been seen, but individual quality scores are not available yet.',
756
919
  }));
757
920
  } else {
758
- cb.appendChild(EL('p', {
759
- className: 'muted',
760
- style: 'font-size:13px;margin-bottom:16px',
761
- text: String(observedSources) + ' sources with observed mean quality ' +
762
- Number(aggregate.mean_quality).toFixed(3) + '.',
763
- }));
764
- entries.forEach(function (k) {
765
- var v = Number(scores[k]);
766
- var row = EL('div', { style: 'margin-bottom:14px' });
767
- var meta = EL('div', {
768
- style: 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px',
769
- });
770
- meta.appendChild(EL('span', { className: 'mono', text: k }));
771
- meta.appendChild(EL('b', { className: 'num', text: v.toFixed(2) }));
772
- row.appendChild(meta);
773
- row.appendChild(meter(v * 100));
774
- cb.appendChild(row);
921
+ // allAtPrior: every source at exactly 0.50 means the system has not yet accumulated
922
+ // enough outcome data to shift away from the initialization value. Showing 18 identical
923
+ // rows would mislead a non-technical reader into thinking quality was measured.
924
+ var allAtPrior = entries.every(function (k) {
925
+ return Math.abs(Number(scores[k]) - 0.5) < 0.005;
775
926
  });
927
+ if (allAtPrior) {
928
+ cb.appendChild(EL('p', {
929
+ className: 'muted',
930
+ style: 'font-size:13px;margin-bottom:16px',
931
+ text: String(observedSources) + ' source' + (observedSources === 1 ? '' : 's') +
932
+ ' observed. Quality scores start at 0.5 and shift once recall shows ' +
933
+ 'which sources consistently produce better results. No quality signal has settled yet.',
934
+ }));
935
+ } else {
936
+ cb.appendChild(EL('p', {
937
+ className: 'muted',
938
+ style: 'font-size:13px;margin-bottom:16px',
939
+ text: String(observedSources) + ' source' + (observedSources === 1 ? '' : 's') +
940
+ ' · average quality ' + Number(aggregate.mean_quality).toFixed(2) + '.',
941
+ }));
942
+ entries.forEach(function (k) {
943
+ var v = Number(scores[k]);
944
+ var displayName = humanizeSource(k);
945
+ var row = EL('div', { style: 'margin-bottom:14px' });
946
+ var meta = EL('div', {
947
+ style: 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px',
948
+ });
949
+ meta.appendChild(EL('span', { text: displayName }));
950
+ meta.appendChild(EL('b', { className: 'num', text: v.toFixed(2) }));
951
+ row.appendChild(meta);
952
+ row.appendChild(meter(v * 100));
953
+ cb.appendChild(row);
954
+ });
955
+ }
776
956
  }
777
957
  card.appendChild(cb); sec.appendChild(card);
778
958
  return sec;
@@ -880,7 +1060,7 @@
880
1060
  buildOverview(learning, behavioral, dateMap, living),
881
1061
  buildReward(behavioral),
882
1062
  buildBehaviour(learning, behavioral),
883
- buildClients(living, brain.cross_platform),
1063
+ buildClients(living, brain.cross_platform, brain.bounded_loops),
884
1064
  buildSourceQuality(learning, living)
885
1065
  );
886
1066
  wireTabs(container);