superlocalmemory 4.0.9 → 4.1.0

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +245 -0
  3. package/README.md +7 -7
  4. package/package.json +4 -2
  5. package/plugin/.claude-plugin/plugin.json +2 -2
  6. package/plugin/CLAUDE.md +3 -3
  7. package/plugin/agents/slm-governance-advisor.md +1 -1
  8. package/plugin/agents/slm-loop-runner.md +4 -4
  9. package/plugin/agents/slm-memory-advisor.md +1 -1
  10. package/plugin/agents/slm-optimize-advisor.md +1 -1
  11. package/plugin/requirements.txt +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +2 -2
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +5 -5
  19. package/plugin/skills/slm-recall/SKILL.md +102 -15
  20. package/plugin/skills/slm-remember/SKILL.md +35 -3
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +29 -3
  23. package/plugin/skills/slm-status/SKILL.md +1 -1
  24. package/plugin-src/rules/AGENTS.md +16 -8
  25. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-loop/SKILL.md +2 -2
  30. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-profile/SKILL.md +5 -5
  32. package/plugin-src/skills/slm-recall/SKILL.md +102 -15
  33. package/plugin-src/skills/slm-remember/SKILL.md +35 -3
  34. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +29 -3
  36. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  37. package/pyproject.toml +1 -1
  38. package/src/superlocalmemory/__init__.py +1 -1
  39. package/src/superlocalmemory/cli/commands.py +308 -20
  40. package/src/superlocalmemory/cli/daemon.py +30 -0
  41. package/src/superlocalmemory/cli/db_migrate.py +71 -1
  42. package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
  43. package/src/superlocalmemory/cli/main.py +26 -4
  44. package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
  45. package/src/superlocalmemory/code_graph/database.py +44 -0
  46. package/src/superlocalmemory/compliance/gdpr.py +449 -39
  47. package/src/superlocalmemory/core/admission.py +231 -11
  48. package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
  49. package/src/superlocalmemory/core/config.py +90 -11
  50. package/src/superlocalmemory/core/consolidation_engine.py +34 -0
  51. package/src/superlocalmemory/core/engine.py +140 -11
  52. package/src/superlocalmemory/core/fact_consolidator.py +316 -125
  53. package/src/superlocalmemory/core/graph_analyzer.py +76 -112
  54. package/src/superlocalmemory/core/graph_metrics.py +597 -0
  55. package/src/superlocalmemory/core/graph_pruner.py +121 -0
  56. package/src/superlocalmemory/core/maintenance.py +44 -6
  57. package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
  58. package/src/superlocalmemory/core/memory_health.py +266 -0
  59. package/src/superlocalmemory/core/mode_capability.py +111 -0
  60. package/src/superlocalmemory/core/ollama_validator.py +315 -0
  61. package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
  62. package/src/superlocalmemory/core/operation_request.py +1 -1
  63. package/src/superlocalmemory/core/ops_remediation.py +2 -2
  64. package/src/superlocalmemory/core/projection_drain.py +380 -0
  65. package/src/superlocalmemory/core/recall_pipeline.py +390 -3
  66. package/src/superlocalmemory/core/recall_worker.py +6 -3
  67. package/src/superlocalmemory/core/scale_autopromote.py +196 -0
  68. package/src/superlocalmemory/core/scale_engine.py +16 -2
  69. package/src/superlocalmemory/core/score_contract.py +21 -1
  70. package/src/superlocalmemory/core/session_identity.py +85 -0
  71. package/src/superlocalmemory/core/status_contract.py +108 -0
  72. package/src/superlocalmemory/core/store_pipeline.py +78 -3
  73. package/src/superlocalmemory/core/worker_pool.py +4 -4
  74. package/src/superlocalmemory/core/working_memory.py +288 -0
  75. package/src/superlocalmemory/encoding/cognitive_consolidator.py +51 -7
  76. package/src/superlocalmemory/encoding/context_generator.py +1 -1
  77. package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
  78. package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
  79. package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
  80. package/src/superlocalmemory/encoding/type_router.py +12 -12
  81. package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
  82. package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
  83. package/src/superlocalmemory/graph/cozo_backend.py +103 -138
  84. package/src/superlocalmemory/hooks/portable_kit.py +10 -2
  85. package/src/superlocalmemory/learning/bandit.py +43 -0
  86. package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
  87. package/src/superlocalmemory/learning/database.py +60 -3
  88. package/src/superlocalmemory/learning/entity_compiler.py +21 -58
  89. package/src/superlocalmemory/learning/feedback.py +3 -1
  90. package/src/superlocalmemory/learning/outcomes.py +47 -16
  91. package/src/superlocalmemory/learning/pattern_miner.py +28 -3
  92. package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
  93. package/src/superlocalmemory/learning/pcos.py +291 -0
  94. package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
  95. package/src/superlocalmemory/learning/reward_proxy.py +100 -10
  96. package/src/superlocalmemory/learning/signal_kinds.py +79 -0
  97. package/src/superlocalmemory/mcp/profiles.py +14 -2
  98. package/src/superlocalmemory/mcp/server.py +1 -1
  99. package/src/superlocalmemory/mcp/session_binding.py +92 -0
  100. package/src/superlocalmemory/mcp/tools_active.py +2 -1
  101. package/src/superlocalmemory/mcp/tools_core.py +71 -42
  102. package/src/superlocalmemory/mcp/tools_ops.py +2 -2
  103. package/src/superlocalmemory/mcp/tools_v28.py +20 -1
  104. package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
  105. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
  106. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -11
  107. package/src/superlocalmemory/retrieval/channel_status.py +117 -0
  108. package/src/superlocalmemory/retrieval/engine.py +106 -11
  109. package/src/superlocalmemory/retrieval/entity_channel.py +217 -257
  110. package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
  111. package/src/superlocalmemory/retrieval/scope_policy.py +42 -1
  112. package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
  113. package/src/superlocalmemory/retrieval/spreading.py +288 -0
  114. package/src/superlocalmemory/retrieval/temporal_channel.py +13 -1
  115. package/src/superlocalmemory/retrieval/vector_store.py +63 -0
  116. package/src/superlocalmemory/server/api.py +26 -2
  117. package/src/superlocalmemory/server/asset_versions.py +171 -0
  118. package/src/superlocalmemory/server/bandit_loops.py +17 -1
  119. package/src/superlocalmemory/server/rbac_enforce.py +26 -6
  120. package/src/superlocalmemory/server/recall_serializer.py +9 -0
  121. package/src/superlocalmemory/server/routes/abstraction.py +201 -0
  122. package/src/superlocalmemory/server/routes/behavioral.py +75 -10
  123. package/src/superlocalmemory/server/routes/compliance.py +98 -18
  124. package/src/superlocalmemory/server/routes/config_api.py +186 -4
  125. package/src/superlocalmemory/server/routes/data_io.py +29 -1
  126. package/src/superlocalmemory/server/routes/entity.py +13 -1
  127. package/src/superlocalmemory/server/routes/evolution.py +178 -0
  128. package/src/superlocalmemory/server/routes/ingest.py +8 -0
  129. package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
  130. package/src/superlocalmemory/server/routes/memories.py +49 -7
  131. package/src/superlocalmemory/server/routes/mesh.py +1 -1
  132. package/src/superlocalmemory/server/routes/timeline.py +4 -0
  133. package/src/superlocalmemory/server/routes/v3_api.py +193 -17
  134. package/src/superlocalmemory/server/ui.py +24 -1
  135. package/src/superlocalmemory/server/unified_daemon.py +292 -9
  136. package/src/superlocalmemory/storage/_migration_internals.py +35 -0
  137. package/src/superlocalmemory/storage/_schema_version.py +24 -3
  138. package/src/superlocalmemory/storage/database.py +598 -82
  139. package/src/superlocalmemory/storage/embedding_codec.py +71 -0
  140. package/src/superlocalmemory/storage/lineage_retention.py +236 -0
  141. package/src/superlocalmemory/storage/logical_edges.py +43 -2
  142. package/src/superlocalmemory/storage/migration_runner.py +130 -0
  143. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
  144. package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
  145. package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
  146. package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
  147. package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
  148. package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
  149. package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
  150. package/src/superlocalmemory/storage/migrations.py +18 -2
  151. package/src/superlocalmemory/storage/models.py +40 -1
  152. package/src/superlocalmemory/storage/projection_outbox.py +346 -0
  153. package/src/superlocalmemory/storage/retention_policy.py +860 -0
  154. package/src/superlocalmemory/storage/schema.py +110 -1
  155. package/src/superlocalmemory/storage/write_coordinator.py +19 -2
  156. package/src/superlocalmemory/summaries/base.py +1 -1
  157. package/src/superlocalmemory/summaries/non_answer.py +223 -0
  158. package/src/superlocalmemory/trust/scorer.py +43 -1
  159. package/src/superlocalmemory/ui/index.html +10 -19
  160. package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
  161. package/src/superlocalmemory/ui/js/od-health.js +28 -6
  162. package/src/superlocalmemory/ui/js/od-memories.js +209 -1
  163. package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
  164. package/src/superlocalmemory/ui/js/od-settings.js +87 -1
  165. package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
@@ -189,6 +189,18 @@
189
189
  '</select>' +
190
190
  '</div>' +
191
191
  '<div id="' + id + '-sum-out" style="font-size:13px;color:var(--fg-2)">Pick a summary above.</div>' +
192
+ // Knowledge Overview.
193
+ //
194
+ // The Today/Yesterday buttons above are a date filter over
195
+ // atomic_facts and have never had any link to a cluster, so the
196
+ // Summaries tab could not show what the store actually knows ABOUT
197
+ // anything. This card fills that: cluster summaries, read from the
198
+ // display-only table, which is where they belong now that they are
199
+ // out of the retrieval corpus.
200
+ '<div id="' + id + '-kover" style="margin-top:26px;' +
201
+ 'border-top:1px solid var(--border);padding-top:20px">' +
202
+ _loading('Loading what your memory knows…') +
203
+ '</div>' +
192
204
  '</div>' +
193
205
  // Recall Lab pane — exact IDs required by recall-lab.js:
194
206
  // #recall-lab-query (input), #recall-lab-search (button — click check),
@@ -363,7 +375,7 @@
363
375
  if (pane) pane.classList.add('active');
364
376
  if (tab === 'timeline') _loadTimeline(id);
365
377
  if (tab === 'clusters') _loadClusters(id);
366
- if (tab === 'summary') _loadProjectOptions(id);
378
+ if (tab === 'summary') { _loadProjectOptions(id); _loadKnowledgeOverview(id); }
367
379
  }
368
380
 
369
381
  /* Populate the project picker from projects SLM has actually recorded.
@@ -446,6 +458,25 @@
446
458
  meta.style.cssText = 'margin-top:10px;font-size:12px;color:var(--fg-3);line-height:1.6';
447
459
  meta.textContent = _summaryProvenance(d);
448
460
  out.appendChild(meta);
461
+
462
+ /* Why it came out this way, and what to do about it.
463
+ *
464
+ * The provenance line above says a summary was assembled from the
465
+ * user's own notes rather than written. It never said why. Someone in
466
+ * Mode A — which has no language model by design — saw a plainer
467
+ * result than they expected with no way to learn that a written one
468
+ * needs a model, and would reasonably read the feature as broken.
469
+ */
470
+ var cap = d.capability;
471
+ if (cap && cap.message) {
472
+ var hint = document.createElement('div');
473
+ hint.style.cssText = 'margin-top:10px;font-size:12px;line-height:1.6;' +
474
+ 'color:var(--fg-2);background:var(--card-2);border:1px solid var(--border);' +
475
+ 'border-left:3px solid var(--accent, var(--border));' +
476
+ 'border-radius:var(--r-md);padding:10px 12px';
477
+ hint.textContent = cap.message;
478
+ out.appendChild(hint);
479
+ }
449
480
  })
450
481
  .catch(function (e) {
451
482
  out.textContent = 'Could not build summary: ' + (e && e.message ? e.message : 'error');
@@ -815,6 +846,183 @@
815
846
 
816
847
  // ── Clusters fetch & render ──────────────────────────────────────────────────
817
848
 
849
+ /* Knowledge Overview — cluster summaries, plus an honest health line.
850
+ *
851
+ * Two rules this card exists to obey.
852
+ *
853
+ * IT READS ONLY THE DISPLAY TABLE. These summaries were in the retrieval
854
+ * corpus until 4.0.10, where they out-ranked the user's own words. Moving
855
+ * them out is only worth anything if exactly one surface shows them, and
856
+ * this is that surface.
857
+ *
858
+ * IT DOES NOT DRESS UP JUNK AS INSIGHT. Some stored summaries are a model's
859
+ * non-answer -- "Unfortunately, there is no information available about
860
+ * 'State'..." -- and rendering those as a heading called Knowledge Overview
861
+ * makes the product look broken. The endpoint labels each row's quality; the
862
+ * usable ones are shown and the rest are counted in one plain sentence. Not
863
+ * hidden: hiding them would hide the very problem the release fixes, and a
864
+ * reader who is told "3 could not be generated" trusts the other 47 more.
865
+ */
866
+ function _loadKnowledgeOverview(id) {
867
+ var box = document.getElementById(id + '-kover');
868
+ if (!box) return;
869
+ // Guarded on the element, not a module flag — same reason as
870
+ // _loadProjectOptions: a stale "already loaded" boolean outlives the DOM
871
+ // it described and leaves a permanent spinner after a re-render.
872
+ if (box.dataset.loaded === '1') return;
873
+
874
+ Promise.all([
875
+ fetch('/api/v3/abstraction/consolidated?limit=24')
876
+ .then(function (r) { return r.ok ? r.json() : { summaries: [], unusable: 0 }; })
877
+ .catch(function () { return { summaries: [], unusable: 0 }; }),
878
+ fetch('/api/v3/abstraction/health')
879
+ .then(function (r) { return r.ok ? r.json() : null; })
880
+ .catch(function () { return null; })
881
+ ]).then(function (res) {
882
+ box.dataset.loaded = '1';
883
+ box.innerHTML = _koverHtml(res[0] || {}, res[1]);
884
+ });
885
+ }
886
+
887
+ function _koverHtml(data, health) {
888
+ var all = data.summaries || [];
889
+ var usable = all.filter(function (s) { return s.quality === 'ok'; });
890
+ var unusable = typeof data.unusable === 'number' ? data.unusable
891
+ : (all.length - usable.length);
892
+
893
+ var html = '<h3 style="font-size:15px;margin:0 0 4px">What your memory knows</h3>' +
894
+ '<p style="font-size:12px;color:var(--fg-2);margin:0 0 14px">' +
895
+ 'Written by the summarizer from groups of your memories. These are a ' +
896
+ 'view of your memory, not memories themselves — they are never returned ' +
897
+ 'as answers.' +
898
+ '</p>';
899
+
900
+ if (health) html += _koverHealth(health);
901
+
902
+ if (usable.length === 0) {
903
+ html += '<div style="padding:20px;color:var(--fg-2);font-size:13px">' +
904
+ (all.length === 0
905
+ ? 'No summaries yet. They are written in the background as related ' +
906
+ 'memories accumulate.'
907
+ : 'None of the ' + all.length + ' stored summaries came back usable. ' +
908
+ 'That usually means the summarizer had nothing in common to merge.') +
909
+ '</div>';
910
+ return html;
911
+ }
912
+
913
+ html += '<div style="display:grid;gap:12px;' +
914
+ 'grid-template-columns:repeat(auto-fill,minmax(280px,1fr))">';
915
+ usable.forEach(function (s) { html += _koverCard(s); });
916
+ html += '</div>';
917
+
918
+ var notes = [];
919
+ if (unusable > 0) {
920
+ notes.push(unusable + (unusable === 1 ? ' summary' : ' summaries') +
921
+ ' could not be generated and are not shown. The memories they were ' +
922
+ 'built from are unaffected.');
923
+ }
924
+ if (data.near_duplicates > 0) {
925
+ notes.push(data.near_duplicates + ' near-identical ' +
926
+ (data.near_duplicates === 1 ? 'summary was' : 'summaries were') +
927
+ ' collapsed into the ones above.');
928
+ }
929
+ if (notes.length) {
930
+ html += '<p style="font-size:12px;color:var(--fg-2);margin-top:12px">' +
931
+ notes.map(_esc).join(' ') + '</p>';
932
+ }
933
+ return html;
934
+ }
935
+
936
+ function _koverCard(s) {
937
+ // Every field is escaped. Anything in a memory can reach this screen: the
938
+ // summarizer merges fact content verbatim, so a memory containing markup
939
+ // would otherwise render as markup.
940
+ var body = _koverPlain(s.content);
941
+ var clipped = body.length > 320;
942
+ var shown = clipped ? body.slice(0, 320).replace(/\s+\S*$/, '') + '…' : body;
943
+
944
+ var meta = [];
945
+ if (s.source_count) {
946
+ meta.push(s.source_count + (s.source_count === 1 ? ' memory' : ' memories'));
947
+ }
948
+ var span = _koverSpan(s.source_earliest, s.source_latest);
949
+ if (span) meta.push(span);
950
+
951
+ return '<div style="background:var(--card-2);border:1px solid var(--border);' +
952
+ 'border-radius:var(--r-md);padding:14px">' +
953
+ (s.entity_name
954
+ ? '<div style="font-size:13px;font-weight:600;margin-bottom:6px">' +
955
+ _esc(s.entity_name) + '</div>'
956
+ : '') +
957
+ '<div style="font-size:13px;line-height:1.5">' + _esc(shown) + '</div>' +
958
+ (meta.length
959
+ ? '<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">' +
960
+ meta.map(_koverChip).join('') +
961
+ '</div>'
962
+ : '') +
963
+ '</div>';
964
+ }
965
+
966
+ /* Flatten a summary to plain prose for display.
967
+ *
968
+ * Mode B/C summaries come back with markdown in them — "**Audit Round**",
969
+ * "**Findings**:" — because a chat-tuned model formats for a chat window.
970
+ * Rendered as text those asterisks read as broken output to exactly the
971
+ * reader this card is for. Escaping is still what protects the page; this
972
+ * only removes the emphasis marks that carry no meaning here.
973
+ */
974
+ function _koverPlain(text) {
975
+ return String(text || '')
976
+ .replace(/```[\s\S]*?```/g, ' ') // fenced blocks: no room for them
977
+ .replace(/`([^`]*)`/g, '$1') // inline code marks
978
+ .replace(/\*\*([^*]+)\*\*/g, '$1') // bold
979
+ .replace(/(^|\s)\*([^*\s][^*]*)\*/g, '$1$2') // italic, not bullets
980
+ .replace(/^#{1,6}\s+/gm, '') // headings
981
+ .replace(/^\s*[-*+]\s+/gm, '') // list bullets
982
+ .replace(/\s+/g, ' ') // collapse whitespace + newlines
983
+ .trim();
984
+ }
985
+
986
+ /* A chip, not a keyword dump.
987
+ *
988
+ * The neighbouring community-summary API returns its `summary` field as
989
+ * "Topics: CCQ, Resume, P07…", which printed verbatim reads as a broken
990
+ * page. Counts and date spans are rendered as discrete labels instead.
991
+ */
992
+ function _koverChip(text) {
993
+ return '<span style="font-size:11px;padding:2px 8px;border-radius:999px;' +
994
+ 'background:var(--card);border:1px solid var(--border);color:var(--fg-2)">' +
995
+ _esc(text) + '</span>';
996
+ }
997
+
998
+ function _koverSpan(earliest, latest) {
999
+ var a = String(earliest || '').slice(0, 10);
1000
+ var b = String(latest || '').slice(0, 10);
1001
+ if (!a && !b) return '';
1002
+ if (!a || !b || a === b) return a || b;
1003
+ return a + ' to ' + b;
1004
+ }
1005
+
1006
+ /* Whether the memories behind all this can actually be found.
1007
+ *
1008
+ * The one question no screen answered before. A machine ran for months with
1009
+ * 43.7% of its store unreachable by meaning while every status line it showed
1010
+ * said healthy, because nothing counted the reachable share.
1011
+ */
1012
+ function _koverHealth(h) {
1013
+ var lines = h.summary || [];
1014
+ if (!lines.length) return '';
1015
+ var bad = h.healthy === false;
1016
+ return '<div style="margin:0 0 16px;padding:12px 14px;border-radius:var(--r-md);' +
1017
+ 'background:var(--card-2);border-left:3px solid ' +
1018
+ (bad ? 'var(--warn)' : 'var(--ok)') + '">' +
1019
+ lines.map(function (l) {
1020
+ return '<div style="font-size:12px;color:var(--fg-2);line-height:1.5">' +
1021
+ _esc(l) + '</div>';
1022
+ }).join('') +
1023
+ '</div>';
1024
+ }
1025
+
818
1026
  function _loadClusters(id) {
819
1027
  if (_st.cluLoaded) return;
820
1028
  var grid = document.getElementById(id + '-clu-grid');
@@ -1,4 +1,4 @@
1
- /* od-ops-health.js — Wave-3 Admin Operations & Health panel
1
+ /* od-ops-health.js — Admin Operations & Health panel
2
2
  * Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0-or-later
3
3
  *
4
4
  * Injects an "Admin: Ops Health" tab into the Governance pane tab bar
@@ -222,6 +222,71 @@
222
222
  }
223
223
 
224
224
  /* ── Layout ───────────────────────────────────────────────── */
225
+ /* Decay preview — three curves, one per source-trust level.
226
+ Shows what the kappa setting actually does before it is saved: the same
227
+ memory strength fading at three different rates depending on where the
228
+ memory came from. At kappa 0 the three curves coincide, which is the
229
+ honest picture of trust-modulation being off. */
230
+ var DECAY_TRUSTS = [
231
+ { trust: 0.0, color: '#e0245e', label: 'low trust' },
232
+ { trust: 0.5, color: '#f59e0b', label: 'medium' },
233
+ { trust: 1.0, color: '#10b981', label: 'trusted' }
234
+ ];
235
+
236
+ function buildDecayPreview() {
237
+ var wrap = el('div', null, { display:'flex', flexDirection:'column', gap:'6px' });
238
+ var cvs = el('canvas', { id: P + '-forg-decay' });
239
+ cvs.width = 300; cvs.height = 96;
240
+ Object.assign(cvs.style, {
241
+ border:'1px solid var(--border)', borderRadius:'4px', display:'block'
242
+ });
243
+ wrap.appendChild(cvs);
244
+ var key = el('div', null, { display:'flex', gap:'10px' });
245
+ DECAY_TRUSTS.forEach(function (t) {
246
+ key.appendChild(el('span', { text: t.label },
247
+ { fontSize:'10.5px', color: t.color, fontFamily:'var(--font-mono)' }));
248
+ });
249
+ wrap.appendChild(key);
250
+ return wrap;
251
+ }
252
+
253
+ /* The decay rate is per HOUR, and the window is chosen so the three curves
254
+ are actually distinguishable. At a strong memory's strength (100, the
255
+ configured maximum) retention over sixty days is indistinguishable from
256
+ zero for every trust level, so a two-month preview would draw three flat
257
+ lines along the bottom and say nothing. Three days separates them. */
258
+ var DECAY_HOURS = 72;
259
+ var DECAY_STRENGTH = 100.0;
260
+
261
+ function retentionAt(hours, kappa, trust) {
262
+ // Mirrors math/ebbinghaus.trust_modulated_retention exactly:
263
+ // lambda_eff = (1 / S) * (1 + kappa * (1 - trust))
264
+ // retention = exp(-lambda_eff * hours)
265
+ var lambdaEff = (1.0 / DECAY_STRENGTH) * (1.0 + kappa * (1.0 - trust));
266
+ return Math.exp(-lambdaEff * hours);
267
+ }
268
+
269
+ function drawDecayCurve(kappa) {
270
+ var cvs = q('forg-decay');
271
+ if (!cvs || !cvs.getContext) return;
272
+ var ctx = cvs.getContext('2d');
273
+ var W = cvs.width, H = cvs.height, STEPS = 72;
274
+ ctx.clearRect(0, 0, W, H);
275
+ DECAY_TRUSTS.forEach(function (t) {
276
+ ctx.beginPath();
277
+ for (var i = 0; i <= STEPS; i++) {
278
+ var hours = (i / STEPS) * DECAY_HOURS;
279
+ var retention = retentionAt(hours, kappa, t.trust);
280
+ var x = (i / STEPS) * (W - 2) + 1;
281
+ var y = H - 2 - retention * (H - 4);
282
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
283
+ }
284
+ ctx.strokeStyle = t.color;
285
+ ctx.lineWidth = 1.6;
286
+ ctx.stroke();
287
+ });
288
+ }
289
+
225
290
  function makeRow(lbl, desc, key, ctrl) {
226
291
  var d = el('div', { 'data-set':'', 'data-txt':(lbl + ' ' + key).toLowerCase() });
227
292
  Object.assign(d.style, { display:'flex', alignItems:'center',
@@ -719,10 +784,16 @@
719
784
  intInp.style.minWidth = '80px'; intInp.style.maxWidth = '100px';
720
785
  var statsEl = el('span', { id: P + '-forg-stats', text:'Loading…' },
721
786
  { fontSize:'12px', color:'var(--fg-2)', fontFamily:'var(--font-mono)' });
787
+ var kapR = makeRange('forg-kap', '0', '5', '0.1', '2.0');
788
+ var decayEl = buildDecayPreview();
722
789
  var saveBtn = makeBtn('forg-save', 'Save Forgetting', true);
723
790
  var st = makeSt('forg');
724
791
  var restNote = makeRestartNote('forg');
725
792
  saveBtn.addEventListener('click', saveForgetting);
793
+ kapR.querySelector('input').addEventListener('input', function () {
794
+ drawDecayCurve(parseFloat(this.value));
795
+ });
796
+ kapR.querySelector('input').addEventListener('change', saveForgetting);
726
797
  onSw.addEventListener('od-toggle', saveForgetting);
727
798
  immSw.addEventListener('od-toggle', saveForgetting);
728
799
  arcR.querySelector('input').addEventListener('change', saveForgetting);
@@ -734,6 +805,11 @@
734
805
  makeRow('Archive threshold', 'Memories below this strength move to cold archive', 'forgetting.archive_threshold', arcR),
735
806
  makeRow('Forget threshold', 'Memories below this strength are permanently deleted', 'forgetting.forget_threshold', fgtR),
736
807
  makeRow('Run every (minutes)', 'How often the forgetting engine runs in the background', 'forgetting.scheduler_interval_minutes', intInp),
808
+ makeRow('Fade untrusted sources faster (\u03ba)',
809
+ 'Memories from sources you trust less decay quicker. 0 = every memory fades at the same rate.',
810
+ 'forgetting.trust_kappa', kapR),
811
+ makeRow('Decay preview', 'Retention over 3 days at three source-trust levels',
812
+ '', decayEl),
737
813
  makeRow('Memory zone stats', '', 'forgetting.stats', statsEl),
738
814
  makeRow('Actions', '', '', bRow(saveBtn, st, restNote))
739
815
  ]);
@@ -751,6 +827,15 @@
751
827
  var fgt = q('forg-fgt');
752
828
  if (fgt) { fgt.value = d.forget_threshold || 0.05; var dF = q('forg-fgt-disp'); if (dF) dF.textContent = Number(d.forget_threshold || 0.05).toFixed(2); }
753
829
  var it = q('forg-int'); if (it) it.value = d.scheduler_interval_minutes || 30;
830
+ // == null, not ||: kappa 0 is a real setting (trust-modulation off) and
831
+ // a falsy check would silently redraw it as the 2.0 default.
832
+ var kap = q('forg-kap');
833
+ var kv = (d.trust_kappa == null) ? 2.0 : Number(d.trust_kappa);
834
+ if (kap) {
835
+ kap.value = kv;
836
+ var dK = q('forg-kap-disp'); if (dK) dK.textContent = kv.toFixed(2);
837
+ }
838
+ drawDecayCurve(kv);
754
839
  }).catch(function () {});
755
840
  fetch('/api/v3/forgetting/stats').then(function (r) { return r.ok ? r.json() : null; })
756
841
  .then(function (d) {
@@ -773,7 +858,8 @@
773
858
  core_memory_immune: q('forg-imm') ? q('forg-imm').classList.contains('on') : true,
774
859
  archive_threshold: parseFloat(q('forg-arc') ? q('forg-arc').value : '0.20'),
775
860
  forget_threshold: parseFloat(q('forg-fgt') ? q('forg-fgt').value : '0.05'),
776
- scheduler_interval_minutes: parseInt(q('forg-int') ? q('forg-int').value : '30') || 30
861
+ scheduler_interval_minutes: parseInt(q('forg-int') ? q('forg-int').value : '30') || 30,
862
+ trust_kappa: q('forg-kap') ? parseFloat(q('forg-kap').value) : 2.0
777
863
  }).then(function (r) { return r.json(); })
778
864
  .then(function (d) {
779
865
  setSt('forg', 'Saved', true);
@@ -7,8 +7,63 @@ var recallLabState = {
7
7
  perPage: 10,
8
8
  query: '',
9
9
  synthesis: '',
10
+ channelStatus: {},
10
11
  };
11
12
 
13
+ // How each channel status reads to someone looking at a thin result set.
14
+ // 'empty', 'disabled' and 'not_configured' are muted on purpose: the first is a
15
+ // valid finding and the other two are the operator's own configuration. Only a
16
+ // genuine degradation is coloured as a problem.
17
+ var CHANNEL_STATUS_STYLE = {
18
+ ok: {cls: 'bg-success', label: 'ok'},
19
+ empty: {cls: 'bg-secondary', label: 'no hits'},
20
+ disabled: {cls: 'bg-secondary', label: 'off'},
21
+ not_configured: {cls: 'bg-secondary', label: 'n/a'},
22
+ error: {cls: 'bg-danger', label: 'failed'},
23
+ timeout: {cls: 'bg-warning', label: 'timed out'},
24
+ no_embedding: {cls: 'bg-danger', label: 'no embedding'},
25
+ };
26
+
27
+ var CHANNEL_FAULTS = ['error', 'timeout', 'no_embedding'];
28
+
29
+ // Built with createElement and textContent throughout — the status strings come
30
+ // from the server, and this pane has no reason to be an injection sink.
31
+ function buildChannelHealth(statuses) {
32
+ var names = Object.keys(statuses || {});
33
+ if (names.length === 0) return null;
34
+ names.sort();
35
+
36
+ var wrap = document.createElement('div');
37
+ wrap.className = 'mt-2 d-flex flex-wrap align-items-center gap-1';
38
+
39
+ var degraded = names.filter(function(n) {
40
+ return CHANNEL_FAULTS.indexOf(statuses[n]) !== -1;
41
+ });
42
+
43
+ var lead = document.createElement('span');
44
+ lead.className = degraded.length ? 'me-1 text-danger fw-semibold' : 'me-1 text-muted';
45
+ lead.style.fontSize = '0.75rem';
46
+ // Said plainly, because the reason this strip exists is that a partly-broken
47
+ // retrieval path and a store with nothing to say produced the same answer.
48
+ lead.textContent = degraded.length
49
+ ? 'Retrieval degraded — this answer is missing what ' +
50
+ degraded.length + ' channel' + (degraded.length === 1 ? '' : 's') + ' would have found:'
51
+ : 'Channels:';
52
+ wrap.appendChild(lead);
53
+
54
+ names.forEach(function(name) {
55
+ var status = statuses[name];
56
+ var style = CHANNEL_STATUS_STYLE[status] || {cls: 'bg-dark', label: String(status)};
57
+ var badge = document.createElement('span');
58
+ badge.className = 'badge ' + style.cls;
59
+ badge.style.fontSize = '0.68rem';
60
+ badge.textContent = name + ' \u00b7 ' + style.label;
61
+ wrap.appendChild(badge);
62
+ });
63
+
64
+ return wrap;
65
+ }
66
+
12
67
  // Delegation handles the case where the recall-lab tab pane is injected after parse time.
13
68
  document.addEventListener('click', function(e) {
14
69
  if (!e.target || e.target.id !== 'recall-lab-search') return;
@@ -54,6 +109,14 @@ document.addEventListener('click', function(e) {
54
109
 
55
110
  recallLabState.allResults = data.results || [];
56
111
  recallLabState.synthesis = data.synthesis || '';
112
+ recallLabState.channelStatus = data.channel_status || {};
113
+
114
+ // Rendered BEFORE the no-results branch below, and that ordering is the
115
+ // whole point: every channel failing produces exactly zero results, so
116
+ // the case most in need of an explanation is the one that would
117
+ // otherwise show a bare 'No results found'.
118
+ var health = buildChannelHealth(recallLabState.channelStatus);
119
+ if (health) metaDiv.appendChild(health);
57
120
 
58
121
  if (recallLabState.allResults.length === 0) {
59
122
  resultsDiv.textContent = '';
@@ -118,9 +181,21 @@ function renderRecallPage() {
118
181
  numLabel.textContent = '#' + (globalIndex + 1);
119
182
  scoreRow.appendChild(numLabel);
120
183
  var scoreBadges = document.createElement('div');
121
- scoreBadges.innerHTML = '<span class="badge bg-primary me-1">Score: ' + r.score + '</span>' +
122
- '<span class="badge bg-secondary me-1">Trust: ' + r.trust_score + '</span>' +
123
- '<span class="badge bg-outline-info" style="border:1px solid #0dcaf0;color:#0dcaf0;">Conf: ' + r.confidence + '</span>';
184
+ // Built element by element rather than as an HTML string. The values are
185
+ // server-supplied, and this file's sibling panes are held to the same
186
+ // rule: this pane renders DOM, it does not parse markup.
187
+ [
188
+ ['badge bg-primary me-1', 'Score: ' + r.score, ''],
189
+ ['badge bg-secondary me-1', 'Trust: ' + r.trust_score, ''],
190
+ ['badge bg-outline-info', 'Conf: ' + r.confidence,
191
+ 'border:1px solid #0dcaf0;color:#0dcaf0;'],
192
+ ].forEach(function(spec) {
193
+ var badge = document.createElement('span');
194
+ badge.className = spec[0];
195
+ badge.textContent = spec[1];
196
+ if (spec[2]) badge.style.cssText = spec[2];
197
+ scoreBadges.appendChild(badge);
198
+ });
124
199
  scoreRow.appendChild(scoreBadges);
125
200
  item.appendChild(scoreRow);
126
201