superlocalmemory 4.0.4 → 4.0.6

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 (90) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +23 -14
  3. package/ide/configs/codex-mcp.toml +2 -2
  4. package/package.json +3 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.mcp.json +1 -0
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +3 -2
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +2 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +6 -5
  26. package/plugin-src/skills/slm-graph/SKILL.md +2 -1
  27. package/plugin-src/skills/slm-profile/SKILL.md +1 -0
  28. package/pyproject.toml +1 -1
  29. package/src/superlocalmemory/__init__.py +1 -1
  30. package/src/superlocalmemory/access/rbac.py +106 -0
  31. package/src/superlocalmemory/brain/__init__.py +5 -0
  32. package/src/superlocalmemory/brain/truth.py +418 -0
  33. package/src/superlocalmemory/cli/__main__.py +17 -0
  34. package/src/superlocalmemory/cli/commands.py +96 -28
  35. package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
  36. package/src/superlocalmemory/cli/gdpr_io.py +109 -0
  37. package/src/superlocalmemory/cli/main.py +88 -0
  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/context_cache.py +58 -1
  44. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  45. package/src/superlocalmemory/core/maintenance.py +43 -1
  46. package/src/superlocalmemory/core/mutations.py +155 -25
  47. package/src/superlocalmemory/core/recall_pipeline.py +6 -10
  48. package/src/superlocalmemory/core/recall_worker.py +33 -12
  49. package/src/superlocalmemory/core/remember_runtime.py +271 -2
  50. package/src/superlocalmemory/core/store_pipeline.py +100 -38
  51. package/src/superlocalmemory/encoding/consolidator.py +17 -47
  52. package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
  53. package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
  54. package/src/superlocalmemory/infra/backup.py +138 -0
  55. package/src/superlocalmemory/infra/backup_obligations.py +423 -0
  56. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +4 -3
  57. package/src/superlocalmemory/learning/engagement.py +165 -0
  58. package/src/superlocalmemory/mcp/profiles.py +19 -7
  59. package/src/superlocalmemory/mcp/server.py +4 -2
  60. package/src/superlocalmemory/mcp/tools_brain.py +54 -10
  61. package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
  62. package/src/superlocalmemory/mcp/tools_core.py +88 -3
  63. package/src/superlocalmemory/mcp/tools_v3.py +20 -6
  64. package/src/superlocalmemory/retrieval/engine.py +28 -10
  65. package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
  66. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
  67. package/src/superlocalmemory/server/routes/brain.py +297 -14
  68. package/src/superlocalmemory/server/routes/learning.py +13 -25
  69. package/src/superlocalmemory/server/routes/memories.py +129 -3
  70. package/src/superlocalmemory/server/routes/v3_api.py +171 -60
  71. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  72. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  73. package/src/superlocalmemory/storage/correction_cases.py +670 -0
  74. package/src/superlocalmemory/storage/database.py +230 -24
  75. package/src/superlocalmemory/storage/migration_runner.py +7 -0
  76. package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
  77. package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
  78. package/src/superlocalmemory/storage/models.py +12 -4
  79. package/src/superlocalmemory/storage/write_coordinator.py +4 -0
  80. package/src/superlocalmemory/summaries/__init__.py +37 -0
  81. package/src/superlocalmemory/summaries/base.py +108 -0
  82. package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
  83. package/src/superlocalmemory/summaries/project_work_log.py +424 -0
  84. package/src/superlocalmemory/summaries/session_summary.py +307 -0
  85. package/src/superlocalmemory/ui/css/design-system.css +76 -1
  86. package/src/superlocalmemory/ui/index.html +28 -11
  87. package/src/superlocalmemory/ui/js/brain.js +43 -7
  88. package/src/superlocalmemory/ui/js/od-agents.js +49 -5
  89. package/src/superlocalmemory/ui/js/od-brain.js +280 -84
  90. 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,35 +255,53 @@
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;
229
265
  var pCount = ((beh.patterns) || []).length;
230
- var feedback = (living && living.feedback) || {};
266
+ // BrainTruth is the portable V4.0.5 source of truth. Legacy sections
267
+ // remain a rolling-upgrade fallback only.
268
+ var truth = (living && living.brain_truth) || {};
269
+ var memoryActivity = truth.memory_activity || {};
270
+ var feedback = truth.feedback || (living && living.feedback) || {};
271
+ var experience = truth.agent_experience || (living && living.agent_experience) || {};
272
+ var externalEvidence = truth.external_evidence || experience.external_graph_evidence || {};
273
+ var correctionQuality = truth.correction_quality || {};
231
274
  var graph = (living && living.graph) || {};
232
- var experience = (living && living.agent_experience) || {};
233
- var externalEvidence = experience.external_graph_evidence || {};
275
+
276
+ function truthCount(section, key, unit) {
277
+ if (!section || section.availability === 'unavailable') {
278
+ return 'Unavailable' + (section && section.reason ? ': ' + section.reason : '');
279
+ }
280
+ var value = section[key];
281
+ return value == null ? 'No data yet' : String(value) + (unit ? ' ' + unit : '');
282
+ }
234
283
 
235
284
  // KPI strip
236
285
  var strip = EL('div', { className: 'kpi-strip', style: 'margin-bottom:16px' });
237
286
  // Ranking phase: text label → isNumeric=false (font-size:24px to match design)
238
287
  strip.appendChild(kpiCard('skill', 'Ranking phase', phaseLabel(phase),
239
288
  phaseDelta, modelActive, undefined, false));
240
- // Feedback signals: numeric isNumeric=true
241
- strip.appendChild(kpiCard('optimize', 'Feedback signals', fmtNum(signals),
242
- '▲ ' + 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));
243
297
  // Engagement health: text label → isNumeric=false
244
298
  strip.appendChild(kpiCard('health', 'Engagement health',
245
299
  healthStatus.charAt(0) + healthStatus.slice(1).toLowerCase(),
246
- (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',
247
301
  healthStatus === 'HEALTHY', healthColor, false));
248
302
  // Patterns: numeric → isNumeric=true
249
303
  strip.appendChild(kpiCard('brain', 'Patterns learned', String(pCount),
250
- '▲ ' + (beh.cross_project_transfers || 0) + ' transferable', pCount > 0, undefined, true));
304
+ (beh.cross_project_transfers || 0) + ' used across projects', pCount > 0, undefined, true));
251
305
  sec.appendChild(strip);
252
306
 
253
307
  // 2-column grid
@@ -263,8 +317,17 @@
263
317
  var pmeta = EL('div', {
264
318
  style: 'display:flex;justify-content:space-between;font-size:12px;color:var(--fg-2);margin-bottom:8px',
265
319
  });
266
- pmeta.appendChild(EL('span', { text: fmtNum(signals) + ' / ' + fmtNum(mlGate) + ' signals' }));
267
- 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
+ }
268
331
  pb.appendChild(pmeta);
269
332
  pb.appendChild(meter(pct));
270
333
  var phasesRow = EL('div', {
@@ -314,13 +377,14 @@
314
377
  ['Models trained', String(stats.models_trained || 0)],
315
378
  ['Verified active models', String(stats.models_active_verified || 0)],
316
379
  ['Sources tracked', String(stats.tracked_sources || 0)],
317
- ['Explicit feedback', String(feedback.explicit_signals || 0)],
318
- ['Settled outcomes', String(feedback.settled_outcomes || 0)],
319
- ['Claimed evidence authority', String(experience.claimed_evidence_experiences || 0)],
320
- ['Cognitive turns', String(experience.turns_total || 0) +
321
- ' · ' + String((experience.turns_by_state || {}).finalized || 0) + ' finalized'],
322
- ['Bounded Loop observations', String(externalEvidence.total || 0) +
323
- (externalEvidence.is_real ? ' terminal receipts' : ' unavailable')],
380
+ ['Memory activity', truthCount(memoryActivity, 'facts_total', 'facts')],
381
+ ['Feedback signals', truthCount(feedback, 'signals_total', 'signals')],
382
+ ['Claimed evidence', truthCount(experience, 'claimed_experiences_total', '')],
383
+ ['Independently verified evidence', truthCount(
384
+ experience, 'independently_verified_experiences_total', '',
385
+ )],
386
+ ['External observations', truthCount(externalEvidence, 'receipts_total', '')],
387
+ ['Correction quality', truthCount(correctionQuality, 'cases_total', '')],
324
388
  ['Graph evidence', String(graph.fact_nodes || 0) + ' nodes · ' +
325
389
  String(graph.association_edges || 0) + ' edges'],
326
390
  ].forEach(function (row) {
@@ -353,24 +417,29 @@
353
417
  style: 'margin:0 0 14px;font-size:12px;line-height:1.55',
354
418
  text: 'SLM records completed work when an integration supplies evidence. These records do not change recall, ranking, or model routing by themselves.',
355
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);
356
427
  var evGrid = EL('div', { className: 'kpi-strip', style: 'margin:0' });
357
- evGrid.appendChild(kpiCard('fact_check', 'Recorded experiences',
358
- fmtNum(experience.experiences_total || 0), 'profile-scoped durable receipts',
359
- Number(experience.experiences_total || 0) > 0, undefined, true));
360
- evGrid.appendChild(kpiCard('verified', 'Claimed evidence authority',
361
- fmtNum(experience.claimed_evidence_experiences || 0), 'declared by the producing host',
362
- Number(experience.claimed_evidence_experiences || 0) > 0, undefined, true));
363
- evGrid.appendChild(kpiCard('account_tree', 'Cognitive turns',
364
- fmtNum(experience.turns_total || 0),
365
- fmtNum((experience.turns_by_state || {}).open || 0) + ' open · ' +
366
- fmtNum((experience.turns_by_state || {}).finalized || 0) + ' finalized',
367
- Number(experience.turns_total || 0) > 0, undefined, true));
368
- evGrid.appendChild(kpiCard('account_tree', 'Bounded Loop observations',
369
- fmtNum(externalEvidence.total || 0),
370
- externalEvidence.is_real
371
- ? fmtNum(externalEvidence.demonstrations || 0) + ' demonstrations · no automatic learning'
372
- : 'connect Bounded Loops to observe terminal runs',
373
- Number(externalEvidence.total || 0) > 0, undefined, true));
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',
437
+ truthCount(externalEvidence, 'receipts_total', ''),
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));
374
443
  evb.appendChild(evGrid);
375
444
  evc.appendChild(evb);
376
445
  sec.appendChild(evc);
@@ -411,7 +480,7 @@
411
480
  hmhr.appendChild(EL('h3', { text: 'Reward signal density' }));
412
481
  hmhr.appendChild(EL('span', {
413
482
  className: 'sub',
414
- 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',
415
484
  }));
416
485
  hmhr.appendChild(EL('div', { className: 'spacer' }));
417
486
  hmhr.appendChild(heatLegend());
@@ -423,16 +492,26 @@
423
492
  hmcr.appendChild(hmbr);
424
493
  sec.appendChild(hmcr);
425
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
+
426
505
  // 2-column: sparkline + outcome mix
427
506
  var grid = EL('div', { className: 'grid', style: 'grid-template-columns:1fr 1fr;align-items:start' });
428
507
 
429
- // Average settled reward and real daily series
508
+ // Recall quality card: average reward score + real daily sparkline
430
509
  var fbCard = EL('div', { className: 'card' });
431
510
  var fbH = EL('div', { className: 'card-head' });
432
- fbH.appendChild(EL('h3', { text: 'Average settled reward' }));
511
+ fbH.appendChild(EL('h3', { text: 'Recall quality' }));
433
512
  fbH.appendChild(EL('span', {
434
513
  className: 'sub',
435
- text: fmtNum(reward.count || 0) + ' finalized labels',
514
+ text: fmtNum(reward.count || 0) + (isUnmeasuredPrior ? ' interactions · default score' : ' interactions'),
436
515
  }));
437
516
  fbCard.appendChild(fbH);
438
517
  var fbB = EL('div', { className: 'card-pad' });
@@ -442,6 +521,13 @@
442
521
  style: 'font-size:30px;margin-bottom:12px',
443
522
  text: Number(reward.average).toFixed(3),
444
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
+ }
445
531
  }
446
532
  var fbSp = EL('div', { id: 'od-brain-sp-fb' });
447
533
  var sparkVals = timeline.slice(-30).map(function (point) {
@@ -455,7 +541,7 @@
455
541
  fbSp.appendChild(EL('p', {
456
542
  className: 'muted',
457
543
  style: 'padding:32px;text-align:center;font-size:13px',
458
- text: 'No settled reward history is available yet.',
544
+ text: 'No recall history is available yet.',
459
545
  }));
460
546
  }
461
547
  fbB.appendChild(fbSp);
@@ -466,16 +552,17 @@
466
552
  var outCard = EL('div', { className: 'card' });
467
553
  var outH = EL('div', { className: 'card-head' });
468
554
  outH.appendChild(EL('h3', { text: 'Reward distribution' }));
469
- 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' }));
470
556
  outCard.appendChild(outH);
471
557
  var outB = EL('div', { className: 'card-pad', id: 'od-brain-outcomes' });
472
- var total = Number(reward.count || 0);
473
- var bd = reward.distribution || {};
474
- if (total === 0) {
558
+ if (total === 0 || isUnmeasuredPrior) {
475
559
  outB.appendChild(EL('p', {
476
560
  className: 'muted',
477
561
  style: 'padding:16px;text-align:center;font-size:13px',
478
- 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.',
479
566
  }));
480
567
  } else {
481
568
  [
@@ -514,7 +601,7 @@
514
601
  var tc = EL('div', { className: 'card' });
515
602
  var tch = EL('div', { className: 'card-head' });
516
603
  tch.appendChild(EL('h3', { text: 'Tech preferences' }));
517
- tch.appendChild(EL('span', { className: 'sub', text: 'Layer 1 · confidence-weighted' }));
604
+ tch.appendChild(EL('span', { className: 'sub', text: 'your tools and technology preferences' }));
518
605
  tc.appendChild(tch);
519
606
  var tcb = EL('div', { className: 'card-pad' });
520
607
  var techItems = l.tech_preferences || [];
@@ -555,7 +642,7 @@
555
642
  var wc = EL('div', { className: 'card' });
556
643
  var wch = EL('div', { className: 'card-head' });
557
644
  wch.appendChild(EL('h3', { text: 'Workflow patterns' }));
558
- 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' }));
559
646
  wc.appendChild(wch);
560
647
  var wcb = EL('div', { className: 'card-pad' });
561
648
  var wfPats = l.workflow_patterns || [];
@@ -656,11 +743,26 @@
656
743
  // ======================================================================
657
744
  // Tab: CONNECTED CLIENTS
658
745
  // ======================================================================
659
- 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) {
660
758
  var sec = EL('section', { className: 'tabpane', 'data-p': 'clients' });
661
- 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;
662
763
  var configuredData = configured || {};
663
764
 
765
+ // ── Recent client activity ────────────────────────────────────────────
664
766
  // Activity is presence reported by host lifecycle hooks, not the old
665
767
  // tool-event proxy. A configured adapter and a recent client are two
666
768
  // different truths, rendered as separate cards.
@@ -670,8 +772,46 @@
670
772
  evh.appendChild(EL('span', { className: 'sub', text: 'host lifecycle presence · last 5 minutes' }));
671
773
  evc.appendChild(evh);
672
774
  var evb = EL('div', { className: 'card-pad' });
673
- if (clients.length === 0) {
674
- 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',
675
815
  text: 'No host activity in the last 5 minutes. This does not mean an integration is uninstalled.',
676
816
  }));
677
817
  } else {
@@ -679,7 +819,7 @@
679
819
  var row = EL('div', { className: 'list-row' });
680
820
  row.appendChild(EL('b', { style: 'flex:1', text: String(client.kind || 'other') }));
681
821
  row.appendChild(EL('span', { className: 'muted',
682
- text: 'active ' + Number(client.last_seen_seconds_ago || 0) + 's ago',
822
+ text: 'active ' + fmtSecondsAgo(client.last_seen_seconds_ago),
683
823
  }));
684
824
  evb.appendChild(row);
685
825
  });
@@ -687,8 +827,11 @@
687
827
  evc.appendChild(evb);
688
828
  sec.appendChild(evc);
689
829
 
690
- // Configured integrations are installation/sync state, not client activity.
691
- 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' });
692
835
  var tch = EL('div', { className: 'card-head' });
693
836
  tch.appendChild(EL('h3', { text: 'Configured integrations' }));
694
837
  tch.appendChild(EL('span', { className: 'sub', text: 'installation and sync availability' }));
@@ -698,14 +841,50 @@
698
841
  var state = configuredData[kind] || {};
699
842
  var row = EL('div', { className: 'list-row' });
700
843
  row.appendChild(EL('span', { style: 'flex:1', text: kind.replace(/_/g, ' ') }));
701
- 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', {
702
848
  className: 'badge ' + (state.active ? 'ok' : 'warn'),
703
- text: state.active ? 'available' : (state.reason || 'not available'),
704
- }));
849
+ text: badgeText,
850
+ });
851
+ if (state.evidence) badgeEl.setAttribute('title', String(state.evidence));
852
+ row.appendChild(badgeEl);
705
853
  tcb.appendChild(row);
706
854
  });
707
855
  tc.appendChild(tcb);
708
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
+
709
888
  return sec;
710
889
  }
711
890
 
@@ -721,7 +900,7 @@
721
900
  var card = EL('div', { className: 'card' });
722
901
  var ch = EL('div', { className: 'card-head' });
723
902
  ch.appendChild(EL('h3', { text: 'Source quality' }));
724
- 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' }));
725
904
  card.appendChild(ch);
726
905
  var cb = EL('div', { className: 'card-pad' });
727
906
  var aggregate = (living && living.source_quality) || {};
@@ -736,27 +915,44 @@
736
915
  cb.appendChild(EL('p', {
737
916
  className: 'muted',
738
917
  style: 'padding:16px;text-align:center;font-size:13px',
739
- 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.',
740
919
  }));
741
920
  } else {
742
- cb.appendChild(EL('p', {
743
- className: 'muted',
744
- style: 'font-size:13px;margin-bottom:16px',
745
- text: String(observedSources) + ' sources with observed mean quality ' +
746
- Number(aggregate.mean_quality).toFixed(3) + '.',
747
- }));
748
- entries.forEach(function (k) {
749
- var v = Number(scores[k]);
750
- var row = EL('div', { style: 'margin-bottom:14px' });
751
- var meta = EL('div', {
752
- style: 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px',
753
- });
754
- meta.appendChild(EL('span', { className: 'mono', text: k }));
755
- meta.appendChild(EL('b', { className: 'num', text: v.toFixed(2) }));
756
- row.appendChild(meta);
757
- row.appendChild(meter(v * 100));
758
- 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;
759
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
+ }
760
956
  }
761
957
  card.appendChild(cb); sec.appendChild(card);
762
958
  return sec;
@@ -854,8 +1050,8 @@
854
1050
  var head = EL('div', { className: 'page-head' });
855
1051
  head.appendChild(EL('h2', { text: 'The living brain' }));
856
1052
  head.appendChild(EL('p', {
857
- text: 'How your memory is getting smarter — ranking phase, the reward signal it learns from, ' +
858
- 'and the behavioural patterns it has extracted. Everything trained on-device from your own usage.',
1053
+ text: 'A local view of memory activity, feedback, and evidence. Observations are shown separately ' +
1054
+ 'from ranking and do not change recall, ranking, or model routing by themselves.',
859
1055
  }));
860
1056
 
861
1057
  container.replaceChildren(
@@ -864,7 +1060,7 @@
864
1060
  buildOverview(learning, behavioral, dateMap, living),
865
1061
  buildReward(behavioral),
866
1062
  buildBehaviour(learning, behavioral),
867
- buildClients(living, brain.cross_platform),
1063
+ buildClients(living, brain.cross_platform, brain.bounded_loops),
868
1064
  buildSourceQuality(learning, living)
869
1065
  );
870
1066
  wireTabs(container);