superlocalmemory 3.7.7 → 3.8.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 (262) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +85 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -2
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +96 -12
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/auth_middleware.py +28 -0
  116. package/src/superlocalmemory/infra/backup.py +12 -1
  117. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  118. package/src/superlocalmemory/infra/data_root.py +43 -4
  119. package/src/superlocalmemory/infra/event_bus.py +107 -24
  120. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  121. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  122. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  123. package/src/superlocalmemory/learning/cross_project.py +28 -19
  124. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  125. package/src/superlocalmemory/loops/__init__.py +56 -0
  126. package/src/superlocalmemory/loops/budget.py +58 -0
  127. package/src/superlocalmemory/loops/engine.py +164 -0
  128. package/src/superlocalmemory/loops/ledger.py +243 -0
  129. package/src/superlocalmemory/loops/models.py +152 -0
  130. package/src/superlocalmemory/loops/rules.py +52 -0
  131. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  132. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  133. package/src/superlocalmemory/mcp/profiles.py +103 -0
  134. package/src/superlocalmemory/mcp/server.py +21 -49
  135. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  136. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  137. package/src/superlocalmemory/mcp/tools_core.py +50 -5
  138. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  139. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  140. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  141. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  142. package/src/superlocalmemory/mesh/broker.py +237 -129
  143. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  144. package/src/superlocalmemory/optimize/NOTICE +1 -6
  145. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  147. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  148. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  149. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  150. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  151. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  152. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  153. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  154. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  155. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  156. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  157. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  158. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  159. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  160. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  161. package/src/superlocalmemory/retrieval/engine.py +168 -26
  162. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  163. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  164. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  165. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  166. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  167. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  168. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  169. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  170. package/src/superlocalmemory/server/api.py +21 -4
  171. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  172. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  173. package/src/superlocalmemory/server/recall_health.py +24 -3
  174. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  175. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  176. package/src/superlocalmemory/server/routes/agents.py +128 -38
  177. package/src/superlocalmemory/server/routes/backup.py +34 -10
  178. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  179. package/src/superlocalmemory/server/routes/brain.py +21 -5
  180. package/src/superlocalmemory/server/routes/chat.py +72 -16
  181. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  182. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  183. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  184. package/src/superlocalmemory/server/routes/entity.py +9 -4
  185. package/src/superlocalmemory/server/routes/events.py +24 -8
  186. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  187. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  188. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  189. package/src/superlocalmemory/server/routes/insights.py +3 -3
  190. package/src/superlocalmemory/server/routes/learning.py +14 -14
  191. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  192. package/src/superlocalmemory/server/routes/memories.py +221 -49
  193. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  194. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  195. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  196. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  197. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  198. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  199. package/src/superlocalmemory/server/routes/stats.py +13 -6
  200. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  201. package/src/superlocalmemory/server/routes/v3_api.py +194 -81
  202. package/src/superlocalmemory/server/routes/ws.py +5 -2
  203. package/src/superlocalmemory/server/security_middleware.py +12 -5
  204. package/src/superlocalmemory/server/ui.py +30 -5
  205. package/src/superlocalmemory/server/unified_daemon.py +431 -75
  206. package/src/superlocalmemory/server/write_identity.py +38 -8
  207. package/src/superlocalmemory/storage/database.py +265 -53
  208. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/models.py +4 -0
  217. package/src/superlocalmemory/storage/schema.py +87 -0
  218. package/src/superlocalmemory/storage/schema_v32.py +0 -9
  219. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  220. package/src/superlocalmemory/trust/gate.py +49 -8
  221. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  222. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  223. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  224. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  225. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  226. package/src/superlocalmemory/ui/favicon.svg +35 -4
  227. package/src/superlocalmemory/ui/index.html +306 -173
  228. package/src/superlocalmemory/ui/js/brain.js +5 -20
  229. package/src/superlocalmemory/ui/js/core.js +47 -31
  230. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  231. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  232. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  233. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  234. package/src/superlocalmemory/ui/js/memories.js +15 -4
  235. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  236. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  237. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  238. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  239. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  240. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  241. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  242. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  243. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  244. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  245. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  246. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  247. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  248. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  249. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  250. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  251. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  252. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  253. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  254. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  255. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  256. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  257. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  258. package/src/superlocalmemory/ui/js/settings.js +2 -2
  259. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  260. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  261. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  262. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -0,0 +1,887 @@
1
+ // od-memories.js — Memories · Timeline · Knowledge Clusters (OD design port)
2
+ // Render: #memories-pane | Public: window.odRenderMemories(container)
3
+ // CSP-safe: data-od-act event delegation. XSS-safe: _esc() on all API values.
4
+ // nosemgrep: innerHTML — all dynamic values escaped via _esc()
5
+ //
6
+ // Endpoints (live daemon 127.0.0.1:8765):
7
+ // GET /api/memories?limit=N&offset=N[&category=X]
8
+ // → { memories[{id,memory_id,content,category,importance(0-1),
9
+ // access_count,created_at,project_name}], total }
10
+ // GET /api/memories/{id}/facts → { facts[{fact_type,content}] }
11
+ // GET /api/v3/timeline/?range=Xd&group_by=category&limit=N
12
+ // → { events[{id,timestamp,category}] }
13
+ // GET /api/clusters → { clusters[{cluster_id,member_count,categories,summary}] }
14
+ // GET /api/clusters/{id}?limit=N → { members[], summary }
15
+ // TODO: /api/memories/{id}/score-breakdown — Semantic/BM25/Entity/Temporal not yet exposed.
16
+ (function () {
17
+ 'use strict';
18
+
19
+ // ── Utilities ───────────────────────────────────────────────────────────────
20
+
21
+ function _esc(s) {
22
+ if (typeof escapeHtml === 'function') return escapeHtml(s);
23
+ var d = document.createElement('div');
24
+ d.textContent = String(s == null ? '' : s);
25
+ return d.innerHTML;
26
+ }
27
+
28
+ function _fmt(iso) {
29
+ if (!iso) return '—';
30
+ var d = new Date(iso);
31
+ return isNaN(d.getTime()) ? String(iso) : d.toLocaleDateString();
32
+ }
33
+
34
+ function _ago(iso) {
35
+ if (!iso) return '';
36
+ var s = (Date.now() - new Date(iso).getTime()) / 1000;
37
+ if (s < 0) s = 0;
38
+ if (s < 60) return Math.floor(s) + 's ago';
39
+ if (s < 3600) return Math.floor(s / 60) + 'm ago';
40
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago';
41
+ return Math.floor(s / 86400) + 'd ago';
42
+ }
43
+
44
+ // importance arrives as 0–1 float; display as X/10 integer
45
+ function _imp(raw) { return Math.round((parseFloat(raw) || 0) * 10); }
46
+
47
+ // importance (0–1) → badge class for Score column
48
+ function _scoreCls(raw) {
49
+ var v = parseFloat(raw) || 0;
50
+ return v >= 0.7 ? 'ok' : v >= 0.4 ? 'warn' : 'danger';
51
+ }
52
+
53
+ // API category → badge class
54
+ function _catCls(cat) {
55
+ var MAP = { semantic: 'violet', episodic: 'cyan', opinion: 'warn',
56
+ temporal: 'ok', consolidation: 'danger' };
57
+ return MAP[String(cat).toLowerCase()] || 'neutral';
58
+ }
59
+
60
+ // Normalize /api/search result → memory shape (fact_id→id, score→importance)
61
+ function _normSearch(r) { return {id:r.fact_id||r.memory_id||'',content:r.content||'',category:r.category||'semantic',project_name:r.project_name||'',importance:r.score||r.confidence||0,created_at:r.created_at||'',access_count:r.access_count||0}; }
62
+ // shared_with is stored as a JSON array string; show it comma-separated.
63
+ function _sharedToStr(v) {
64
+ if (!v) return '';
65
+ if (Array.isArray(v)) return v.join(', ');
66
+ try { var a = JSON.parse(v); return Array.isArray(a) ? a.join(', ') : ''; }
67
+ catch (e) { return ''; }
68
+ }
69
+
70
+ // Write-auth token: reuses dashboard.js closure when available, else fails gracefully
71
+ function _getMutToken() { return typeof window.dashboardInstallToken==='function'?window.dashboardInstallToken():Promise.resolve(''); }
72
+
73
+ // ── CSS (injected once) ─────────────────────────────────────────────────────
74
+
75
+ var _cssInjected = false;
76
+ function _injectCSS() {
77
+ if (_cssInjected) return;
78
+ _cssInjected = true;
79
+ var s = document.createElement('style');
80
+ s.dataset.odModule = 'memories';
81
+ s.textContent =
82
+ '.od-drawer{position:fixed;top:0;right:0;height:100vh;width:420px;max-width:92vw;' +
83
+ 'background:var(--card);border-left:1px solid var(--border);box-shadow:var(--sh-lg);' +
84
+ 'transform:translateX(100%);transition:transform .28s cubic-bezier(.22,1,.36,1);' +
85
+ 'z-index:60;overflow-y:auto;padding:24px;}' +
86
+ '.od-drawer.open{transform:none;}' +
87
+ '.od-drawer-scrim{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:59;' +
88
+ 'opacity:0;pointer-events:none;transition:opacity .25s;}' +
89
+ '.od-drawer-scrim.on{opacity:1;pointer-events:auto;}' +
90
+ '.od-prov{padding:14px;border-radius:var(--r-md);background:var(--card-2);' +
91
+ 'border:1px solid var(--border);margin-top:12px;}';
92
+ document.head.appendChild(s);
93
+ }
94
+
95
+ // ── Module state ────────────────────────────────────────────────────────────
96
+
97
+ var _st = {
98
+ rootId: null,
99
+ activeTab: 'all',
100
+ category: null, // null = all; string = filtered category
101
+ sort: 'created',
102
+ page: 0,
103
+ pageSize: 50,
104
+ total: 0,
105
+ memories: [],
106
+ catCounts: {}, // pre-fetched per-category totals
107
+ tlRange: '30d',
108
+ cluLoaded: false, // reset in render() to avoid stale state on re-entry
109
+ };
110
+
111
+ // Known categories from the daemon (pre-fetched at render time)
112
+ var KNOWN_CATS = ['semantic', 'episodic', 'opinion', 'temporal', 'consolidation'];
113
+
114
+ // ── Main entry ──────────────────────────────────────────────────────────────
115
+
116
+ function render(container) {
117
+ if (!container) return;
118
+ _injectCSS();
119
+ var id = 'od-mem-' + Math.random().toString(36).slice(2, 8);
120
+ _st = Object.assign({}, _st, {
121
+ rootId: id, page: 0, category: null, sort: 'created',
122
+ memories: [], catCounts: {}, cluLoaded: false,
123
+ });
124
+ // Clear per-id timeline cache for this render instance
125
+ _tlLoaded = {};
126
+ container.innerHTML = _scaffold(id);
127
+ _wire(container, id);
128
+ // Fire initial loads in parallel
129
+ _loadMem(id);
130
+ _loadCatCounts(id);
131
+ }
132
+
133
+ // ── Scaffold HTML ────────────────────────────────────────────────────────────
134
+
135
+ function _scaffold(id) {
136
+ return (
137
+ '<div id="' + id + '">' +
138
+ '<div class="page-head">' +
139
+ '<h2>Everything you\'ve remembered</h2>' +
140
+ '<p id="' + id + '-sub" style="color:var(--fg-2);margin-top:5px">Loading…</p>' +
141
+ '</div>' +
142
+ '<div class="tabs" id="' + id + '-tabs">' +
143
+ '<button class="tab active" data-od-act="tab" data-tab="all">' +
144
+ 'All memories <span class="cnt" id="' + id + '-cnt-all">…</span></button>' +
145
+ '<button class="tab" data-od-act="tab" data-tab="timeline">Creation timeline</button>' +
146
+ '<button class="tab" data-od-act="tab" data-tab="clusters">' +
147
+ 'Knowledge clusters <span class="cnt" id="' + id + '-cnt-clusters">…</span></button>' +
148
+ '</div>' +
149
+ '<div class="tabpane active" id="' + id + '-pane-all">' + _allScaffold(id) + '</div>' +
150
+ '<div class="tabpane" id="' + id + '-pane-timeline">' + _tlScaffold(id) + '</div>' +
151
+ '<div class="tabpane" id="' + id + '-pane-clusters">' +
152
+ '<div class="launch-grid" id="' + id + '-clu-grid">' +
153
+ _loading('Loading clusters…') +
154
+ '</div>' +
155
+ '</div>' +
156
+ '</div>'
157
+ );
158
+ }
159
+
160
+ function _allScaffold(id) {
161
+ return (
162
+ '<div style="margin-bottom:16px;display:flex;gap:8px;align-items:center">' +
163
+ '<input data-od-act="search" placeholder="Search all memories…" autocomplete="off" ' +
164
+ 'style="flex:1;padding:8px 12px;border:1px solid var(--border);' +
165
+ 'border-radius:var(--r-md);background:var(--card-2);color:var(--fg);' +
166
+ 'font-size:13.5px;outline:none">' +
167
+ '<select data-od-act="window" title="Limit search to a time window" ' +
168
+ 'style="padding:8px 10px;border:1px solid var(--border);border-radius:var(--r-md);' +
169
+ 'background:var(--card-2);color:var(--fg);font-size:13px;outline:none">' +
170
+ '<option value="">Any time</option>' +
171
+ '<option value="24h">Last 24h</option>' +
172
+ '<option value="7d">Last 7 days</option>' +
173
+ '<option value="30d">Last 30 days</option>' +
174
+ '<option value="90d">Last 90 days</option>' +
175
+ '<option value="1y">Last year</option>' +
176
+ '</select>' +
177
+ '</div>' +
178
+ // Filter bar: category chips (populated after cat-count fetch) + sort seg
179
+ '<div id="' + id + '-cats" ' +
180
+ 'style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px;align-items:center">' +
181
+ '<div class="chip on" data-od-act="cat" data-cat="">All categories</div>' +
182
+ '<div style="flex:1"></div>' +
183
+ // Scope view: Mine (this profile) / Shared with me / Global / Everything
184
+ '<div class="seg" title="Which memories to show">' +
185
+ '<button class="active" data-od-act="scope-view" data-scope="mine">Mine</button>' +
186
+ '<button data-od-act="scope-view" data-scope="shared">Shared</button>' +
187
+ '<button data-od-act="scope-view" data-scope="global">Global</button>' +
188
+ '<button data-od-act="scope-view" data-scope="all">All</button>' +
189
+ '</div>' +
190
+ '<div class="seg">' +
191
+ '<button class="active" data-od-act="sort" data-sort="created">Newest</button>' +
192
+ '<button data-od-act="sort" data-sort="score">Score</button>' +
193
+ '<button data-od-act="sort" data-sort="importance">Importance</button>' +
194
+ '</div>' +
195
+ '</div>' +
196
+ '<div class="card" id="' + id + '-tbl-wrap">' + _loading('Loading memories…') + '</div>' +
197
+ '<div id="' + id + '-pg" style="margin-top:10px"></div>'
198
+ );
199
+ }
200
+
201
+ function _tlScaffold(id) {
202
+ return (
203
+ '<div class="card">' +
204
+ '<div class="card-head">' +
205
+ '<h3>Memory creation timeline</h3>' +
206
+ '<span class="sub" id="' + id + '-tl-sub">facts stored per day</span>' +
207
+ '<div style="flex:1"></div>' +
208
+ '<div class="seg">' +
209
+ '<button data-od-act="tl-range" data-range="7d">7d</button>' +
210
+ '<button class="active" data-od-act="tl-range" data-range="30d">30d</button>' +
211
+ '</div>' +
212
+ '</div>' +
213
+ '<div class="card-pad">' +
214
+ '<div class="bars" id="' + id + '-bars" style="height:120px">' +
215
+ _loading('Loading timeline…') +
216
+ '</div>' +
217
+ '<div style="display:flex;justify-content:space-between;margin-top:10px;' +
218
+ 'font-size:11.5px;color:var(--fg-3)">' +
219
+ '<span id="' + id + '-tl-start"></span><span>today</span>' +
220
+ '</div>' +
221
+ '</div>' +
222
+ '</div>' +
223
+ '<div class="card" style="margin-top:16px">' +
224
+ '<div class="card-head"><h3>By fact type</h3></div>' +
225
+ '<div class="card-pad" id="' + id + '-ftypes">' + _loading('Loading breakdown…') + '</div>' +
226
+ '</div>'
227
+ );
228
+ }
229
+
230
+ function _loading(msg) {
231
+ return '<div style="padding:40px;text-align:center;color:var(--fg-2);font-size:13px">' +
232
+ _esc(msg) + '</div>';
233
+ }
234
+
235
+ // ── Tab switching ────────────────────────────────────────────────────────────
236
+
237
+ function _switchTab(id, tab) {
238
+ _st = Object.assign({}, _st, { activeTab: tab });
239
+ var root = document.getElementById(id);
240
+ if (!root) return;
241
+ root.querySelectorAll('#' + id + '-tabs .tab').forEach(function (b) {
242
+ b.classList.toggle('active', b.dataset.tab === tab);
243
+ });
244
+ root.querySelectorAll('.tabpane').forEach(function (p) { p.classList.remove('active'); });
245
+ var pane = document.getElementById(id + '-pane-' + tab);
246
+ if (pane) pane.classList.add('active');
247
+ if (tab === 'timeline') _loadTimeline(id);
248
+ if (tab === 'clusters') _loadClusters(id);
249
+ }
250
+
251
+ // ── Category counts (pre-fetch real totals) ──────────────────────────────────
252
+
253
+ function _loadCatCounts(id) {
254
+ Promise.all(KNOWN_CATS.map(function (cat) {
255
+ return fetch('/api/memories?limit=1&category=' + encodeURIComponent(cat))
256
+ .then(function (r) { return r.json(); })
257
+ .then(function (d) { return { cat: cat, total: d.total || 0 }; })
258
+ .catch(function () { return { cat: cat, total: 0 }; });
259
+ })).then(function (results) {
260
+ var counts = {};
261
+ results.forEach(function (r) { if (r.total > 0) counts[r.cat] = r.total; });
262
+ _st = Object.assign({}, _st, { catCounts: counts });
263
+ _rebuildCatBar(id);
264
+ });
265
+ }
266
+
267
+ // Rebuild the full filter bar (chips + sort) from current _st
268
+ function _rebuildCatBar(id) {
269
+ var bar = document.getElementById(id + '-cats');
270
+ if (!bar) return;
271
+ var active = _st.category || '';
272
+ var html = '<div class="chip' + (!active ? ' on' : '') +
273
+ '" data-od-act="cat" data-cat="">All categories</div>';
274
+ Object.keys(_st.catCounts).forEach(function (cat) {
275
+ html += '<div class="chip' + (_st.category === cat ? ' on' : '') +
276
+ '" data-od-act="cat" data-cat="' + _esc(cat) + '">' +
277
+ _esc(cat) + ' <span class="cnt">' +
278
+ _st.catCounts[cat].toLocaleString() + '</span></div>';
279
+ });
280
+ var sv = _st.scopeView || 'mine';
281
+ html += '<div style="flex:1"></div>' +
282
+ '<div class="seg" title="Which memories to show">' +
283
+ ['mine', 'shared', 'global', 'all'].map(function (s) {
284
+ var label = { mine: 'Mine', shared: 'Shared', global: 'Global', all: 'All' }[s];
285
+ return '<button class="' + (sv === s ? 'active' : '') +
286
+ '" data-od-act="scope-view" data-scope="' + s + '">' + label + '</button>';
287
+ }).join('') +
288
+ '</div>' +
289
+ '<div class="seg">' +
290
+ '<button class="' + (_st.sort === 'created' ? 'active' : '') +
291
+ '" data-od-act="sort" data-sort="created">Newest</button>' +
292
+ '<button class="' + (_st.sort === 'score' ? 'active' : '') +
293
+ '" data-od-act="sort" data-sort="score">Score</button>' +
294
+ '<button class="' + (_st.sort === 'importance' ? 'active' : '') +
295
+ '" data-od-act="sort" data-sort="importance">Importance</button>' +
296
+ '</div>';
297
+ bar.innerHTML = html;
298
+ }
299
+
300
+ // ── Memories fetch & render ──────────────────────────────────────────────────
301
+
302
+ function _loadMem(id) {
303
+ var url = '/api/memories?limit=' + _st.pageSize +
304
+ '&offset=' + (_st.page * _st.pageSize);
305
+ if (_st.category) url += '&category=' + encodeURIComponent(_st.category);
306
+ if (_st.scopeView && _st.scopeView !== 'mine') {
307
+ url += '&scope=' + encodeURIComponent(_st.scopeView);
308
+ }
309
+ var wrap = document.getElementById(id + '-tbl-wrap');
310
+ if (wrap) wrap.innerHTML = _loading('Loading memories…');
311
+
312
+ fetch(url)
313
+ .then(function (r) {
314
+ if (!r.ok) throw new Error('HTTP ' + r.status);
315
+ return r.json();
316
+ })
317
+ .then(function (d) {
318
+ _st = Object.assign({}, _st, { memories: d.memories || [], total: d.total || 0 });
319
+ _renderTable(id, d);
320
+ })
321
+ .catch(function (err) {
322
+ var w = document.getElementById(id + '-tbl-wrap');
323
+ if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center">' +
324
+ 'Failed to load memories: ' + _esc(err.message) + '</div>';
325
+ });
326
+ }
327
+
328
+ function _renderTable(id, d) {
329
+ var mems = d.memories || [];
330
+ var total = d.total || 0;
331
+ var wrap = document.getElementById(id + '-tbl-wrap');
332
+ var cntEl = document.getElementById(id + '-cnt-all');
333
+ var subEl = document.getElementById(id + '-sub');
334
+
335
+ if (cntEl) cntEl.textContent = total.toLocaleString();
336
+ if (subEl) {
337
+ var _projs = mems.reduce(function (s, m) {
338
+ if (m.project_name) s.add(m.project_name);
339
+ return s;
340
+ }, new Set()).size;
341
+ var _pStr = _projs > 0
342
+ ? ' across ' + _projs + ' project' + (_projs === 1 ? '' : 's')
343
+ : '';
344
+ subEl.textContent = total.toLocaleString() + ' memories' + _pStr +
345
+ '. Every memory shows its provenance — why it was remembered and how strongly it scored.';
346
+ }
347
+ if (!wrap) return;
348
+
349
+ if (mems.length === 0) {
350
+ wrap.innerHTML = '<div class="card-pad" style="text-align:center;padding:40px;' +
351
+ 'color:var(--fg-2)">No memories found. Try a different filter.</div>';
352
+ _renderPag(id, d);
353
+ return;
354
+ }
355
+
356
+ var rows = mems.map(function (m, idx) {
357
+ var imp = _imp(m.importance);
358
+ var impCls = imp >= 8 ? 'ok' : imp >= 5 ? 'warn' : 'neutral';
359
+ var scorePct = Math.round((parseFloat(m.importance) || 0) * 100);
360
+ var scoreCls = _scoreCls(m.importance);
361
+ var cat = m.category || 'semantic';
362
+ var preview = (m.content || '').substring(0, 120);
363
+ if ((m.content || '').length > 120) preview += '…';
364
+
365
+ return '<tr class="row" data-od-act="drawer" data-idx="' + idx + '" style="cursor:pointer">' +
366
+ '<td style="max-width:420px">' +
367
+ '<b class="mono dim" style="font-size:11px">#' +
368
+ _esc((m.id || '').substring(0, 8)) +
369
+ '</b>' +
370
+ '<div style="margin-top:2px">' + _esc(preview) + '</div>' +
371
+ '</td>' +
372
+ '<td><span class="badge ' + _esc(_catCls(cat)) + '">' + _esc(cat) + '</span></td>' +
373
+ '<td class="mono dim" style="font-size:12px">' + _esc(m.project_name || '—') + '</td>' +
374
+ '<td><b class="num">' + imp + '</b>/10</td>' +
375
+ '<td><span class="badge ' + _esc(scoreCls) + '">' + scorePct + '%</span></td>' +
376
+ '<td class="mono dim" style="font-size:12px">' + _esc(_fmt(m.created_at)) + '</td>' +
377
+ '</tr>';
378
+ }).join('');
379
+
380
+ wrap.innerHTML =
381
+ '<table class="tbl">' +
382
+ '<thead><tr>' +
383
+ '<th>Memory</th><th>Category</th><th>Project</th>' +
384
+ '<th>Importance</th><th>Score</th><th>Created</th>' +
385
+ '</tr></thead>' +
386
+ '<tbody>' + rows + '</tbody>' +
387
+ '</table>';
388
+
389
+ _renderPag(id, d);
390
+ }
391
+
392
+ function _renderPag(id, d) {
393
+ var el = document.getElementById(id + '-pg');
394
+ if (!el) return;
395
+ var tot = d.total || 0;
396
+ var lim = d.limit || _st.pageSize;
397
+ var off = d.offset || (_st.page * _st.pageSize);
398
+ var pg = Math.floor(off / lim);
399
+ var last = Math.max(0, Math.ceil(tot / lim) - 1);
400
+ var show = Math.min(off + lim, tot);
401
+ var from = tot === 0 ? 0 : off + 1;
402
+
403
+ el.innerHTML =
404
+ '<div style="display:flex;justify-content:space-between;align-items:center;' +
405
+ 'font-size:12.5px;color:var(--fg-2)">' +
406
+ '<span>Showing ' + from + '–' + show + ' of ' + tot.toLocaleString() + '</span>' +
407
+ '<div class="seg">' +
408
+ '<button ' + (pg <= 0 ? 'disabled style="opacity:.4"' :
409
+ 'data-od-act="pg" data-page="' + (pg - 1) + '"') + '>← Prev</button>' +
410
+ '<button disabled style="opacity:.7;cursor:default">Page ' +
411
+ (pg + 1) + ' / ' + (last + 1) + '</button>' +
412
+ '<button ' + (pg >= last ? 'disabled style="opacity:.4"' :
413
+ 'data-od-act="pg" data-page="' + (pg + 1) + '"') + '>Next →</button>' +
414
+ '</div>' +
415
+ '</div>';
416
+ }
417
+
418
+ // ── Sort (client-side on current page) ──────────────────────────────────────
419
+
420
+ function _doSort(id, by) {
421
+ _st = Object.assign({}, _st, { sort: by });
422
+ var mems = _st.memories.slice();
423
+ // 'score' and 'importance' both rank by the same underlying importance float
424
+ if (by === 'importance' || by === 'score') {
425
+ mems.sort(function (a, b) {
426
+ return (parseFloat(b.importance) || 0) - (parseFloat(a.importance) || 0);
427
+ });
428
+ } else {
429
+ mems.sort(function (a, b) {
430
+ return String(b.created_at || '').localeCompare(String(a.created_at || ''));
431
+ });
432
+ }
433
+ _st = Object.assign({}, _st, { memories: mems });
434
+ _renderTable(id, {
435
+ memories: mems, total: _st.total,
436
+ limit: _st.pageSize, offset: _st.page * _st.pageSize,
437
+ });
438
+ }
439
+
440
+ // ── Search — POST /api/search (full corpus, 0.5–1.4 s warm) ─────────────────
441
+
442
+ var _searchTimer = null;
443
+ function _doSearch(id, q) {
444
+ clearTimeout(_searchTimer);
445
+ _searchTimer = setTimeout(function () {
446
+ _st = Object.assign({}, _st, { searchQ: q || null });
447
+ if (!q) { _loadMem(id); return; }
448
+ var wrap = document.getElementById(id + '-tbl-wrap');
449
+ if (wrap) wrap.innerHTML = _loading('Searching…');
450
+ fetch('/api/search', { method: 'POST', credentials: 'same-origin',
451
+ headers: { 'Content-Type': 'application/json' },
452
+ body: JSON.stringify({ query: q, limit: 50, window: _st.window || '' })
453
+ }).then(function (r) {
454
+ if (!r.ok) throw new Error('HTTP ' + r.status);
455
+ return r.json();
456
+ }).then(function (d) {
457
+ var mems = (d.results || []).map(_normSearch);
458
+ _st = Object.assign({}, _st, { memories: mems, total: mems.length });
459
+ _renderTable(id, { memories: mems, total: mems.length, limit: mems.length, offset: 0 });
460
+ }).catch(function (err) {
461
+ var w = document.getElementById(id + '-tbl-wrap');
462
+ if (w) w.innerHTML = '<div class="card-pad" style="color:var(--danger);text-align:center;' +
463
+ 'padding:24px">Search failed: ' + _esc(err.message) + '</div>';
464
+ });
465
+ }, 350);
466
+ }
467
+
468
+ // ── Timeline fetch & render ──────────────────────────────────────────────────
469
+
470
+ var _tlLoaded = {};
471
+ function _loadTimeline(id) {
472
+ var range = _st.tlRange;
473
+ if (_tlLoaded[id + '-' + range]) return;
474
+ var barsEl = document.getElementById(id + '-bars');
475
+ var ftEl = document.getElementById(id + '-ftypes');
476
+ var startEl = document.getElementById(id + '-tl-start');
477
+
478
+ fetch('/api/v3/timeline/?range=' + range + '&group_by=category&limit=1000')
479
+ .then(function (r) { return r.json(); })
480
+ .then(function (d) {
481
+ var events = d.events || [];
482
+ _tlLoaded[id + '-' + range] = true;
483
+ if (events.length === 0) {
484
+ if (barsEl) barsEl.innerHTML =
485
+ '<div style="display:grid;place-items:center;width:100%;height:100%;' +
486
+ 'color:var(--fg-3);font-size:13px">No memory events in this range.</div>';
487
+ if (ftEl) ftEl.innerHTML =
488
+ '<div style="color:var(--fg-3);font-size:13px">No category data available.</div>';
489
+ return;
490
+ }
491
+ _renderBars(id, events, range, barsEl, startEl);
492
+ _renderFtypes(id, events, ftEl);
493
+ })
494
+ .catch(function (err) {
495
+ if (barsEl) barsEl.innerHTML =
496
+ '<div style="color:var(--fg-3);font-size:13px;text-align:center;padding:16px">' +
497
+ 'Timeline unavailable: ' + _esc(err.message) + '</div>';
498
+ // TODO: endpoint returns 404 when timeline feature is disabled
499
+ });
500
+ }
501
+
502
+ function _renderBars(id, events, range, barsEl, startEl) {
503
+ if (!barsEl) return;
504
+ var days = range === '30d' ? 30 : 7;
505
+ var today = new Date();
506
+ var counts = new Array(days).fill(0);
507
+ events.forEach(function (ev) {
508
+ var diff = Math.floor((today - new Date(ev.timestamp)) / 86400000);
509
+ if (diff >= 0 && diff < days) counts[days - 1 - diff]++;
510
+ });
511
+ if (typeof slmBars === 'function') {
512
+ slmBars(barsEl, counts);
513
+ } else {
514
+ var maxVal = Math.max.apply(null, counts) || 1;
515
+ barsEl.innerHTML = counts.map(function (v) {
516
+ return '<i style="height:' + Math.max(4, (v / maxVal) * 100) + '%' +
517
+ '" title="' + v + ' events"></i>';
518
+ }).join('');
519
+ }
520
+ if (startEl) startEl.textContent = days + 'd ago';
521
+ var subEl = document.getElementById(id + '-tl-sub');
522
+ if (subEl) subEl.textContent = events.length.toLocaleString() + ' events · last ' + days + ' days';
523
+ }
524
+
525
+ function _renderFtypes(id, events, ftEl) {
526
+ if (!ftEl) return;
527
+ var catMap = {};
528
+ events.forEach(function (ev) {
529
+ catMap[ev.category || 'unknown'] = (catMap[ev.category || 'unknown'] || 0) + 1;
530
+ });
531
+ var total = events.length;
532
+ var colors = { temporal:'var(--ok)', semantic:'var(--violet)', episodic:'var(--cyan)', opinion:'var(--warn)' };
533
+ var sorted = Object.keys(catMap).sort(function (a, b) { return catMap[b] - catMap[a]; });
534
+ ftEl.innerHTML = sorted.map(function (cat) {
535
+ var pct = Math.round(catMap[cat] / total * 100);
536
+ var color = colors[cat] || 'var(--fg-3)';
537
+ return '<div style="margin-bottom:13px">' +
538
+ '<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px">' +
539
+ '<span class="mono">' + _esc(cat) + '</span><b>' + pct + '%</b>' +
540
+ '</div>' +
541
+ '<div class="meter"><i style="width:' + pct + '%;background:' + color + '"></i></div>' +
542
+ '</div>';
543
+ }).join('');
544
+ }
545
+
546
+ // ── Clusters fetch & render ──────────────────────────────────────────────────
547
+
548
+ function _loadClusters(id) {
549
+ if (_st.cluLoaded) return;
550
+ var grid = document.getElementById(id + '-clu-grid');
551
+ if (!grid) return;
552
+ fetch('/api/clusters')
553
+ .then(function (r) {
554
+ if (!r.ok) throw new Error('HTTP ' + r.status);
555
+ return r.json();
556
+ })
557
+ .then(function (d) {
558
+ _st = Object.assign({}, _st, { cluLoaded: true });
559
+ var clus = d.clusters || [];
560
+ var cntEl = document.getElementById(id + '-cnt-clusters');
561
+ if (cntEl) cntEl.textContent = clus.length.toLocaleString();
562
+ if (clus.length === 0) {
563
+ grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:40px;' +
564
+ 'color:var(--fg-2)">No clusters yet. Clusters form as memories accumulate.</div>';
565
+ return;
566
+ }
567
+ _renderClusters(id, clus, grid);
568
+ })
569
+ .catch(function (err) {
570
+ if (grid) grid.innerHTML =
571
+ '<div style="grid-column:1/-1;text-align:center;padding:24px;' +
572
+ 'color:var(--danger);font-size:13px">Failed to load clusters: ' +
573
+ _esc(err.message) + '</div>';
574
+ });
575
+ }
576
+
577
+ var _PAL = ['var(--violet)', 'var(--cyan)', 'var(--warn)', 'var(--ok)', 'var(--danger)'];
578
+
579
+ function _renderClusters(id, clus, grid) {
580
+ grid.innerHTML = clus.map(function (c, i) {
581
+ var color = _PAL[i % _PAL.length];
582
+ var summary = c.summary || c.categories || '';
583
+ var preview = summary.length > 80 ? summary.substring(0, 80) + '…' : summary;
584
+ var cid = String(c.cluster_id || '');
585
+ return '<div class="launch-card card" style="cursor:pointer"' +
586
+ ' data-od-act="expand-cluster" data-cid="' + _esc(cid) + '">' +
587
+ '<div class="ic" style="background:color-mix(in srgb,' + color + ' 15%,transparent);color:' + color + '">' +
588
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="21" height="21">' +
589
+ '<path d="M12 3l1.9 4.6L18 9l-3.5 3 1 5-3.5-2.5L8.5 17l1-5L6 9l4.1-1.4z"/>' +
590
+ '</svg>' +
591
+ '</div>' +
592
+ '<h3>' + _esc(preview) + '</h3>' +
593
+ '<p><b class="num">' + _esc(String(c.member_count)) + '</b> memories · expand →</p>' +
594
+ '<div style="display:none;margin-top:14px;border-top:1px solid var(--border);' +
595
+ 'padding-top:12px;font-size:13px" id="' + id + '-clu-' + _esc(cid) + '"></div>' +
596
+ '</div>';
597
+ }).join('');
598
+ }
599
+
600
+ function _expandCluster(id, cid) {
601
+ var detEl = document.getElementById(id + '-clu-' + cid);
602
+ if (!detEl) return;
603
+ if (detEl.style.display !== 'none') { detEl.style.display = 'none'; return; }
604
+ detEl.style.display = 'block';
605
+ if (detEl.dataset.loaded) return;
606
+ detEl.dataset.loaded = '1';
607
+ detEl.textContent = 'Loading members…';
608
+ fetch('/api/clusters/' + encodeURIComponent(cid) + '?limit=5')
609
+ .then(function (r) { return r.json(); })
610
+ .then(function (d) {
611
+ var members = d.members || [];
612
+ if (members.length === 0) { detEl.textContent = 'No members found.'; return; }
613
+ detEl.innerHTML = members.map(function (m, i) {
614
+ var txt = m.content || m.summary || '';
615
+ return '<div style="margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid var(--border)">' +
616
+ '<div style="font-size:12.5px">' + (i + 1) + '. ' +
617
+ _esc(txt.substring(0, 120)) + '</div></div>';
618
+ }).join('');
619
+ })
620
+ .catch(function () { detEl.textContent = 'Failed to load members.'; });
621
+ }
622
+
623
+ // ── Provenance drawer ────────────────────────────────────────────────────────
624
+
625
+ function _ensureDrawer() {
626
+ if (document.getElementById('od-mem-drawer')) return;
627
+ var scrim = document.createElement('div');
628
+ scrim.id = 'od-mem-drawer-scrim';
629
+ scrim.className = 'od-drawer-scrim';
630
+ scrim.dataset.odAct = 'close-drawer';
631
+ document.body.appendChild(scrim);
632
+ var drawer = document.createElement('aside');
633
+ drawer.id = 'od-mem-drawer';
634
+ drawer.className = 'od-drawer';
635
+ document.body.appendChild(drawer);
636
+ }
637
+
638
+ function _openDrawer(idx) {
639
+ var mem = _st.memories[idx];
640
+ if (!mem) return;
641
+ _ensureDrawer();
642
+ var drawer = document.getElementById('od-mem-drawer');
643
+ var scrim = document.getElementById('od-mem-drawer-scrim');
644
+ if (!drawer || !scrim) return;
645
+
646
+ var imp = _imp(mem.importance);
647
+ var cat = mem.category || 'semantic';
648
+ var scorePct = Math.round((parseFloat(mem.importance) || 0) * 100);
649
+
650
+ drawer.innerHTML =
651
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start">' +
652
+ '<div>' +
653
+ '<div style="font-size:12px;color:var(--fg-2)">Memory</div>' +
654
+ '<h2 style="font-size:19px;margin-top:2px">' +
655
+ _esc('#' + (mem.id || '').substring(0, 12)) + '</h2>' +
656
+ '</div>' +
657
+ '<button class="btn icon ghost" data-od-act="close-drawer">✕</button>' +
658
+ '</div>' +
659
+ '<div class="od-prov" style="font-size:14px;line-height:1.7">' +
660
+ _esc(mem.content || '') +
661
+ '</div>' +
662
+ '<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">' +
663
+ '<span class="badge ' + _esc(_catCls(cat)) + '">' + _esc(cat) + '</span>' +
664
+ '<span class="badge neutral">' + _esc(mem.project_name || 'no project') + '</span>' +
665
+ '<span class="badge ' + (mem.scope === 'global' ? 'success' : mem.scope === 'shared' ? 'warn' : 'neutral') +
666
+ '">' + _esc(mem.scope || 'personal') + '</span>' +
667
+ '</div>' +
668
+ // Scope & sharing control (C2) — set personal/shared/global from the UI
669
+ '<h3 style="font-size:13px;margin:20px 0 8px">Scope &amp; sharing</h3>' +
670
+ '<div class="od-prov" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">' +
671
+ '<select id="od-scope-sel" style="padding:6px 8px;border-radius:6px;background:var(--card-2);color:var(--fg);border:1px solid var(--border);font-size:13px">' +
672
+ ['personal', 'shared', 'global'].map(function (s) {
673
+ return '<option value="' + s + '"' + ((mem.scope || 'personal') === s ? ' selected' : '') + '>' + s + '</option>';
674
+ }).join('') +
675
+ '</select>' +
676
+ '<input id="od-scope-shared" placeholder="share with profiles (comma-sep)" ' +
677
+ 'value="' + _esc(_sharedToStr(mem.shared_with)) + '" ' +
678
+ 'style="flex:1;min-width:140px;padding:6px 8px;border-radius:6px;background:var(--card-2);color:var(--fg);border:1px solid var(--border);font-size:13px">' +
679
+ '<button data-od-act="set-scope" data-mid="' + _esc(mem.id) + '" ' +
680
+ 'style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--card-2);color:var(--fg);cursor:pointer;font-size:13px">Apply</button>' +
681
+ '</div>' +
682
+ '<h3 style="font-size:13px;margin:20px 0 8px">Why this was remembered</h3>' +
683
+ '<div class="od-prov">' +
684
+ '<div style="font-size:13px;color:var(--fg-2);line-height:1.6">' +
685
+ 'Importance: <b>' + imp + '/10</b>. ' +
686
+ 'Score: <b>' + scorePct + '%</b>. ' +
687
+ 'Recalled <b>' + _esc(String(mem.access_count || 0)) + '</b> time' +
688
+ (mem.access_count === 1 ? '' : 's') + '. ' +
689
+ 'Stored ' + _esc(_ago(mem.created_at)) + '.' +
690
+ '</div>' +
691
+ // TODO: /api/memories/{id}/score-breakdown — Semantic/BM25/EntityGraph/Temporal
692
+ // decomposition is not yet exposed by the daemon; show when available.
693
+ '</div>' +
694
+ '<h3 style="font-size:13px;margin:20px 0 8px">Atomic facts</h3>' +
695
+ '<div id="od-drawer-facts" style="color:var(--fg-3);font-size:13px">Loading facts…</div>' +
696
+ '<div style="display:flex;gap:8px;margin-top:20px;padding-top:12px;border-top:1px solid var(--border)">' +
697
+ '<button data-od-act="edit-mem" data-mid="' + _esc(mem.id) + '" style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--card-2);color:var(--fg);cursor:pointer;font-size:13px">Edit</button>' +
698
+ '<button data-od-act="del-mem" data-mid="' + _esc(mem.id) + '" style="padding:6px 14px;border:1px solid var(--danger);border-radius:6px;background:transparent;color:var(--danger);cursor:pointer;font-size:13px">Delete</button>' +
699
+ '</div><div id="od-mem-act" style="margin-top:10px"></div>';
700
+
701
+ drawer.classList.add('open');
702
+ scrim.classList.add('on');
703
+ if (mem.id) _loadFacts(mem.id);
704
+ }
705
+
706
+ function _closeDrawer() {
707
+ var d = document.getElementById('od-mem-drawer');
708
+ var s = document.getElementById('od-mem-drawer-scrim');
709
+ if (d) d.classList.remove('open');
710
+ if (s) s.classList.remove('on');
711
+ }
712
+
713
+ function _loadFacts(memId) {
714
+ var el = document.getElementById('od-drawer-facts');
715
+ if (!el) return;
716
+ fetch('/api/memories/' + encodeURIComponent(memId) + '/facts')
717
+ .then(function (r) { return r.json(); })
718
+ .then(function (d) {
719
+ var facts = d.facts || [];
720
+ if (facts.length === 0) {
721
+ el.textContent = 'No atomic facts recorded for this memory.'; return;
722
+ }
723
+ el.innerHTML = facts.map(function (f) {
724
+ return '<div class="od-prov" style="margin-bottom:8px">' +
725
+ '<span class="badge neutral" style="font-size:10px">' +
726
+ _esc(f.fact_type || 'fact') +
727
+ '</span>' +
728
+ '<div style="font-size:13px;line-height:1.6;margin-top:4px">' +
729
+ _esc((f.content || '').substring(0, 200)) +
730
+ '</div></div>';
731
+ }).join('');
732
+ })
733
+ .catch(function () { if (el) el.textContent = 'Could not load facts.'; });
734
+ }
735
+
736
+ function _startEdit(mid) { // show inline edit form in drawer
737
+ var m = _st.memories.filter(function (x) { return x.id === mid; })[0];
738
+ var el = document.getElementById('od-mem-act'); if (!el || !m) return;
739
+ el.innerHTML = '<textarea id="od-ea" rows="4" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--card-2);color:var(--fg);font-size:13px">' + _esc(m.content) + '</textarea><button data-od-act="save-edit" data-mid="' + _esc(mid) + '" style="display:block;margin-top:6px;padding:6px 14px;border:none;border-radius:6px;background:var(--violet);color:#fff;cursor:pointer">Save</button>';
740
+ }
741
+ function _startDel(mid) { // show delete confirmation in drawer
742
+ var el = document.getElementById('od-mem-act'); if (!el) return;
743
+ el.innerHTML = '<p style="font-size:13px;color:var(--fg-2);margin-bottom:10px">Permanently delete this memory? This cannot be undone.</p><button data-od-act="confirm-del" data-mid="' + _esc(mid) + '" style="padding:6px 14px;border-radius:6px;background:var(--danger);color:#fff;border:none;cursor:pointer;margin-right:8px">Confirm delete</button><button data-od-act="cancel-act" style="padding:6px 14px;border-radius:6px;background:var(--card-2);border:1px solid var(--border);color:var(--fg);cursor:pointer">Cancel</button>';
744
+ }
745
+
746
+ // ── Event delegation ─────────────────────────────────────────────────────────
747
+ function _wire(container, id) {
748
+ container.addEventListener('click', function (e) {
749
+ var el = e.target.closest('[data-od-act]');
750
+ if (!el) return;
751
+ var act = el.dataset.odAct;
752
+
753
+ if (act === 'tab') {
754
+ _switchTab(id, el.dataset.tab);
755
+ return;
756
+ }
757
+ if (act === 'cat') {
758
+ // Toggle on/off chip states without a full re-render
759
+ var bar = document.getElementById(id + '-cats');
760
+ if (bar) bar.querySelectorAll('[data-od-act="cat"]').forEach(function (c) {
761
+ c.classList.toggle('on', c.dataset.cat === el.dataset.cat);
762
+ });
763
+ _st = Object.assign({}, _st, { category: el.dataset.cat || null, page: 0 });
764
+ _loadMem(id);
765
+ return;
766
+ }
767
+ if (act === 'sort') {
768
+ // Update active state on sort buttons in-place
769
+ var root = document.getElementById(id);
770
+ if (root) root.querySelectorAll('[data-od-act="sort"]').forEach(function (b) {
771
+ b.classList.toggle('active', b.dataset.sort === el.dataset.sort);
772
+ });
773
+ _doSort(id, el.dataset.sort);
774
+ return;
775
+ }
776
+ if (act === 'scope-view') {
777
+ var sroot = document.getElementById(id);
778
+ if (sroot) sroot.querySelectorAll('[data-od-act="scope-view"]').forEach(function (b) {
779
+ b.classList.toggle('active', b.dataset.scope === el.dataset.scope);
780
+ });
781
+ _st = Object.assign({}, _st, { scopeView: el.dataset.scope, page: 0 });
782
+ _loadMem(id);
783
+ return;
784
+ }
785
+ if (act === 'drawer') {
786
+ var row = el.closest('tr[data-od-act]');
787
+ var idx = parseInt((row || el).dataset.idx, 10);
788
+ if (!isNaN(idx)) _openDrawer(idx);
789
+ return;
790
+ }
791
+ if (act === 'close-drawer') { _closeDrawer(); return; }
792
+ if (act === 'pg') {
793
+ var pg = parseInt(el.dataset.page, 10);
794
+ if (!isNaN(pg)) { _st = Object.assign({}, _st, { page: pg }); _loadMem(id); }
795
+ return;
796
+ }
797
+ if (act === 'expand-cluster') { _expandCluster(id, el.dataset.cid); return; }
798
+ if (act === 'tl-range') {
799
+ var root2 = document.getElementById(id);
800
+ if (root2) root2.querySelectorAll('[data-od-act="tl-range"]').forEach(function (b) {
801
+ b.classList.toggle('active', b.dataset.range === el.dataset.range);
802
+ });
803
+ _tlLoaded[id + '-' + _st.tlRange] = false; // invalidate cache
804
+ _st = Object.assign({}, _st, { tlRange: el.dataset.range });
805
+ _loadTimeline(id);
806
+ return;
807
+ }
808
+ });
809
+
810
+ // Search — input event
811
+ container.addEventListener('change', function (e) {
812
+ if (e.target.dataset.odAct === 'window') {
813
+ _st = Object.assign({}, _st, { window: e.target.value || '' });
814
+ var wq = (_st.searchQ || '').trim();
815
+ if (wq) _doSearch(id, wq); // re-run active search with the new window
816
+ }
817
+ });
818
+ container.addEventListener('input', function (e) {
819
+ if (e.target.dataset.odAct === 'search') _doSearch(id, e.target.value.trim());
820
+ });
821
+
822
+ // Drawer actions — document-level (drawer is outside container), guarded once
823
+ if (!window._slmMemDrawerWired) {
824
+ window._slmMemDrawerWired = true;
825
+ document.addEventListener('click', function (e) {
826
+ var el = e.target.closest('[data-od-act]');
827
+ if (!el) return;
828
+ var act = el.dataset.odAct;
829
+ if (act === 'close-drawer') { _closeDrawer(); return; }
830
+ if (act === 'edit-mem') { _startEdit(el.dataset.mid); return; }
831
+ if (act === 'del-mem') { _startDel(el.dataset.mid); return; }
832
+ if (act === 'cancel-act') { var ac = document.getElementById('od-mem-act'); if (ac) ac.innerHTML = ''; return; }
833
+ if (act === 'set-scope') {
834
+ var smid = el.dataset.mid;
835
+ var scopeVal = (document.getElementById('od-scope-sel') || {}).value || 'personal';
836
+ var sharedVal = ((document.getElementById('od-scope-shared') || {}).value || '').trim();
837
+ if (scopeVal === 'shared' && !sharedVal) {
838
+ showToast('Enter at least one profile to share with'); return;
839
+ }
840
+ _getMutToken().then(function (tok) {
841
+ if (!tok) { showToast('Auth unavailable — reload the page'); return; }
842
+ fetch('/api/memories/' + encodeURIComponent(smid) + '/scope', {
843
+ method: 'PATCH', credentials: 'same-origin',
844
+ headers: { 'Content-Type': 'application/json', 'X-Install-Token': tok },
845
+ body: JSON.stringify({ scope: scopeVal, shared_with: sharedVal }),
846
+ })
847
+ .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
848
+ .then(function () { showToast('Scope updated → ' + scopeVal); _closeDrawer(); _loadMem(_st.rootId); })
849
+ .catch(function (e) { showToast('Scope update failed: ' + e.message); });
850
+ });
851
+ return;
852
+ }
853
+ if (act === 'save-edit' || act === 'confirm-del') {
854
+ var rid = _st.rootId, isDel = act === 'confirm-del', mid = el.dataset.mid;
855
+ var content = isDel ? '' : ((document.getElementById('od-ea') || {}).value || '').trim();
856
+ if (!isDel && !content) { showToast('Content cannot be empty'); return; }
857
+ _getMutToken().then(function (tok) {
858
+ if (!tok) { showToast('Auth unavailable — reload the page'); return; }
859
+ var opts = isDel
860
+ ? {method:'DELETE',credentials:'same-origin',headers:{'X-Install-Token':tok}}
861
+ : {method:'PATCH',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Install-Token':tok},body:JSON.stringify({content:content})};
862
+ fetch('/api/memories/' + encodeURIComponent(mid), opts)
863
+ .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
864
+ .then(function () { showToast(isDel ? 'Memory deleted' : 'Memory updated'); _closeDrawer(); _loadMem(rid); })
865
+ .catch(function (e) { showToast((isDel ? 'Delete' : 'Edit') + ' failed: ' + e.message); });
866
+ });
867
+ return;
868
+ }
869
+ });
870
+ }
871
+ }
872
+
873
+ // ── Public API + auto-init ───────────────────────────────────────────────────
874
+
875
+ window.odRenderMemories = render;
876
+
877
+ if (document.readyState === 'loading') {
878
+ document.addEventListener('DOMContentLoaded', function () {
879
+ var p = document.getElementById('memories-pane');
880
+ if (p) render(p);
881
+ });
882
+ } else {
883
+ var _p = document.getElementById('memories-pane');
884
+ if (_p) render(_p);
885
+ }
886
+
887
+ }());