superlocalmemory 3.8.0 → 3.8.1

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 (118) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +2 -1
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/cli/commands.py +134 -7
  34. package/src/superlocalmemory/cli/daemon.py +7 -0
  35. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  36. package/src/superlocalmemory/cli/main.py +10 -4
  37. package/src/superlocalmemory/cli/version_banner.py +17 -3
  38. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  39. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  40. package/src/superlocalmemory/core/embeddings.py +119 -45
  41. package/src/superlocalmemory/core/engine.py +24 -21
  42. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  43. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  44. package/src/superlocalmemory/core/injection.py +12 -7
  45. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  46. package/src/superlocalmemory/core/recall_pipeline.py +5 -3
  47. package/src/superlocalmemory/core/store_pipeline.py +153 -20
  48. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  49. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  50. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  51. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  52. package/src/superlocalmemory/learning/reward.py +50 -0
  53. package/src/superlocalmemory/learning/source_quality.py +523 -1
  54. package/src/superlocalmemory/loops/ledger.py +25 -5
  55. package/src/superlocalmemory/mcp/server.py +11 -30
  56. package/src/superlocalmemory/mcp/tools_core.py +4 -3
  57. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  58. package/src/superlocalmemory/retrieval/engine.py +53 -21
  59. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  60. package/src/superlocalmemory/server/config_file.py +90 -0
  61. package/src/superlocalmemory/server/origin.py +50 -0
  62. package/src/superlocalmemory/server/routes/backup.py +293 -70
  63. package/src/superlocalmemory/server/routes/behavioral.py +336 -59
  64. package/src/superlocalmemory/server/routes/brain.py +57 -16
  65. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  66. package/src/superlocalmemory/server/routes/entity.py +100 -23
  67. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  68. package/src/superlocalmemory/server/routes/learning.py +276 -105
  69. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  70. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  71. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  72. package/src/superlocalmemory/server/routes/stats.py +93 -155
  73. package/src/superlocalmemory/server/routes/token.py +3 -13
  74. package/src/superlocalmemory/server/routes/v3_api.py +64 -20
  75. package/src/superlocalmemory/server/unified_daemon.py +467 -40
  76. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  77. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  78. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  79. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  80. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  81. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  82. package/src/superlocalmemory/storage/schema.py +49 -1
  83. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  84. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  85. package/src/superlocalmemory/ui/index.html +3 -6
  86. package/src/superlocalmemory/ui/js/core.js +52 -9
  87. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  88. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  89. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  90. package/src/superlocalmemory/ui/js/od-entities.js +22 -22
  91. package/src/superlocalmemory/ui/js/od-graph.js +11 -4
  92. package/src/superlocalmemory/ui/js/od-memories.js +47 -5
  93. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  94. package/src/superlocalmemory/ui/js/od-settings.js +113 -59
  95. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  96. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  97. package/src/superlocalmemory/ui/js/settings.js +15 -1
  98. package/plugin-src/.mcp.json +0 -12
  99. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  100. package/plugin-src/agents/slm-loop-runner.md +0 -71
  101. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  102. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  103. package/plugin-src/commands/slm-loop.md +0 -31
  104. package/plugin-src/hooks/.gitkeep +0 -0
  105. package/plugin-src/hooks/hooks.json +0 -102
  106. package/plugin-src/manifest.json +0 -30
  107. package/plugin-src/requirements.txt +0 -1
  108. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  109. package/plugin-src/scripts/ensure-venv.bat +0 -122
  110. package/plugin-src/scripts/ensure-venv.sh +0 -105
  111. package/plugin-src/scripts/slm-launch +0 -62
  112. package/plugin-src/scripts/slm-launch.bat +0 -23
  113. package/plugin-src/settings.json +0 -25
  114. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  115. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  116. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  117. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  118. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -38,6 +38,8 @@ _TABLES: Final[tuple[str, ...]] = (
38
38
  "memories",
39
39
  "atomic_facts",
40
40
  "canonical_entities",
41
+ "fact_entity_associations",
42
+ "fact_entity_association_repair_state",
41
43
  "entity_aliases",
42
44
  "entity_profiles",
43
45
  "memory_scenes",
@@ -328,7 +330,52 @@ CREATE INDEX IF NOT EXISTS idx_entities_profile
328
330
  CREATE INDEX IF NOT EXISTS idx_entities_name_lower
329
331
  ON canonical_entities (profile_id, canonical_name COLLATE NOCASE);
330
332
  CREATE INDEX IF NOT EXISTS idx_entities_type
331
- ON canonical_entities (profile_id, entity_type);"""
333
+ ON canonical_entities (profile_id, entity_type);
334
+ CREATE INDEX IF NOT EXISTS idx_entities_profile_fact_count
335
+ ON canonical_entities (profile_id, fact_count DESC);
336
+ CREATE INDEX IF NOT EXISTS idx_entities_profile_type_fact_count
337
+ ON canonical_entities (
338
+ profile_id, entity_type COLLATE NOCASE, fact_count DESC
339
+ );"""
340
+
341
+
342
+ # ---------------------------------------------------------------------------
343
+ # Normalized fact/entity associations (also the ingestion effect ledger)
344
+ # ---------------------------------------------------------------------------
345
+
346
+ _SQL_FACT_ENTITY_ASSOCIATIONS: Final[str] = """
347
+ CREATE TABLE IF NOT EXISTS fact_entity_associations (
348
+ profile_id TEXT NOT NULL,
349
+ fact_id TEXT NOT NULL,
350
+ entity_id TEXT NOT NULL,
351
+ first_operation_id TEXT NOT NULL DEFAULT '',
352
+ count_applied INTEGER NOT NULL DEFAULT 0
353
+ CHECK (count_applied IN (0, 1)),
354
+ created_at TEXT NOT NULL DEFAULT (
355
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
356
+ ),
357
+ PRIMARY KEY (profile_id, fact_id, entity_id),
358
+ FOREIGN KEY (fact_id) REFERENCES atomic_facts(fact_id) ON DELETE CASCADE,
359
+ FOREIGN KEY (entity_id)
360
+ REFERENCES canonical_entities(entity_id) ON DELETE CASCADE
361
+ );
362
+ CREATE INDEX IF NOT EXISTS idx_fact_entity_associations_entity
363
+ ON fact_entity_associations(profile_id, entity_id, fact_id);
364
+ CREATE TABLE IF NOT EXISTS fact_entity_association_repair_state (
365
+ repair_key TEXT PRIMARY KEY,
366
+ state TEXT NOT NULL DEFAULT 'pending'
367
+ CHECK (state IN ('pending', 'running', 'retrying', 'complete')),
368
+ target_fact_rowid INTEGER NOT NULL DEFAULT -1,
369
+ last_fact_rowid INTEGER NOT NULL DEFAULT 0,
370
+ scanned INTEGER NOT NULL DEFAULT 0,
371
+ inserted INTEGER NOT NULL DEFAULT 0,
372
+ last_error TEXT NOT NULL DEFAULT '',
373
+ updated_at TEXT NOT NULL
374
+ );
375
+ INSERT OR IGNORE INTO fact_entity_association_repair_state
376
+ (repair_key, state, target_fact_rowid, updated_at)
377
+ VALUES ('historical-backfill', 'pending', -1, '');
378
+ """
332
379
 
333
380
 
334
381
  # ---------------------------------------------------------------------------
@@ -752,6 +799,7 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
752
799
  _SQL_MEMORIES,
753
800
  _SQL_ATOMIC_FACTS,
754
801
  _SQL_CANONICAL_ENTITIES,
802
+ _SQL_FACT_ENTITY_ASSOCIATIONS,
755
803
  _SQL_ENTITY_ALIASES,
756
804
  _SQL_ENTITY_PROFILES,
757
805
  _SQL_MEMORY_SCENES,
@@ -359,6 +359,8 @@ V32_DDL: list[str] = [
359
359
  ON soft_prompt_templates(profile_id, active);
360
360
  CREATE INDEX IF NOT EXISTS idx_soft_prompt_category
361
361
  ON soft_prompt_templates(profile_id, category);
362
+ CREATE INDEX IF NOT EXISTS idx_soft_prompts_profile_active_category
363
+ ON soft_prompt_templates(profile_id, active, category, prompt_id);
362
364
  CREATE UNIQUE INDEX IF NOT EXISTS idx_soft_prompt_unique_cat
363
365
  ON soft_prompt_templates(profile_id, category)
364
366
  WHERE active = 1;
@@ -48,6 +48,8 @@ CREATE INDEX IF NOT EXISTS idx_tool_events_session
48
48
  ON tool_events(session_id);
49
49
  CREATE INDEX IF NOT EXISTS idx_tool_events_project
50
50
  ON tool_events(project_path);
51
+ CREATE INDEX IF NOT EXISTS idx_tool_events_profile_created
52
+ ON tool_events(profile_id, created_at DESC);
51
53
  """
52
54
 
53
55
  # ---------------------------------------------------------------------------
@@ -80,6 +82,8 @@ CREATE INDEX IF NOT EXISTS idx_assertions_project
80
82
  ON behavioral_assertions(project_path, profile_id);
81
83
  CREATE INDEX IF NOT EXISTS idx_assertions_category
82
84
  ON behavioral_assertions(category);
85
+ CREATE INDEX IF NOT EXISTS idx_assertions_profile_confidence
86
+ ON behavioral_assertions(profile_id, confidence DESC);
83
87
  """
84
88
 
85
89
  # ---------------------------------------------------------------------------
@@ -182,7 +182,6 @@
182
182
  <div class="value num" id="k-clu">—</div>
183
183
  <div class="delta" id="k-clu-delta">&nbsp;</div>
184
184
  <div class="spark" id="sp-clu"></div>
185
- <!-- TODO: endpoint /api/v3/clusters/count does not exist yet -->
186
185
  </div>
187
186
  </section>
188
187
 
@@ -192,8 +191,8 @@
192
191
  <!-- memory growth + quick actions -->
193
192
  <div class="card">
194
193
  <div class="card-head">
195
- <h3>Memory growth</h3>
196
- <span class="sub">facts stored over time</span>
194
+ <h3>Memory activity</h3>
195
+ <span class="sub">facts added by day · past year</span>
197
196
  <div class="spacer"></div>
198
197
  </div>
199
198
  <div class="card-pad">
@@ -300,9 +299,7 @@
300
299
  <div class="card">
301
300
  <div class="card-head"><h3>Where memories come from</h3></div>
302
301
  <div class="card-pad">
303
- <!-- populated by loadDashboardSources() in dashboard.js -->
304
- <!-- TODO: endpoint /api/v3/ingestion/sources does not exist yet;
305
- showing static breakdown from /api/stats for now -->
302
+ <!-- populated by loadDashboardSources() from profile-scoped provenance -->
306
303
  <div id="src">
307
304
  <div class="muted" style="padding:16px;text-align:center;font-size:13px">Loading...</div>
308
305
  </div>
@@ -35,11 +35,11 @@ window.slmInstallToken = (function () {
35
35
  };
36
36
  })();
37
37
 
38
- // Global fetch patch: apply the abort timeout to every relative-URL request
38
+ // Global fetch patch: apply the abort timeout to every same-origin request
39
39
  // automatically. 17 UI modules call bare fetch() — patching here avoids
40
40
  // touching each one and guarantees no future callsite can regress to an
41
- // un-timed fetch that holds the spinner forever. Absolute URLs (external
42
- // resources) are passed through unchanged. Callers that already supply
41
+ // un-timed fetch that holds the spinner forever. External URLs are passed
42
+ // through unchanged. Callers that already supply
43
43
  // `signal` keep their own behavior. `init.timeoutMs` lets callers override
44
44
  // the default per-request.
45
45
  (function patchFetch() {
@@ -49,25 +49,68 @@ window.slmInstallToken = (function () {
49
49
  window.__slmOriginalFetch = _origFetch;
50
50
  window.fetch = function (input, init) {
51
51
  init = Object.assign({}, init || {});
52
- var urlStr = typeof input === 'string' ? input : (input && input.url) || '';
53
- var isRelative = !(/^https?:\/\//i.test(urlStr));
52
+ var urlStr = typeof input === 'string'
53
+ ? input
54
+ : (input && (input.url || input.href)) || '';
55
+ // Resolve every URL before deciding whether it is local. In particular,
56
+ // protocol-relative URLs such as //host/path inherit the current scheme
57
+ // but are not same-origin and must never receive the install token.
58
+ var isSameOrigin = false;
59
+ var requestUrl = null;
60
+ try {
61
+ requestUrl = new URL(urlStr, window.location.href);
62
+ isSameOrigin = requestUrl.origin === window.location.origin;
63
+ } catch (e) {
64
+ // A malformed URL is handled by native fetch. Treat it as external
65
+ // here so it cannot obtain local credentials while failing.
66
+ }
54
67
  var method = String(
55
68
  init.method || (input && input.method) || 'GET'
56
69
  ).toUpperCase();
57
70
  var mutating = ['POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method) !== -1;
71
+ var invalidatesCache = init.slmInvalidatesCache !== false;
72
+ var requiresWriteAuth = init.slmRequiresWriteAuth !== false;
73
+ delete init.slmInvalidatesCache;
74
+ delete init.slmRequiresWriteAuth;
75
+
76
+ function invalidateAfterMutation(request) {
77
+ if (!isSameOrigin || !mutating || !invalidatesCache) return request;
78
+ return request.then(function (response) {
79
+ if (
80
+ response && response.ok &&
81
+ typeof window.slmInvalidatePanes === 'function'
82
+ ) {
83
+ window.slmInvalidatePanes();
84
+ }
85
+ if (
86
+ response && response.ok &&
87
+ typeof window.slmInvalidateDashboardCache === 'function'
88
+ ) {
89
+ window.slmInvalidateDashboardCache();
90
+ }
91
+ return response;
92
+ });
93
+ }
58
94
 
59
95
  function send() {
60
- if (!isRelative || init.signal) {
61
- return _origFetch(input, init);
96
+ if (!isSameOrigin || init.signal) {
97
+ return invalidateAfterMutation(_origFetch(input, init));
62
98
  }
63
99
  var controller = new AbortController();
64
100
  var timeoutMs = init.timeoutMs || window.SLM_FETCH_TIMEOUT_MS;
65
101
  var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
66
102
  init.signal = controller.signal;
67
- return _origFetch(input, init).finally(function () { clearTimeout(timer); });
103
+ return invalidateAfterMutation(
104
+ _origFetch(input, init).finally(function () { clearTimeout(timer); })
105
+ );
68
106
  }
69
107
 
70
- if (isRelative && mutating && urlStr !== '/internal/token') {
108
+ if (
109
+ isSameOrigin &&
110
+ mutating &&
111
+ requiresWriteAuth &&
112
+ (!requestUrl || requestUrl.pathname !== '/internal/token')
113
+ ) {
71
114
  return window.slmInstallToken(false).then(function (token) {
72
115
  if (!token) throw new Error('local write credential unavailable');
73
116
  var headers = new Headers(
@@ -5,29 +5,64 @@
5
5
  // loadDashboard() → GET /api/v3/dashboard → system card (#dashboard-mode etc.)
6
6
  // loadDashboardStats() → GET /api/stats → OD KPI strip (#k-mem, #k-nodes, #k-edges)
7
7
  // loadDashboardFeed() → GET /api/memories?limit=6 → activity feed (#feed)
8
- // loadDashboardSources() → static from stats breakdown → capture-sources (#src)
9
- // loadDashboardSparklines() → builds sparklines from stats values via slmSpark
8
+ // loadDashboardSources() → real provenance from /api/stats → capture-sources (#src)
9
+ // loadDashboardTimeline() → real daily fact counts from /api/timeline #sp-big
10
+
11
+ var DASHBOARD_CACHE_MS = 30000;
12
+ var dashboardLastRefresh = 0;
13
+ var dashboardRefreshInFlight = null;
14
+ var dashboardCacheGeneration = 0;
15
+ var dashboardRefreshQueued = false;
16
+
17
+ window.slmInvalidateDashboardCache = function() {
18
+ dashboardCacheGeneration += 1;
19
+ dashboardLastRefresh = 0;
20
+ if (dashboardRefreshInFlight) dashboardRefreshQueued = true;
21
+ };
10
22
 
11
23
  // Auto-refresh dashboard when tab becomes visible (fixes stale data after settings change)
12
24
  document.addEventListener('visibilitychange', function() {
13
25
  if (!document.hidden) refreshDashboard();
14
26
  });
15
27
 
16
- // Refresh system card on focus (alt-tab back to browser).
17
- // Only calls loadDashboard() not the feed/sparklines, which would cause
18
- // DOM teardown and rebuild on every focus event (excessive churn).
19
- window.addEventListener('focus', function() { loadDashboard(); });
28
+ // Focus and tab navigation use a short cache window. This keeps normal
29
+ // browser switching from repeatedly querying a large local database.
30
+ window.addEventListener('focus', function() { refreshDashboard(); });
20
31
 
21
32
  // Load when the Dashboard tab is shown via the sidebar. od-shell.js's activateTab()
22
33
  // dispatches shown.bs.tab on the hidden #dashboard-tab button.
23
34
  document.getElementById('dashboard-tab')?.addEventListener('shown.bs.tab', function() { refreshDashboard(); });
24
35
 
25
- // refreshDashboard: fires all OD + legacy loaders in parallel (called by event-delegation.js 'refresh-dashboard')
26
- window.refreshDashboard = function() {
27
- loadDashboard();
28
- loadDashboardStats();
29
- loadDashboardFeed();
30
- loadDashboardSources();
36
+ // refreshDashboard coalesces concurrent startup/navigation events and keeps a
37
+ // 30-second snapshot. Mutations pass {force:true} to re-read daemon truth.
38
+ window.refreshDashboard = function(options) {
39
+ var force = options && options.force === true;
40
+ var now = Date.now();
41
+ if (dashboardRefreshInFlight) {
42
+ if (force) dashboardRefreshQueued = true;
43
+ return dashboardRefreshInFlight;
44
+ }
45
+ if (!force && dashboardLastRefresh && now - dashboardLastRefresh < DASHBOARD_CACHE_MS) {
46
+ return Promise.resolve();
47
+ }
48
+ var refreshGeneration = dashboardCacheGeneration;
49
+ dashboardRefreshInFlight = Promise.all([
50
+ loadDashboard(),
51
+ loadDashboardStats(),
52
+ loadDashboardTimeline(),
53
+ loadDashboardFeed()
54
+ ]).then(function(results) {
55
+ dashboardLastRefresh = refreshGeneration === dashboardCacheGeneration && results.every(Boolean)
56
+ ? Date.now()
57
+ : 0;
58
+ }).finally(function() {
59
+ dashboardRefreshInFlight = null;
60
+ }).then(function() {
61
+ if (!dashboardRefreshQueued) return;
62
+ dashboardRefreshQueued = false;
63
+ return window.refreshDashboard({ force: true });
64
+ });
65
+ return dashboardRefreshInFlight;
31
66
  };
32
67
 
33
68
  // ----------------------------------------------------------------
@@ -36,7 +71,7 @@ window.refreshDashboard = function() {
36
71
  async function loadDashboardStats() {
37
72
  try {
38
73
  var r = await fetch('/api/stats', { credentials: 'same-origin' });
39
- if (!r.ok) return;
74
+ if (!r.ok) return false;
40
75
  var data = await r.json();
41
76
  var ov = data.overview || {};
42
77
 
@@ -44,12 +79,14 @@ async function loadDashboardStats() {
44
79
  var facts = ov.total_facts || 0;
45
80
  var nodes = ov.graph_nodes || 0;
46
81
  var edges = ov.graph_edges || 0;
82
+ var clusters = ov.total_clusters || 0;
47
83
 
48
84
  // OD KPI strip
49
85
  setKpiValue('k-mem', memories);
50
86
  setKpiValue('k-nodes', nodes);
51
87
  setKpiValue('k-edges', edges);
52
88
  setKpiValue('k-facts', facts);
89
+ setKpiValue('k-clu', clusters);
53
90
 
54
91
  // Legacy hidden compat IDs (core.js loadStats() may race — that's OK,
55
92
  // the hidden IDs are only a data bridge, not visible).
@@ -57,51 +94,62 @@ async function loadDashboardStats() {
57
94
  setTextById('stat-facts', facts);
58
95
  setTextById('stat-nodes', nodes);
59
96
  setTextById('stat-edges', edges);
60
-
61
- // Sparklines — build synthetic ascending series from the real count
62
- // (no time-series endpoint exists yet; honest placeholder).
63
- // TODO: replace with GET /api/v3/stats/history when that endpoint ships.
64
- if (typeof window.slmSpark === 'function') {
65
- loadDashboardSparklines(memories, nodes, edges);
66
- }
97
+ loadDashboardSources(data);
98
+ return true;
67
99
  } catch (e) {
68
100
  // Non-fatal: KPI strip shows — values
101
+ return false;
69
102
  }
70
103
  }
71
104
 
72
105
  // ----------------------------------------------------------------
73
- // loadDashboardSparklinesbuilds inline SVG sparklines from totals
74
- // No time-series endpoint yet: we synthesise a plausible trailing
75
- // series anchored to the real current total. Honest placeholder.
106
+ // loadDashboardTimelinerenders real daily fact counts from /api/timeline
76
107
  // ----------------------------------------------------------------
77
- function loadDashboardSparklines(memories, nodes, edges) {
78
- function syntheticSeries(current, n, volatility) {
79
- var a = [];
80
- var v = current * 0.60;
81
- var step = (current - v) / n;
82
- for (var i = 0; i < n; i++) {
83
- v += step + (Math.random() - 0.45) * volatility * current;
84
- a.push(Math.max(0, Math.round(v)));
108
+ async function loadDashboardTimeline() {
109
+ var spBig = document.getElementById('sp-big');
110
+ if (!spBig) return true;
111
+ try {
112
+ var response = await fetch('/api/timeline?days=365&group_by=day&include_categories=false', {
113
+ credentials: 'same-origin'
114
+ });
115
+ if (!response.ok) throw new Error('timeline unavailable');
116
+ var stats = await response.json();
117
+ var points = Array.isArray(stats.timeline) ? stats.timeline : [];
118
+ var byDate = {};
119
+ points.forEach(function(point) {
120
+ if (point && point.period) byDate[String(point.period)] = Number(point.count) || 0;
121
+ });
122
+ var counts = [];
123
+ var today = new Date();
124
+ today.setHours(0, 0, 0, 0);
125
+ for (var dayOffset = 364; dayOffset >= 0; dayOffset--) {
126
+ var day = new Date(today);
127
+ day.setDate(today.getDate() - dayOffset);
128
+ counts.push(byDate[day.toISOString().slice(0, 10)] || 0);
85
129
  }
86
- a[n - 1] = current;
87
- return a;
88
- }
89
- var memSeries = syntheticSeries(memories || 1, 24, 0.015);
90
- var nodeSeries = syntheticSeries(nodes || 1, 24, 0.018);
91
- var edgeSeries = syntheticSeries(edges || 1, 24, 0.020);
92
- var bigSeries = syntheticSeries(memories || 1, 90, 0.012);
93
-
94
- var spMem = document.getElementById('sp-mem');
95
- var spNodes = document.getElementById('sp-nodes');
96
- var spEdges = document.getElementById('sp-edges');
97
- var spBig = document.getElementById('sp-big');
98
-
99
- if (spMem) spMem.innerHTML = window.slmSpark(memSeries, { color: 'var(--violet)' });
100
- if (spNodes) spNodes.innerHTML = window.slmSpark(nodeSeries, { color: 'var(--cyan)' });
101
- if (spEdges) spEdges.innerHTML = window.slmSpark(edgeSeries, { color: 'var(--violet)' });
102
- if (spBig) spBig.innerHTML = window.slmSpark(bigSeries, { w: 600, h: 140, color: 'var(--violet)' });
103
- if (spBig && spBig.querySelector('svg')) {
104
- spBig.querySelector('svg').style.cssText = 'width:100%;height:140px;display:block;';
130
+ var total = counts.reduce(function(sum, value) { return sum + value; }, 0);
131
+ var avgEl = document.getElementById('k-avg-day');
132
+ if (avgEl) avgEl.textContent = (total / 365).toFixed(1);
133
+
134
+ if (!total || typeof window.slmSpark !== 'function') {
135
+ spBig.textContent = 'No dated memory history recorded for this profile.';
136
+ spBig.className = 'muted';
137
+ return true;
138
+ }
139
+ spBig.className = '';
140
+ spBig.innerHTML = window.slmSpark(counts, {
141
+ w: 600,
142
+ h: 140,
143
+ color: 'var(--violet)'
144
+ });
145
+ if (spBig.querySelector('svg')) {
146
+ spBig.querySelector('svg').style.cssText = 'width:100%;height:140px;display:block;';
147
+ }
148
+ return true;
149
+ } catch (e) {
150
+ spBig.textContent = 'Memory history unavailable.';
151
+ spBig.className = 'muted';
152
+ return false;
105
153
  }
106
154
  }
107
155
 
@@ -110,7 +158,7 @@ function loadDashboardSparklines(memories, nodes, edges) {
110
158
  // ----------------------------------------------------------------
111
159
  async function loadDashboardFeed() {
112
160
  var feedEl = document.getElementById('feed');
113
- if (!feedEl) return;
161
+ if (!feedEl) return true;
114
162
 
115
163
  var CATEGORY_COLORS = {
116
164
  'episodic': ['rgba(34,197,94,0.12)', '#22c55e'],
@@ -131,13 +179,13 @@ async function loadDashboardFeed() {
131
179
  var r = await fetch('/api/memories?limit=6', { credentials: 'same-origin' });
132
180
  if (!r.ok) {
133
181
  feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No recent activity</div>';
134
- return;
182
+ return false;
135
183
  }
136
184
  var data = await r.json();
137
185
  var items = data.memories || data.results || data || [];
138
186
  if (!Array.isArray(items) || items.length === 0) {
139
187
  feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No memories yet — start capturing!</div>';
140
- return;
188
+ return true;
141
189
  }
142
190
 
143
191
  // Build feed using DOM methods (XSS-safe — no innerHTML with user content)
@@ -180,37 +228,76 @@ async function loadDashboardFeed() {
180
228
  item.appendChild(time);
181
229
  feedEl.appendChild(item);
182
230
  });
231
+ return true;
183
232
  } catch (e) {
184
233
  feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">Activity unavailable</div>';
234
+ return false;
185
235
  }
186
236
  }
187
237
 
188
238
  // ----------------------------------------------------------------
189
- // loadDashboardSources — static breakdown from available stats
190
- // TODO: GET /api/v3/ingestion/sources does not exist yet.
239
+ // loadDashboardSources — profile-scoped provenance returned by /api/stats
191
240
  // ----------------------------------------------------------------
192
- function loadDashboardSources() {
241
+ function loadDashboardSources(stats) {
193
242
  var srcEl = document.getElementById('src');
194
243
  if (!srcEl) return;
195
- // Honest static breakdown until endpoint ships.
196
- // These are representative percentages, not synthetic fake data —
197
- // they describe the general SLM capture pattern.
198
- var SRC = [
199
- ['MCP agents (Claude, Cursor)', 62, 'var(--violet)'],
200
- ['Auto-capture · decisions', 21, 'var(--cyan)'],
201
- ['Auto-capture · bugs', 9, 'var(--warn,#f59e0b)'],
202
- ['Manual / CLI', 8, 'var(--fg-3)'],
203
- ];
204
- srcEl.innerHTML = SRC.map(function(s) {
205
- return '<div style="margin-bottom:14px">' +
206
- '<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px">' +
207
- '<span>' + s[0] + '</span><b class="num">' + s[1] + '%</b>' +
208
- '</div>' +
209
- '<div class="meter"><i style="width:' + s[1] + '%;background:' + s[2] + '"></i></div>' +
210
- '</div>';
211
- }).join('') + '<p style="font-size:11px;color:var(--fg-3);margin-top:8px">' +
212
- '<!-- TODO: wire to /api/v3/ingestion/sources when available -->' +
213
- '</p>';
244
+ var sourceStatus = stats && stats.ingestion_sources_status;
245
+ if (sourceStatus && sourceStatus.available === false) {
246
+ srcEl.textContent = 'Provenance metrics are temporarily unavailable.';
247
+ srcEl.className = 'muted';
248
+ return;
249
+ }
250
+ var sources = stats && Array.isArray(stats.ingestion_sources)
251
+ ? stats.ingestion_sources
252
+ : [];
253
+ if (!sources.length) {
254
+ srcEl.textContent = 'No provenance recorded for this profile.';
255
+ srcEl.className = 'muted';
256
+ return;
257
+ }
258
+
259
+ var LABELS = {
260
+ 'store': 'Historical / engine store',
261
+ 'http': 'Dashboard, hooks, and API',
262
+ 'http-observe': 'Auto-capture observations',
263
+ 'mcp-offline-worker': 'Queued MCP ingestion',
264
+ 'cli-sync': 'CLI sync',
265
+ 'unknown': 'Unclassified'
266
+ };
267
+ var COLORS = ['var(--violet)', 'var(--cyan)', 'var(--warn,#f59e0b)', 'var(--fg-3)'];
268
+ var total = sources.reduce(function(sum, source) {
269
+ return sum + (Number(source.count) || 0);
270
+ }, 0);
271
+
272
+ srcEl.className = '';
273
+ srcEl.textContent = '';
274
+ sources.forEach(function(source, index) {
275
+ var count = Number(source.count) || 0;
276
+ var percent = total ? Math.round((count / total) * 100) : 0;
277
+ var row = document.createElement('div');
278
+ row.style.marginBottom = '14px';
279
+
280
+ var heading = document.createElement('div');
281
+ heading.style.cssText = 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px';
282
+ var label = document.createElement('span');
283
+ label.textContent = LABELS[source.source_type] || source.source_type || 'Unclassified';
284
+ var value = document.createElement('b');
285
+ value.className = 'num';
286
+ value.textContent = count.toLocaleString() + ' · ' + percent + '%';
287
+ heading.appendChild(label);
288
+ heading.appendChild(value);
289
+
290
+ var meter = document.createElement('div');
291
+ meter.className = 'meter';
292
+ var fill = document.createElement('i');
293
+ fill.style.width = percent + '%';
294
+ fill.style.background = COLORS[index % COLORS.length];
295
+ meter.appendChild(fill);
296
+
297
+ row.appendChild(heading);
298
+ row.appendChild(meter);
299
+ srcEl.appendChild(row);
300
+ });
214
301
  }
215
302
 
216
303
  // ----------------------------------------------------------------
@@ -247,7 +334,7 @@ async function loadDashboard() {
247
334
  var response = await fetch('/api/v3/dashboard', { credentials: 'same-origin' });
248
335
  if (!response.ok) {
249
336
  showPaneError(PANE_ID, paneErrorMessage(response.status), loadDashboard, true);
250
- return;
337
+ return false;
251
338
  }
252
339
  var data = await response.json();
253
340
 
@@ -282,9 +369,11 @@ async function loadDashboard() {
282
369
  document.querySelectorAll('.mode-btn').forEach(function(btn) {
283
370
  btn.classList.toggle('active', btn.dataset.mode === data.mode);
284
371
  });
372
+ return true;
285
373
  } catch (e) {
286
374
  showPaneError(PANE_ID, paneErrorMessage(0), loadDashboard, true);
287
375
  console.log('Dashboard load error:', e);
376
+ return false;
288
377
  }
289
378
  }
290
379
 
@@ -293,10 +382,7 @@ document.addEventListener('DOMContentLoaded', function() {
293
382
  // od-shell.js calls activateTab('dashboard-pane') which dispatches shown.bs.tab,
294
383
  // but the listener may not yet be attached at that point. Call directly as well.
295
384
  setTimeout(function() {
296
- loadDashboard();
297
- loadDashboardStats();
298
- loadDashboardFeed();
299
- loadDashboardSources();
385
+ refreshDashboard({ force: true });
300
386
  }, 200);
301
387
  });
302
388
 
@@ -351,8 +437,7 @@ document.getElementById('quick-store-btn')?.addEventListener('click', async func
351
437
  }
352
438
  await r.json();
353
439
  if (input) input.value = '';
354
- loadDashboard();
355
- loadDashboardFeed();
440
+ refreshDashboard({ force: true });
356
441
  showToast('Stored!');
357
442
  } catch(e) {
358
443
  showToast('Store failed: ' + e.message);
@@ -372,6 +457,8 @@ document.getElementById('quick-recall-btn')?.addEventListener('click', function(
372
457
  fetch('/api/search', {
373
458
  method: 'POST', credentials: 'same-origin',
374
459
  headers: {'Content-Type': 'application/json'},
460
+ slmInvalidatesCache: false,
461
+ slmRequiresWriteAuth: false,
375
462
  body: JSON.stringify({query: query, limit: 5})
376
463
  }).then(function(r) {
377
464
  btn.disabled = false;