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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +245 -0
- package/README.md +7 -7
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +4 -4
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +2 -2
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -5
- package/plugin/skills/slm-recall/SKILL.md +102 -15
- package/plugin/skills/slm-remember/SKILL.md +35 -3
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +29 -3
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +16 -8
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +2 -2
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -5
- package/plugin-src/skills/slm-recall/SKILL.md +102 -15
- package/plugin-src/skills/slm-remember/SKILL.md +35 -3
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +29 -3
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +308 -20
- package/src/superlocalmemory/cli/daemon.py +30 -0
- package/src/superlocalmemory/cli/db_migrate.py +71 -1
- package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
- package/src/superlocalmemory/cli/main.py +26 -4
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
- package/src/superlocalmemory/code_graph/database.py +44 -0
- package/src/superlocalmemory/compliance/gdpr.py +449 -39
- package/src/superlocalmemory/core/admission.py +231 -11
- package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
- package/src/superlocalmemory/core/config.py +90 -11
- package/src/superlocalmemory/core/consolidation_engine.py +34 -0
- package/src/superlocalmemory/core/engine.py +140 -11
- package/src/superlocalmemory/core/fact_consolidator.py +316 -125
- package/src/superlocalmemory/core/graph_analyzer.py +76 -112
- package/src/superlocalmemory/core/graph_metrics.py +597 -0
- package/src/superlocalmemory/core/graph_pruner.py +121 -0
- package/src/superlocalmemory/core/maintenance.py +44 -6
- package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
- package/src/superlocalmemory/core/memory_health.py +266 -0
- package/src/superlocalmemory/core/mode_capability.py +111 -0
- package/src/superlocalmemory/core/ollama_validator.py +315 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
- package/src/superlocalmemory/core/operation_request.py +1 -1
- package/src/superlocalmemory/core/ops_remediation.py +2 -2
- package/src/superlocalmemory/core/projection_drain.py +380 -0
- package/src/superlocalmemory/core/recall_pipeline.py +390 -3
- package/src/superlocalmemory/core/recall_worker.py +6 -3
- package/src/superlocalmemory/core/scale_autopromote.py +196 -0
- package/src/superlocalmemory/core/scale_engine.py +16 -2
- package/src/superlocalmemory/core/score_contract.py +21 -1
- package/src/superlocalmemory/core/session_identity.py +85 -0
- package/src/superlocalmemory/core/status_contract.py +108 -0
- package/src/superlocalmemory/core/store_pipeline.py +78 -3
- package/src/superlocalmemory/core/worker_pool.py +4 -4
- package/src/superlocalmemory/core/working_memory.py +288 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +51 -7
- package/src/superlocalmemory/encoding/context_generator.py +1 -1
- package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
- package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
- package/src/superlocalmemory/encoding/type_router.py +12 -12
- package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
- package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
- package/src/superlocalmemory/graph/cozo_backend.py +103 -138
- package/src/superlocalmemory/hooks/portable_kit.py +10 -2
- package/src/superlocalmemory/learning/bandit.py +43 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
- package/src/superlocalmemory/learning/database.py +60 -3
- package/src/superlocalmemory/learning/entity_compiler.py +21 -58
- package/src/superlocalmemory/learning/feedback.py +3 -1
- package/src/superlocalmemory/learning/outcomes.py +47 -16
- package/src/superlocalmemory/learning/pattern_miner.py +28 -3
- package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
- package/src/superlocalmemory/learning/pcos.py +291 -0
- package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
- package/src/superlocalmemory/learning/reward_proxy.py +100 -10
- package/src/superlocalmemory/learning/signal_kinds.py +79 -0
- package/src/superlocalmemory/mcp/profiles.py +14 -2
- package/src/superlocalmemory/mcp/server.py +1 -1
- package/src/superlocalmemory/mcp/session_binding.py +92 -0
- package/src/superlocalmemory/mcp/tools_active.py +2 -1
- package/src/superlocalmemory/mcp/tools_core.py +71 -42
- package/src/superlocalmemory/mcp/tools_ops.py +2 -2
- package/src/superlocalmemory/mcp/tools_v28.py +20 -1
- package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -11
- package/src/superlocalmemory/retrieval/channel_status.py +117 -0
- package/src/superlocalmemory/retrieval/engine.py +106 -11
- package/src/superlocalmemory/retrieval/entity_channel.py +217 -257
- package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
- package/src/superlocalmemory/retrieval/scope_policy.py +42 -1
- package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
- package/src/superlocalmemory/retrieval/spreading.py +288 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +13 -1
- package/src/superlocalmemory/retrieval/vector_store.py +63 -0
- package/src/superlocalmemory/server/api.py +26 -2
- package/src/superlocalmemory/server/asset_versions.py +171 -0
- package/src/superlocalmemory/server/bandit_loops.py +17 -1
- package/src/superlocalmemory/server/rbac_enforce.py +26 -6
- package/src/superlocalmemory/server/recall_serializer.py +9 -0
- package/src/superlocalmemory/server/routes/abstraction.py +201 -0
- package/src/superlocalmemory/server/routes/behavioral.py +75 -10
- package/src/superlocalmemory/server/routes/compliance.py +98 -18
- package/src/superlocalmemory/server/routes/config_api.py +186 -4
- package/src/superlocalmemory/server/routes/data_io.py +29 -1
- package/src/superlocalmemory/server/routes/entity.py +13 -1
- package/src/superlocalmemory/server/routes/evolution.py +178 -0
- package/src/superlocalmemory/server/routes/ingest.py +8 -0
- package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
- package/src/superlocalmemory/server/routes/memories.py +49 -7
- package/src/superlocalmemory/server/routes/mesh.py +1 -1
- package/src/superlocalmemory/server/routes/timeline.py +4 -0
- package/src/superlocalmemory/server/routes/v3_api.py +193 -17
- package/src/superlocalmemory/server/ui.py +24 -1
- package/src/superlocalmemory/server/unified_daemon.py +292 -9
- package/src/superlocalmemory/storage/_migration_internals.py +35 -0
- package/src/superlocalmemory/storage/_schema_version.py +24 -3
- package/src/superlocalmemory/storage/database.py +598 -82
- package/src/superlocalmemory/storage/embedding_codec.py +71 -0
- package/src/superlocalmemory/storage/lineage_retention.py +236 -0
- package/src/superlocalmemory/storage/logical_edges.py +43 -2
- package/src/superlocalmemory/storage/migration_runner.py +130 -0
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
- package/src/superlocalmemory/storage/migrations.py +18 -2
- package/src/superlocalmemory/storage/models.py +40 -1
- package/src/superlocalmemory/storage/projection_outbox.py +346 -0
- package/src/superlocalmemory/storage/retention_policy.py +860 -0
- package/src/superlocalmemory/storage/schema.py +110 -1
- package/src/superlocalmemory/storage/write_coordinator.py +19 -2
- package/src/superlocalmemory/summaries/base.py +1 -1
- package/src/superlocalmemory/summaries/non_answer.py +223 -0
- package/src/superlocalmemory/trust/scorer.py +43 -1
- package/src/superlocalmemory/ui/index.html +10 -19
- package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
- package/src/superlocalmemory/ui/js/od-health.js +28 -6
- package/src/superlocalmemory/ui/js/od-memories.js +209 -1
- package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
- package/src/superlocalmemory/ui/js/od-settings.js +87 -1
- 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')
|
|
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');
|
|
@@ -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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|