@agentmemory/agentmemory 0.9.1 → 0.9.3

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 (72) hide show
  1. package/README.md +40 -13
  2. package/dist/cli.mjs +147 -26
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/hooks/notification.mjs +6 -0
  5. package/dist/hooks/notification.mjs.map +1 -1
  6. package/dist/hooks/post-tool-failure.mjs +6 -0
  7. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  8. package/dist/hooks/post-tool-use.mjs +35 -1
  9. package/dist/hooks/post-tool-use.mjs.map +1 -1
  10. package/dist/hooks/pre-compact.mjs +6 -0
  11. package/dist/hooks/pre-compact.mjs.map +1 -1
  12. package/dist/hooks/pre-tool-use.mjs +6 -0
  13. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  14. package/dist/hooks/prompt-submit.mjs +6 -0
  15. package/dist/hooks/prompt-submit.mjs.map +1 -1
  16. package/dist/hooks/session-end.mjs +6 -0
  17. package/dist/hooks/session-end.mjs.map +1 -1
  18. package/dist/hooks/session-start.mjs +6 -0
  19. package/dist/hooks/session-start.mjs.map +1 -1
  20. package/dist/hooks/stop.mjs +6 -0
  21. package/dist/hooks/stop.mjs.map +1 -1
  22. package/dist/hooks/subagent-start.mjs +6 -0
  23. package/dist/hooks/subagent-start.mjs.map +1 -1
  24. package/dist/hooks/subagent-stop.mjs +6 -0
  25. package/dist/hooks/subagent-stop.mjs.map +1 -1
  26. package/dist/hooks/task-completed.mjs +6 -0
  27. package/dist/hooks/task-completed.mjs.map +1 -1
  28. package/dist/image-refs-CESf9ndJ.mjs +3 -0
  29. package/dist/image-store-DGvZMMrI.mjs +3 -0
  30. package/dist/index.mjs +2100 -157
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{src-Dw_gJcCy.mjs → src-3Snd7D3T.mjs} +2021 -267
  33. package/dist/src-3Snd7D3T.mjs.map +1 -0
  34. package/dist/{standalone-BEWvWM5P.mjs → standalone-BG9uPsDK.mjs} +2 -2
  35. package/dist/{standalone-BEWvWM5P.mjs.map → standalone-BG9uPsDK.mjs.map} +1 -1
  36. package/dist/standalone.mjs +136 -2
  37. package/dist/standalone.mjs.map +1 -1
  38. package/dist/{tools-registry-BvWNlj6u.mjs → tools-registry-m8Ofn9vV.mjs} +166 -12
  39. package/dist/tools-registry-m8Ofn9vV.mjs.map +1 -0
  40. package/dist/viewer/index.html +528 -68
  41. package/package.json +5 -3
  42. package/plugin/.claude-plugin/plugin.json +2 -2
  43. package/plugin/scripts/notification.mjs +6 -0
  44. package/plugin/scripts/notification.mjs.map +1 -1
  45. package/plugin/scripts/post-tool-failure.mjs +6 -0
  46. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  47. package/plugin/scripts/post-tool-use.mjs +35 -1
  48. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  49. package/plugin/scripts/pre-compact.mjs +6 -0
  50. package/plugin/scripts/pre-compact.mjs.map +1 -1
  51. package/plugin/scripts/pre-tool-use.mjs +6 -0
  52. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  53. package/plugin/scripts/prompt-submit.mjs +6 -0
  54. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  55. package/plugin/scripts/session-end.mjs +6 -0
  56. package/plugin/scripts/session-end.mjs.map +1 -1
  57. package/plugin/scripts/session-start.mjs +6 -0
  58. package/plugin/scripts/session-start.mjs.map +1 -1
  59. package/plugin/scripts/stop.mjs +6 -0
  60. package/plugin/scripts/stop.mjs.map +1 -1
  61. package/plugin/scripts/subagent-start.mjs +6 -0
  62. package/plugin/scripts/subagent-start.mjs.map +1 -1
  63. package/plugin/scripts/subagent-stop.mjs +6 -0
  64. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  65. package/plugin/scripts/task-completed.mjs +6 -0
  66. package/plugin/scripts/task-completed.mjs.map +1 -1
  67. package/dist/src-Dw_gJcCy.mjs.map +0 -1
  68. package/dist/tools-registry-BvWNlj6u.mjs.map +0 -1
  69. package/dist/transformers-BX_tgxdO.mjs +0 -38684
  70. package/dist/transformers-BX_tgxdO.mjs.map +0 -1
  71. package/dist/transformers-KMm1i9no.mjs +0 -38683
  72. package/dist/transformers-KMm1i9no.mjs.map +0 -1
@@ -9,6 +9,7 @@
9
9
  :root {
10
10
  --bg: #F9F9F7;
11
11
  --bg-alt: #F0F0EC;
12
+ --bg-subtle: #F4F4F0;
12
13
  --bg-inset: #E8E8E3;
13
14
  --border: #111111;
14
15
  --border-light: #D4D4CF;
@@ -43,6 +44,7 @@
43
44
  html[data-theme="dark"] {
44
45
  --bg: #1a1a1e;
45
46
  --bg-alt: #232328;
47
+ --bg-subtle: #1f1f24;
46
48
  --bg-inset: #2a2a30;
47
49
  --border: #444;
48
50
  --border-light: #3a3a42;
@@ -77,6 +79,8 @@
77
79
  line-height: 1.6;
78
80
  overflow: hidden;
79
81
  height: 100vh;
82
+ display: flex;
83
+ flex-direction: column;
80
84
  background-image: radial-gradient(circle, #D4D4CF 0.5px, transparent 0.5px);
81
85
  background-size: 16px 16px;
82
86
  }
@@ -168,7 +172,7 @@
168
172
  border-bottom-color: var(--accent);
169
173
  }
170
174
 
171
- .view { display: none; height: calc(100vh - 90px); overflow-y: auto; padding: 24px; }
175
+ .view { display: none; flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 24px; }
172
176
  .view.active { display: block; }
173
177
 
174
178
  .stats-grid {
@@ -576,6 +580,81 @@
576
580
  }
577
581
  .empty-state .empty-icon { font-size: 36px; margin-bottom: 10px; opacity: 0.4; }
578
582
  .empty-state p { font-size: 14px; font-family: var(--font-body); font-style: italic; }
583
+ .empty-state .empty-title { font-size: 16px; font-weight: 600; font-style: normal; color: var(--ink-muted); margin-bottom: 8px; }
584
+ .empty-state .empty-lead { font-style: normal; font-size: 14px; color: var(--ink-muted); max-width: 520px; margin: 0 auto 14px; line-height: 1.5; }
585
+ .empty-state pre.empty-cmd {
586
+ display: inline-block; margin: 10px auto 12px; padding: 10px 14px;
587
+ background: var(--bg-alt); border: 1px solid var(--border);
588
+ border-radius: 4px; font-family: var(--font-mono); font-size: 12px;
589
+ color: var(--ink); text-align: left; font-style: normal; white-space: pre;
590
+ }
591
+ .empty-state .empty-link { color: var(--accent); text-decoration: underline; font-size: 13px; font-style: normal; }
592
+
593
+ /* Feature flag banner system — compact collapsed by default */
594
+ .flag-banners { padding: 0 0 10px 0; }
595
+ button.flag-summary {
596
+ display: flex; align-items: center; gap: 12px;
597
+ padding: 8px 14px; border-radius: 4px;
598
+ border: 1px solid var(--border);
599
+ background: var(--bg-subtle);
600
+ font-family: var(--font-ui); font-size: 12px;
601
+ color: var(--ink-muted);
602
+ cursor: pointer; user-select: none;
603
+ width: 100%; text-align: left;
604
+ appearance: none;
605
+ }
606
+ button.flag-summary:hover,
607
+ button.flag-summary:focus-visible { background: var(--bg-alt); outline: 2px solid var(--border); outline-offset: 1px; }
608
+ .flag-summary .flag-count { color: var(--ink); font-weight: 600; }
609
+ .flag-summary .flag-pill {
610
+ display: inline-block; padding: 1px 8px; border-radius: 10px;
611
+ background: #f59e0b20; color: #d97706; font-size: 11px; font-weight: 600;
612
+ margin-right: 6px;
613
+ }
614
+ .flag-summary .flag-pill.info { background: var(--border-light); color: var(--ink-muted); }
615
+ .flag-summary .flag-toggle { margin-left: auto; font-size: 11px; opacity: 0.7; }
616
+ .flag-list {
617
+ display: none; flex-direction: column; gap: 6px;
618
+ margin-top: 6px;
619
+ }
620
+ .flag-list.open { display: flex; }
621
+ .flag-banner {
622
+ display: flex; align-items: flex-start; gap: 10px;
623
+ padding: 10px 14px; border-radius: 3px;
624
+ border: 1px solid var(--border);
625
+ background: var(--bg-subtle);
626
+ font-family: var(--font-ui); font-size: 12px;
627
+ }
628
+ .flag-banner.warn { border-left: 3px solid #f59e0b; }
629
+ .flag-banner.info { border-left: 3px solid var(--ink-muted); }
630
+ .flag-banner .flag-icon { flex-shrink: 0; font-size: 14px; line-height: 1.3; }
631
+ .flag-banner .flag-body { flex: 1; min-width: 0; }
632
+ .flag-banner .flag-title { font-weight: 600; color: var(--ink); margin-bottom: 2px; font-size: 12px; }
633
+ .flag-banner .flag-title code { font-family: var(--font-mono); font-size: 10px; color: var(--ink-muted); font-weight: 400; margin-left: 4px; }
634
+ .flag-banner .flag-desc { color: var(--ink-muted); margin-bottom: 4px; line-height: 1.4; font-size: 12px; }
635
+ .flag-banner .flag-enable {
636
+ display: block; margin-top: 2px; padding: 5px 8px;
637
+ background: var(--bg); border: 1px solid var(--border); border-radius: 3px;
638
+ font-family: var(--font-mono); font-size: 10px; color: var(--ink);
639
+ white-space: pre-wrap; word-break: break-all;
640
+ }
641
+ .flag-banner .flag-close {
642
+ background: none; border: none; color: var(--ink-faint); cursor: pointer;
643
+ font-size: 16px; line-height: 1; padding: 0 4px; font-family: inherit;
644
+ }
645
+ .flag-banner .flag-close:hover { color: var(--ink); }
646
+
647
+ /* Viewer footer */
648
+ .viewer-footer {
649
+ margin-top: 48px; padding: 16px 0 24px;
650
+ border-top: 1px solid var(--border-light);
651
+ display: flex; align-items: center; gap: 10px;
652
+ font-family: var(--font-ui); font-size: 11px;
653
+ color: var(--ink-faint); letter-spacing: 0.05em;
654
+ }
655
+ .viewer-footer a { color: var(--ink-muted); text-decoration: none; }
656
+ .viewer-footer a:hover { color: var(--ink); text-decoration: underline; }
657
+ .viewer-footer .footer-sep { color: var(--ink-faint); opacity: 0.5; }
579
658
 
580
659
  .loading { color: var(--ink-faint); padding: 20px; text-align: center; font-style: italic; font-family: var(--font-body); }
581
660
  .empty { color: var(--ink-muted); padding: 24px; text-align: center; font-family: var(--font-body); font-style: italic; border: 1px dashed var(--border); }
@@ -814,6 +893,8 @@
814
893
  <button data-tab="replay">Replay</button>
815
894
  </div>
816
895
 
896
+ <div id="flag-banners" class="flag-banners"></div>
897
+
817
898
  <div id="view-dashboard" class="view active"></div>
818
899
  <div id="view-graph" class="view"></div>
819
900
  <div id="view-memories" class="view"></div>
@@ -831,6 +912,16 @@
831
912
  <div class="modal" id="modal"></div>
832
913
  </div>
833
914
 
915
+ <footer id="viewer-footer" class="viewer-footer">
916
+ <span>agentmemory viewer · <span id="footer-version">loading...</span></span>
917
+ <span class="footer-sep">·</span>
918
+ <a href="https://github.com/rohitg00/agentmemory" target="_blank" rel="noopener">github</a>
919
+ <span class="footer-sep">·</span>
920
+ <a href="https://github.com/rohitg00/agentmemory#readme" target="_blank" rel="noopener">docs</a>
921
+ <span class="footer-sep">·</span>
922
+ <a id="footer-feedback" href="#" target="_blank" rel="noopener">report issue &rarr;</a>
923
+ </footer>
924
+
834
925
  <script nonce="__AGENTMEMORY_VIEWER_NONCE__">
835
926
  var params = new URLSearchParams(window.location.search);
836
927
  var viewerPort = params.get('port') || window.location.port || '3113';
@@ -900,9 +991,10 @@
900
991
  activity: { loaded: false, observations: [], sessions: [], typeFilter: '' },
901
992
  lessons: { loaded: false, items: [], search: '' },
902
993
  actions: { loaded: false, items: [], frontier: [], statusFilter: '', search: '' },
903
- crystals: { loaded: false, items: [], search: '' },
994
+ crystals: { loaded: false, items: [], search: '', lessonMap: {} },
904
995
  profile: { loaded: false, projects: [], selectedProject: '', data: null },
905
996
  replay: { loaded: false, sessions: [], selectedId: '', timeline: null, cursor: 0, playing: false, speed: 1, timer: null, startAt: 0, offsetAt: 0 },
997
+ flagsConfig: null,
906
998
  ws: null
907
999
  };
908
1000
 
@@ -1034,13 +1126,24 @@
1034
1126
  var dotClass = healthStatus === 'healthy' ? 'healthy' : healthStatus === 'degraded' ? 'degraded' : healthStatus === 'critical' ? 'critical' : '';
1035
1127
  var activeSessions = d.sessions.filter(function(s) { return s.status === 'active'; }).length;
1036
1128
  var gs = d.graphStats || {};
1037
- var nodeCount = (gs.nodes !== undefined) ? gs.nodes : (gs.nodeCount || 0);
1038
- var edgeCount = (gs.edges !== undefined) ? gs.edges : (gs.edgeCount || 0);
1129
+ var nodeCount = gs.totalNodes !== undefined ? gs.totalNodes : (gs.nodes !== undefined ? gs.nodes : (gs.nodeCount || 0));
1130
+ var edgeCount = gs.totalEdges !== undefined ? gs.totalEdges : (gs.edges !== undefined ? gs.edges : (gs.edgeCount || 0));
1039
1131
  var fMetrics = h.functionMetrics || [];
1040
1132
  var cb = h.circuitBreaker || null;
1041
1133
  var workers = snap.workers || [];
1042
1134
 
1043
- var html = '<div class="stats-grid">';
1135
+ var html = '';
1136
+ // First-run hero: empty dashboard = guided next step
1137
+ if (d.sessions.length === 0) {
1138
+ html += '<div class="card" style="margin-bottom:14px;padding:24px 28px;background:var(--bg-subtle);border-left:3px solid var(--accent);">' +
1139
+ '<div style="font-family:var(--font-ui);font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:var(--accent);font-weight:700;margin-bottom:8px;">First run &rarr; magical moment in 10 seconds</div>' +
1140
+ '<div style="font-family:var(--font-display,Lora,Georgia,serif);font-size:22px;font-weight:700;color:var(--ink);margin-bottom:8px;">Seed sample data + prove semantic recall works</div>' +
1141
+ '<div style="font-size:13px;color:var(--ink-muted);margin-bottom:12px;line-height:1.5;max-width:640px;">agentmemory is running but hasn&rsquo;t seen any sessions yet. Run the demo command in a second terminal: it seeds 3 realistic coding sessions and proves the hybrid search finds semantically-related memories that keyword search would miss.</div>' +
1142
+ '<pre style="display:inline-block;margin:0;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-family:var(--font-mono);font-size:12px;color:var(--ink);">npx @agentmemory/agentmemory demo</pre>' +
1143
+ '<div style="margin-top:10px;"><a class="empty-link" href="https://github.com/rohitg00/agentmemory#quick-start" target="_blank" rel="noopener" style="font-size:12px;">Or: wire up your real agent &rarr;</a></div>' +
1144
+ '</div>';
1145
+ }
1146
+ html += '<div class="stats-grid">';
1044
1147
  html += '<div class="stat-card"><div class="label">Sessions</div><div class="value">' + d.sessions.length + '</div><div class="sub">' + activeSessions + ' active</div></div>';
1045
1148
  html += '<div class="stat-card"><div class="label">Memories</div><div class="value">' + d.memories.length + '</div><div class="sub">latest versions</div></div>';
1046
1149
  var lessonCount = (d.lessons || []).length;
@@ -1078,7 +1181,8 @@
1078
1181
  var heapTotal = Math.round((snap.memory.heapTotal || 0) / 1024 / 1024);
1079
1182
  var rss = Math.round((snap.memory.rss || 0) / 1024 / 1024);
1080
1183
  var heapPct = heapTotal > 0 ? Math.round((heapUsed / heapTotal) * 100) : 0;
1081
- var heapColor = heapPct > 80 ? 'var(--red)' : heapPct > 60 ? 'var(--yellow)' : 'var(--green)';
1184
+ var rssAboveFloor = rss >= 512;
1185
+ var heapColor = (heapPct > 80 && rssAboveFloor) ? 'var(--red)' : (heapPct > 60 && rssAboveFloor) ? 'var(--yellow)' : 'var(--green)';
1082
1186
  html += '<div class="gauge"><span class="gauge-label">Heap</span><div class="gauge-bar"><div class="gauge-fill" style="width:' + heapPct + '%;background:' + heapColor + '"></div></div><span class="gauge-value">' + heapUsed + ' / ' + heapTotal + ' MB</span></div>';
1083
1187
  html += '<div class="gauge"><span class="gauge-label">RSS</span><div class="gauge-bar"><div class="gauge-fill" style="width:' + Math.min(100, Math.round(rss / 512 * 100)) + '%;background:var(--blue)"></div></div><span class="gauge-value">' + rss + ' MB</span></div>';
1084
1188
  if (snap.memory.external) {
@@ -1113,6 +1217,14 @@
1113
1217
  html += '</div>';
1114
1218
  }
1115
1219
 
1220
+ if (snap.notes && snap.notes.length > 0) {
1221
+ html += '<div class="card" style="margin-bottom:16px;"><div class="card-title" style="color:var(--ink-muted);">Notes (' + snap.notes.length + ')</div>';
1222
+ snap.notes.forEach(function(n) {
1223
+ html += '<div style="font-size:12px;color:var(--ink-muted);padding:4px 0;border-bottom:1px solid var(--border-light);font-family:var(--font-ui);">' + esc(n) + '</div>';
1224
+ });
1225
+ html += '</div>';
1226
+ }
1227
+
1116
1228
  html += '<div class="two-col">';
1117
1229
 
1118
1230
  html += '<div class="card"><div class="card-title">Recent Sessions</div>';
@@ -1272,6 +1384,7 @@
1272
1384
  function startDashboardAutoRefresh() {
1273
1385
  if (dashboardTimer) clearInterval(dashboardTimer);
1274
1386
  dashboardTimer = setInterval(function() {
1387
+ if (pollTimer) return;
1275
1388
  if (state.activeTab === 'dashboard') refreshDashboard();
1276
1389
  }, 30000);
1277
1390
  }
@@ -1326,8 +1439,8 @@
1326
1439
  var sb = document.getElementById('graph-sidebar');
1327
1440
  if (!sb) return;
1328
1441
  var gs = state.graph.stats || {};
1329
- var nodeCount = gs.nodes !== undefined ? gs.nodes : (gs.nodeCount || state.graph.nodes.length);
1330
- var edgeCount = gs.edges !== undefined ? gs.edges : (gs.edgeCount || state.graph.edges.length);
1442
+ var nodeCount = gs.totalNodes !== undefined ? gs.totalNodes : (gs.nodes !== undefined ? gs.nodes : (gs.nodeCount || state.graph.nodes.length));
1443
+ var edgeCount = gs.totalEdges !== undefined ? gs.totalEdges : (gs.edges !== undefined ? gs.edges : (gs.edgeCount || state.graph.edges.length));
1331
1444
 
1332
1445
  var html = '<input type="text" class="graph-search" id="graph-search" placeholder="Search nodes...">';
1333
1446
 
@@ -1936,7 +2049,13 @@
1936
2049
  items.forEach(function(m) { types[m.type] = true; });
1937
2050
  var typeOptions = Object.keys(types).sort();
1938
2051
 
1939
- var html = '<div class="toolbar">';
2052
+ var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
2053
+ html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
2054
+ html += '<strong>Memories</strong> are durable facts, architecture notes, conventions, and lessons saved via <code>memory_remember</code> MCP tool or the <code>/agentmemory/remember</code> endpoint. They survive across sessions and supersede each other as v1, v2, etc. ';
2055
+ html += '<span style="color:var(--ink-faint);">Shown: ' + items.length + ' total.</span>';
2056
+ html += '</div></div>';
2057
+
2058
+ html += '<div class="toolbar">';
1940
2059
  html += '<input type="text" id="mem-search" placeholder="Search memories..." value="' + esc(state.memories.search) + '">';
1941
2060
  html += '<select id="mem-type-filter"><option value="">All types</option>';
1942
2061
  typeOptions.forEach(function(t) {
@@ -1945,12 +2064,20 @@
1945
2064
  html += '</select></div>';
1946
2065
 
1947
2066
  if (filtered.length === 0) {
1948
- html += '<div class="empty-state"><div class="empty-icon">&#128218;</div><p>No memories found</p></div>';
2067
+ html += '<div class="empty-state">' +
2068
+ '<div class="empty-icon">&#128218;</div>' +
2069
+ '<div class="empty-title">No memories yet</div>' +
2070
+ '<div class="empty-lead">Memories are the distilled facts agentmemory keeps across sessions &mdash; things like file paths, architectural decisions, and user preferences. Hooks capture them automatically during coding sessions; you can also save one directly.</div>' +
2071
+ '<pre class="empty-cmd">memory_remember {\n title: "auth uses jose middleware",\n content: "src/middleware/auth.ts handles JWT validation",\n type: "architecture"\n}</pre>' +
2072
+ '<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#memories" target="_blank" rel="noopener">Memory types &rarr;</a></div>' +
2073
+ '</div>';
1949
2074
  } else {
1950
2075
  html += '<table><tr><th>Title</th><th>Type</th><th>Strength</th><th>Version</th><th>Updated</th><th>Actions</th></tr>';
1951
2076
  filtered.forEach(function(m) {
1952
2077
  var badgeClass = TYPE_BADGES[m.type] || 'badge-muted';
1953
- var strength = Math.round((m.strength || 0) * 100);
2078
+ var rawStrength = m.strength || 0;
2079
+ var strength = Math.round(rawStrength <= 1 ? rawStrength * 100 : rawStrength * 10);
2080
+ if (strength > 100) strength = 100;
1954
2081
  var barColor = strength > 70 ? 'var(--green)' : strength > 40 ? 'var(--yellow)' : 'var(--red)';
1955
2082
  html += '<tr>';
1956
2083
  var preview = (m.content || '').split('\n').slice(0, 2).join(' ').trim();
@@ -2399,6 +2526,10 @@
2399
2526
  html += '<div class="session-item' + (selected ? ' selected' : '') + '" data-action="select-session" data-session-id="' + esc(s.id) + '">';
2400
2527
  html += '<div class="session-top"><span class="session-project">' + esc(s.project ? s.project.split('/').pop() : 'Unknown') + '</span>';
2401
2528
  html += '<span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></div>';
2529
+ var preview = s.firstPrompt || s.summary || '';
2530
+ if (preview) {
2531
+ html += '<div class="session-preview" style="font-size:13px;color:var(--ink);margin:4px 0;line-height:1.4;">' + esc(truncate(preview, 140)) + '</div>';
2532
+ }
2402
2533
  html += '<div class="session-meta">' + esc(s.id.slice(0, 12)) + ' &middot; ' + esc(formatTime(s.startedAt));
2403
2534
  html += ' &middot; ' + (s.observationCount || 0) + ' obs';
2404
2535
  if (s.model) html += ' &middot; ' + esc(s.model);
@@ -2417,24 +2548,96 @@
2417
2548
  renderSessions();
2418
2549
  }
2419
2550
 
2420
- function renderSessionDetail() {
2551
+ async function renderSessionDetail() {
2421
2552
  var panel = document.getElementById('session-detail');
2422
2553
  if (!panel) return;
2423
2554
  var s = state.sessions.items.find(function(x) { return x.id === state.sessions.selectedId; });
2424
2555
  if (!s) { panel.innerHTML = ''; return; }
2425
2556
 
2426
- var html = '<div class="detail-panel"><h3>Session Details</h3>';
2427
- html += '<div class="detail-row"><div class="dl">Session ID</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(s.id) + '</div></div>';
2428
- html += '<div class="detail-row"><div class="dl">Project</div><div class="dv">' + esc(s.project) + '</div></div>';
2429
- html += '<div class="detail-row"><div class="dl">Working Dir</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(s.cwd) + '</div></div>';
2430
- html += '<div class="detail-row"><div class="dl">Status</div><div class="dv">' + esc(s.status) + '</div></div>';
2431
- html += '<div class="detail-row"><div class="dl">Started</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(formatTime(s.startedAt)) + '</div></div>';
2432
- if (s.endedAt) html += '<div class="detail-row"><div class="dl">Ended</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(formatTime(s.endedAt)) + '</div></div>';
2433
- html += '<div class="detail-row"><div class="dl">Observations</div><div class="dv" style="font-family:var(--font-mono);">' + (s.observationCount || 0) + '</div></div>';
2434
- if (s.model) html += '<div class="detail-row"><div class="dl">Model</div><div class="dv">' + esc(s.model) + '</div></div>';
2435
- if (s.tags && s.tags.length) html += '<div class="detail-row"><div class="dl">Tags</div><div class="dv">' + s.tags.map(function(t) { return '<span class="badge badge-muted" style="margin-right:4px;">' + esc(t) + '</span>'; }).join('') + '</div></div>';
2436
-
2437
- html += '<div style="margin-top:16px;display:flex;gap:8px;">';
2557
+ panel.innerHTML = '<div class="detail-panel"><h3>Loading session details…</h3></div>';
2558
+
2559
+ var obsRes = await apiGet('observations?sessionId=' + encodeURIComponent(s.id));
2560
+ var obs = (obsRes && obsRes.observations) || [];
2561
+
2562
+ var typeCounts = {};
2563
+ var toolCounts = {};
2564
+ var fileSet = new Set();
2565
+ var firstPromptFromObs = '';
2566
+ obs.forEach(function(o) {
2567
+ var t = o.type || o.hookType || 'other';
2568
+ typeCounts[t] = (typeCounts[t] || 0) + 1;
2569
+ var tool = o.title || o.toolName;
2570
+ if (tool && t !== 'conversation') toolCounts[tool] = (toolCounts[tool] || 0) + 1;
2571
+ (o.files || []).forEach(function(f) { fileSet.add(f); });
2572
+ if (!firstPromptFromObs && (o.userPrompt || (o.type === 'conversation' && o.narrative))) {
2573
+ firstPromptFromObs = o.userPrompt || o.narrative || '';
2574
+ }
2575
+ });
2576
+
2577
+ var durationMs = s.endedAt ? new Date(s.endedAt).getTime() - new Date(s.startedAt).getTime() : 0;
2578
+ var durationLabel = durationMs > 0 ? (durationMs < 60000 ? (durationMs / 1000).toFixed(1) + 's' : (durationMs / 60000).toFixed(1) + 'm') : '-';
2579
+
2580
+ var preview = s.firstPrompt || s.summary || firstPromptFromObs || '';
2581
+
2582
+ var html = '<div class="detail-panel">';
2583
+ html += '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;">';
2584
+ html += '<h3 style="margin:0;">Session · ' + esc(s.project || 'Unknown') + '</h3>';
2585
+ html += '<span class="badge ' + (s.status === 'active' ? 'badge-green' : 'badge-blue') + '">' + esc(s.status) + '</span>';
2586
+ html += '</div>';
2587
+
2588
+ if (preview) {
2589
+ html += '<div style="padding:10px 12px;margin-bottom:12px;background:var(--bg-alt);border-left:3px solid var(--accent);font-size:13px;line-height:1.5;color:var(--ink);">' + esc(truncate(preview, 600)) + '</div>';
2590
+ }
2591
+
2592
+ html += '<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:10px;margin-bottom:14px;">';
2593
+ html += '<div class="card" style="padding:10px;"><div style="font-size:10px;letter-spacing:0.08em;color:var(--ink-muted);">OBSERVATIONS</div><div style="font-size:20px;font-weight:600;">' + obs.length + '</div></div>';
2594
+ html += '<div class="card" style="padding:10px;"><div style="font-size:10px;letter-spacing:0.08em;color:var(--ink-muted);">TOOLS USED</div><div style="font-size:20px;font-weight:600;">' + Object.keys(toolCounts).length + '</div></div>';
2595
+ html += '<div class="card" style="padding:10px;"><div style="font-size:10px;letter-spacing:0.08em;color:var(--ink-muted);">FILES TOUCHED</div><div style="font-size:20px;font-weight:600;">' + fileSet.size + '</div></div>';
2596
+ html += '<div class="card" style="padding:10px;"><div style="font-size:10px;letter-spacing:0.08em;color:var(--ink-muted);">DURATION</div><div style="font-size:20px;font-weight:600;">' + esc(durationLabel) + '</div></div>';
2597
+ html += '</div>';
2598
+
2599
+ var topTools = Object.keys(toolCounts).sort(function(a, b) { return toolCounts[b] - toolCounts[a]; }).slice(0, 10);
2600
+ if (topTools.length > 0) {
2601
+ var maxC = toolCounts[topTools[0]] || 1;
2602
+ html += '<div class="card" style="margin-bottom:12px;"><div class="card-title">Tool Invocations</div>';
2603
+ html += '<div class="bar-chart" style="margin-top:8px;">';
2604
+ topTools.forEach(function(t) {
2605
+ var pct = Math.round((toolCounts[t] / maxC) * 100);
2606
+ html += '<div class="bar-row"><span class="bar-label" style="font-family:var(--font-mono);">' + esc(t) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--accent);"></div></div><span class="bar-value">' + toolCounts[t] + '</span></div>';
2607
+ });
2608
+ html += '</div></div>';
2609
+ }
2610
+
2611
+ var typeKeys = Object.keys(typeCounts).sort(function(a, b) { return typeCounts[b] - typeCounts[a]; });
2612
+ if (typeKeys.length > 0) {
2613
+ html += '<div class="card" style="margin-bottom:12px;"><div class="card-title">Activity Breakdown</div>';
2614
+ html += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px;">';
2615
+ typeKeys.forEach(function(t) {
2616
+ html += '<span class="badge badge-muted" style="font-family:var(--font-mono);">' + esc(t.replace(/_/g, ' ')) + ' · ' + typeCounts[t] + '</span>';
2617
+ });
2618
+ html += '</div></div>';
2619
+ }
2620
+
2621
+ if (fileSet.size > 0) {
2622
+ var filesArr = Array.from(fileSet).slice(0, 30);
2623
+ html += '<div class="card" style="margin-bottom:12px;"><div class="card-title">Files</div>';
2624
+ html += '<div style="font-size:12px;font-family:var(--font-mono);line-height:1.6;margin-top:8px;">';
2625
+ filesArr.forEach(function(f) { html += '<div>&#8226; ' + esc(f) + '</div>'; });
2626
+ if (fileSet.size > 30) html += '<div style="color:var(--ink-faint);">+' + (fileSet.size - 30) + ' more</div>';
2627
+ html += '</div></div>';
2628
+ }
2629
+
2630
+ html += '<div class="card" style="margin-bottom:12px;"><div class="card-title">Metadata</div>';
2631
+ html += '<div style="font-size:12px;font-family:var(--font-mono);margin-top:8px;line-height:1.7;">';
2632
+ html += '<div><span style="color:var(--ink-muted);">id:</span> ' + esc(s.id) + '</div>';
2633
+ html += '<div><span style="color:var(--ink-muted);">cwd:</span> ' + esc(s.cwd || '-') + '</div>';
2634
+ html += '<div><span style="color:var(--ink-muted);">started:</span> ' + esc(formatTime(s.startedAt)) + '</div>';
2635
+ if (s.endedAt) html += '<div><span style="color:var(--ink-muted);">ended:</span> ' + esc(formatTime(s.endedAt)) + '</div>';
2636
+ if (s.model) html += '<div><span style="color:var(--ink-muted);">model:</span> ' + esc(s.model) + '</div>';
2637
+ if (s.tags && s.tags.length) html += '<div><span style="color:var(--ink-muted);">tags:</span> ' + s.tags.map(esc).join(', ') + '</div>';
2638
+ html += '</div></div>';
2639
+
2640
+ html += '<div style="display:flex;gap:8px;">';
2438
2641
  if (s.status === 'active') {
2439
2642
  html += '<button class="btn btn-danger" data-action="end-session" data-session-id="' + esc(s.id) + '">End Session</button>';
2440
2643
  }
@@ -2478,13 +2681,24 @@
2478
2681
  });
2479
2682
  }
2480
2683
 
2481
- var html = '<div style="display:flex;gap:8px;margin-bottom:12px;">';
2684
+ var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
2685
+ html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
2686
+ html += '<strong>Lessons</strong> are portable heuristics — short imperative rules (always/never/prefer/avoid) extracted from past work. Auto-surface from JSONL imports (low confidence, tag <code>auto-import</code>), get reinforced when the agent applies them, and decay if unused. Higher confidence = more battle-tested.';
2687
+ html += '</div></div>';
2688
+
2689
+ html += '<div style="display:flex;gap:8px;margin-bottom:12px;">';
2482
2690
  html += '<input class="search-input" type="text" placeholder="Search lessons..." value="' + esc(state.lessons.search) + '" oninput="state.lessons.search=this.value;renderLessons()" style="flex:1" />';
2483
2691
  html += '<span style="font-size:12px;color:var(--ink-faint);align-self:center;">' + items.length + ' lessons</span>';
2484
2692
  html += '</div>';
2485
2693
 
2486
2694
  if (items.length === 0) {
2487
- html += '<div class="empty-state"><div class="empty-icon">&#128161;</div><p>No lessons yet</p><p style="font-size:12px;color:var(--ink-faint);font-style:italic;">Lessons are extracted from crystals or saved manually via memory_lesson_save.</p></div>';
2695
+ html += '<div class="empty-state">' +
2696
+ '<div class="empty-icon">&#128161;</div>' +
2697
+ '<div class="empty-title">No lessons yet</div>' +
2698
+ '<div class="empty-lead">Lessons are confidence-scored pattern observations &mdash; things you corrected once that the agent should never do again. They persist across projects.</div>' +
2699
+ '<pre class="empty-cmd"># Save a lesson explicitly\nmemory_lesson_save { rule, reason, confidence }\n\n# Or: Replay tab &rarr; Import JSONL auto-extracts lessons\n# from your past Claude Code sessions</pre>' +
2700
+ '<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#lessons" target="_blank" rel="noopener">Lesson decay &amp; scoring &rarr;</a></div>' +
2701
+ '</div>';
2488
2702
  } else {
2489
2703
  html += '<table><thead><tr><th>Lesson</th><th>Confidence</th><th>Reinforcements</th><th>Source</th><th>Project</th><th>Updated</th></tr></thead><tbody>';
2490
2704
  items.forEach(function(l) {
@@ -2510,7 +2724,7 @@
2510
2724
  el.innerHTML = '<div class="loading">Loading actions...</div>';
2511
2725
  var results = await Promise.all([apiGet('actions'), apiGet('frontier')]);
2512
2726
  state.actions.items = (results[0] && results[0].actions) || [];
2513
- state.actions.frontier = (results[1] && results[1].actions) || [];
2727
+ state.actions.frontier = (results[1] && (results[1].frontier || results[1].actions)) || [];
2514
2728
  state.actions.loaded = true;
2515
2729
  renderActions();
2516
2730
  }
@@ -2543,7 +2757,14 @@
2543
2757
  html += '</div>';
2544
2758
 
2545
2759
  if (items.length === 0) {
2546
- html += '<div class="empty-state"><div class="empty-icon">&#9745;</div><p>No actions yet</p><p style="font-size:12px;color:var(--ink-faint);font-style:italic;">Create actions via memory_action_create MCP tool or POST /agentmemory/actions</p></div>';
2760
+ html += '<div class="empty-state">' +
2761
+ '<div class="empty-icon">&#9745;</div>' +
2762
+ '<div class="empty-title">No actions tracked yet</div>' +
2763
+ '<div class="empty-lead">Actions are follow-ups the agent surfaced during a session: <em>decisions to revisit</em>, <em>files to inspect</em>, <em>tasks blocked on input</em>. They show up here with status pending &rarr; active &rarr; done/blocked so nothing slips through between sessions.</div>' +
2764
+ '<div class="empty-lead" style="margin-top:0;">Three ways to create them:</div>' +
2765
+ '<pre class="empty-cmd"># 1. MCP tool (from any agent)\nmemory_action_create { title, description, priority }\n\n# 2. Curl\ncurl -X POST http://localhost:3111/agentmemory/actions \\\n -H \'Content-Type: application/json\' \\\n -d \'{"title":"ship v1","priority":"high"}\'\n\n# 3. Hooks auto-extract from long session bodies</pre>' +
2766
+ '<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#actions" target="_blank" rel="noopener">Action lifecycle docs &rarr;</a></div>' +
2767
+ '</div>';
2547
2768
  } else {
2548
2769
  html += '<table><thead><tr><th>Title</th><th>Status</th><th>Priority</th><th>Tags</th><th>Frontier</th><th>Updated</th></tr></thead><tbody>';
2549
2770
  items = items.slice().sort(function(a, b) { return (b.priority || 0) - (a.priority || 0); });
@@ -2569,15 +2790,13 @@
2569
2790
 
2570
2791
  async function loadCrystals() {
2571
2792
  var el = document.getElementById('view-crystals');
2572
- if (state.dashboard.loaded && state.dashboard.crystals.length) {
2573
- state.crystals.items = state.dashboard.crystals;
2574
- state.crystals.loaded = true;
2575
- renderCrystals();
2576
- return;
2577
- }
2578
2793
  el.innerHTML = '<div class="loading">Loading crystals...</div>';
2579
- var result = await apiGet('crystals');
2580
- state.crystals.items = (result && result.crystals) || [];
2794
+ var results = await Promise.all([apiGet('crystals'), apiGet('lessons')]);
2795
+ state.crystals.items = (results[0] && results[0].crystals) || [];
2796
+ var lessonMap = {};
2797
+ var lessons = (results[1] && results[1].lessons) || [];
2798
+ lessons.forEach(function(l) { if (l && l.id) lessonMap[l.id] = l; });
2799
+ state.crystals.lessonMap = lessonMap;
2581
2800
  state.crystals.loaded = true;
2582
2801
  renderCrystals();
2583
2802
  }
@@ -2586,54 +2805,89 @@
2586
2805
  var el = document.getElementById('view-crystals');
2587
2806
  var items = state.crystals.items;
2588
2807
  var search = state.crystals.search.toLowerCase();
2808
+ var lessonMap = state.crystals.lessonMap || {};
2589
2809
 
2590
2810
  if (search) {
2591
2811
  items = items.filter(function(c) {
2592
- return ((c.narrative || '') + ' ' + (c.keyOutcomes || []).join(' ') + ' ' + (c.lessons || []).join(' ')).toLowerCase().indexOf(search) >= 0;
2812
+ var lessonText = (c.lessons || [])
2813
+ .map(function(lid) {
2814
+ var l = lessonMap[lid];
2815
+ return l && typeof l.content === 'string' ? l.content : lid;
2816
+ })
2817
+ .join(' ');
2818
+ var filesText = (c.filesAffected || []).join(' ');
2819
+ var haystack = [
2820
+ c.narrative || '',
2821
+ (c.keyOutcomes || []).join(' '),
2822
+ lessonText,
2823
+ filesText,
2824
+ c.project || '',
2825
+ ].join(' ').toLowerCase();
2826
+ return haystack.indexOf(search) >= 0;
2593
2827
  });
2594
2828
  }
2595
2829
 
2596
- var html = '<div style="display:flex;gap:8px;margin-bottom:12px;">';
2830
+ var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
2831
+ html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
2832
+ html += '<strong>Crystals</strong> are frozen snapshots of completed work. Each crystal captures one session\'s narrative, the tools invoked (key outcomes), files touched, and lessons surfaced — a replayable summary you keep after raw observations are pruned. Auto-created on JSONL import or via <code>memory_crystallize</code>.';
2833
+ html += '</div></div>';
2834
+
2835
+ html += '<div style="display:flex;gap:8px;margin-bottom:12px;">';
2597
2836
  html += '<input class="search-input" type="text" placeholder="Search crystals..." value="' + esc(state.crystals.search) + '" oninput="state.crystals.search=this.value;renderCrystals()" style="flex:1" />';
2598
2837
  html += '<span style="font-size:12px;color:var(--ink-faint);align-self:center;">' + items.length + ' crystals</span>';
2599
2838
  html += '</div>';
2600
2839
 
2601
2840
  if (items.length === 0) {
2602
- html += '<div class="empty-state"><div class="empty-icon">&#128142;</div><p>No crystals yet</p><p style="font-size:12px;color:var(--ink-faint);font-style:italic;">Crystals are created by compressing completed action chains via memory_crystallize.</p></div>';
2841
+ html += '<div class="empty-state">' +
2842
+ '<div class="empty-icon">&#128142;</div>' +
2843
+ '<div class="empty-title">No crystals yet</div>' +
2844
+ '<div class="empty-lead">Crystals are compressed action digests &mdash; the 3-line summary of what happened in a session. Generated from long conversations to give the next session fast context without re-reading everything.</div>' +
2845
+ '<pre class="empty-cmd"># Auto: import a JSONL transcript\n# Replay tab &rarr; Import JSONL\n\n# Manual: crystallize a specific session\nmemory_crystallize { sessionId }</pre>' +
2846
+ '<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#crystals" target="_blank" rel="noopener">Crystal pipeline &rarr;</a></div>' +
2847
+ '</div>';
2603
2848
  } else {
2604
2849
  items.forEach(function(c) {
2605
- html += '<div class="card" style="margin-bottom:12px;">';
2606
- html += '<div class="card-title" style="display:flex;justify-content:space-between;">';
2607
- html += '<span>' + esc(truncate(c.narrative, 100)) + '</span>';
2608
- html += '<span style="font-size:11px;color:var(--ink-faint);">' + formatTime(c.createdAt) + '</span>';
2850
+ html += '<div class="card" style="margin-bottom:12px;border-left:3px solid var(--accent);">';
2851
+ html += '<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:8px;">';
2852
+ html += '<div style="flex:1;font-size:14px;font-weight:600;color:var(--ink);line-height:1.4;">' + esc(truncate(c.narrative || 'Untitled crystal', 300)) + '</div>';
2853
+ html += '<div style="font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);white-space:nowrap;">' + esc(formatTime(c.createdAt)) + '</div>';
2609
2854
  html += '</div>';
2610
2855
 
2856
+ var pillRow = [];
2857
+ if (c.project) pillRow.push('<span class="badge badge-muted">' + esc(c.project) + '</span>');
2858
+ if (c.sessionId) pillRow.push('<span class="badge badge-blue" style="font-family:var(--font-mono);">' + esc(c.sessionId.slice(0, 14)) + '</span>');
2859
+ if (c.keyOutcomes && c.keyOutcomes.length) pillRow.push('<span style="font-size:11px;color:var(--ink-muted);">' + c.keyOutcomes.length + ' tools</span>');
2860
+ if (c.filesAffected && c.filesAffected.length) pillRow.push('<span style="font-size:11px;color:var(--ink-muted);">' + c.filesAffected.length + ' files</span>');
2861
+ if (c.lessons && c.lessons.length) pillRow.push('<span style="font-size:11px;color:var(--ink-muted);">' + c.lessons.length + ' lessons</span>');
2862
+ if (pillRow.length) html += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;">' + pillRow.join('') + '</div>';
2863
+
2611
2864
  if (c.keyOutcomes && c.keyOutcomes.length > 0) {
2612
- html += '<div style="margin:8px 0;"><strong style="font-size:11px;color:var(--ink-muted);">KEY OUTCOMES</strong>';
2865
+ html += '<div style="margin:10px 0;"><div style="font-size:10px;letter-spacing:0.08em;color:var(--ink-muted);margin-bottom:4px;">TOOLS USED</div>';
2866
+ html += '<div style="display:flex;gap:4px;flex-wrap:wrap;">';
2613
2867
  c.keyOutcomes.forEach(function(o) {
2614
- html += '<div style="font-size:12px;padding:2px 0;color:var(--ink);">&#8226; ' + esc(o) + '</div>';
2868
+ html += '<span class="badge" style="background:var(--bg-alt);color:var(--ink);font-family:var(--font-mono);">' + esc(o) + '</span>';
2615
2869
  });
2616
- html += '</div>';
2617
- }
2618
-
2619
- if (c.lessons && c.lessons.length > 0) {
2620
- html += '<div style="margin:8px 0;"><strong style="font-size:11px;color:var(--ink-muted);">LESSONS</strong>';
2621
- c.lessons.forEach(function(l) {
2622
- html += '<div style="font-size:12px;padding:2px 0;color:var(--ink);">&#128161; ' + esc(l) + '</div>';
2623
- });
2624
- html += '</div>';
2870
+ html += '</div></div>';
2625
2871
  }
2626
2872
 
2627
2873
  if (c.filesAffected && c.filesAffected.length > 0) {
2628
- html += '<div style="margin:8px 0;font-size:11px;color:var(--ink-muted);">Files: <span style="font-family:var(--font-mono);">' + c.filesAffected.map(esc).join(', ') + '</span></div>';
2629
- }
2630
-
2631
- if (c.sourceActionIds && c.sourceActionIds.length > 0) {
2632
- html += '<div style="font-size:11px;color:var(--ink-faint);">Source actions: ' + c.sourceActionIds.map(esc).join(', ') + '</div>';
2874
+ html += '<div style="margin:10px 0;"><div style="font-size:10px;letter-spacing:0.08em;color:var(--ink-muted);margin-bottom:4px;">FILES TOUCHED</div>';
2875
+ html += '<div style="font-size:12px;font-family:var(--font-mono);color:var(--ink);line-height:1.6;">';
2876
+ c.filesAffected.slice(0, 10).forEach(function(f) {
2877
+ html += '<div>&#8226; ' + esc(f) + '</div>';
2878
+ });
2879
+ if (c.filesAffected.length > 10) html += '<div style="color:var(--ink-faint);">+' + (c.filesAffected.length - 10) + ' more</div>';
2880
+ html += '</div></div>';
2633
2881
  }
2634
2882
 
2635
- if (c.project) {
2636
- html += '<div style="font-size:11px;color:var(--ink-faint);margin-top:4px;">Project: ' + esc(c.project) + '</div>';
2883
+ if (c.lessons && c.lessons.length > 0) {
2884
+ html += '<div style="margin:10px 0;"><div style="font-size:10px;letter-spacing:0.08em;color:var(--ink-muted);margin-bottom:4px;">LESSONS SURFACED</div>';
2885
+ c.lessons.slice(0, 8).forEach(function(lid) {
2886
+ var content = lessonMap[lid] ? lessonMap[lid].content : lid;
2887
+ html += '<div style="font-size:12px;padding:4px 8px;margin:2px 0;background:var(--bg-alt);border-radius:3px;color:var(--ink);line-height:1.4;">&#128161; ' + esc(content) + '</div>';
2888
+ });
2889
+ if (c.lessons.length > 8) html += '<div style="font-size:11px;color:var(--ink-faint);margin-top:4px;">+' + (c.lessons.length - 8) + ' more lessons</div>';
2890
+ html += '</div>';
2637
2891
  }
2638
2892
 
2639
2893
  html += '</div>';
@@ -2825,13 +3079,66 @@
2825
3079
 
2826
3080
  var wsReconnectTimer = null;
2827
3081
  var wsRetries = 0;
2828
- var WS_MAX_RETRIES = 10;
3082
+ var WS_MAX_RETRIES = 4;
2829
3083
  var directFailed = false;
2830
3084
  var directFailures = 0;
2831
3085
  var DIRECT_FAILURE_THRESHOLD = 2;
3086
+ var pollTimer = null;
3087
+ var POLL_INTERVAL_MS = 10000;
3088
+
3089
+ function setWsStatus(text, cls) {
3090
+ var el = document.getElementById('ws-status');
3091
+ if (!el) return;
3092
+ el.textContent = text;
3093
+ el.className = 'ws-status ' + cls;
3094
+ }
3095
+
3096
+ var WS_REPROBE_EVERY_TICKS = 6;
3097
+
3098
+ function startPolling() {
3099
+ if (pollTimer) return;
3100
+ setWsStatus('polling · ' + (POLL_INTERVAL_MS / 1000) + 's', 'disconnected');
3101
+ var tick = 0;
3102
+ pollTimer = setInterval(function() {
3103
+ tick++;
3104
+ if (state.activeTab === 'dashboard') {
3105
+ state.dashboard.loaded = false;
3106
+ loadDashboard();
3107
+ } else if (state.activeTab === 'memories') {
3108
+ state.memories.loaded = false;
3109
+ loadMemories();
3110
+ } else if (state.activeTab === 'sessions') {
3111
+ state.sessions.loaded = false;
3112
+ loadSessions();
3113
+ } else if (state.activeTab === 'activity') {
3114
+ state.activity.loaded = false;
3115
+ loadActivity();
3116
+ }
3117
+ if (tick % WS_REPROBE_EVERY_TICKS === 0) {
3118
+ var ws = state.ws;
3119
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
3120
+ wsRetries = 0;
3121
+ directFailures = 0;
3122
+ directFailed = false;
3123
+ connectWs();
3124
+ }
3125
+ }
3126
+ }, POLL_INTERVAL_MS);
3127
+ }
3128
+
3129
+ function stopPolling() {
3130
+ if (!pollTimer) return;
3131
+ clearInterval(pollTimer);
3132
+ pollTimer = null;
3133
+ }
3134
+
3135
+ var WS_CONNECT_TIMEOUT_MS = 5000;
2832
3136
 
2833
3137
  function connectWs() {
2834
- if (wsRetries >= WS_MAX_RETRIES) return;
3138
+ if (wsRetries >= WS_MAX_RETRIES) {
3139
+ startPolling();
3140
+ return;
3141
+ }
2835
3142
  var useDirect = !directFailed;
2836
3143
  var ws;
2837
3144
  try {
@@ -2841,10 +3148,17 @@
2841
3148
  ws = new WebSocket(WS_URL);
2842
3149
  ws.__direct = false;
2843
3150
  }
3151
+ var connectTimer = setTimeout(function() {
3152
+ if (ws.readyState === WebSocket.CONNECTING) {
3153
+ try { ws.close(); } catch {}
3154
+ }
3155
+ }, WS_CONNECT_TIMEOUT_MS);
2844
3156
  try {
2845
3157
  ws.onopen = function() {
3158
+ clearTimeout(connectTimer);
2846
3159
  if (state.ws !== ws) return;
2847
3160
  wsRetries = 0;
3161
+ stopPolling();
2848
3162
  if (ws.__direct) {
2849
3163
  directFailures = 0;
2850
3164
  directFailed = false;
@@ -2859,8 +3173,7 @@
2859
3173
  }
2860
3174
  }));
2861
3175
  }
2862
- document.getElementById('ws-status').textContent = 'live';
2863
- document.getElementById('ws-status').className = 'ws-status connected';
3176
+ setWsStatus('live', 'connected');
2864
3177
  };
2865
3178
  ws.onmessage = function(e) {
2866
3179
  if (state.ws !== ws) return;
@@ -2874,6 +3187,7 @@
2874
3187
  } catch {}
2875
3188
  };
2876
3189
  ws.onclose = function() {
3190
+ clearTimeout(connectTimer);
2877
3191
  if (state.ws !== ws) return;
2878
3192
  if (ws.__direct) {
2879
3193
  directFailures += 1;
@@ -2881,13 +3195,12 @@
2881
3195
  directFailed = true;
2882
3196
  }
2883
3197
  }
2884
- document.getElementById('ws-status').textContent = 'reconnecting...';
2885
- document.getElementById('ws-status').className = 'ws-status disconnected';
2886
3198
  wsRetries++;
2887
3199
  if (wsRetries < WS_MAX_RETRIES) {
3200
+ setWsStatus('connecting...', 'disconnected');
2888
3201
  wsReconnectTimer = setTimeout(connectWs, 2000 + Math.min(wsRetries * 1000, 8000));
2889
3202
  } else {
2890
- document.getElementById('ws-status').textContent = 'disconnected';
3203
+ startPolling();
2891
3204
  }
2892
3205
  };
2893
3206
  ws.onerror = function() {
@@ -2899,6 +3212,8 @@
2899
3212
  wsRetries++;
2900
3213
  if (wsRetries < WS_MAX_RETRIES) {
2901
3214
  wsReconnectTimer = setTimeout(connectWs, 2000 + Math.min(wsRetries * 1000, 8000));
3215
+ } else {
3216
+ startPolling();
2902
3217
  }
2903
3218
  }
2904
3219
  }
@@ -2966,6 +3281,151 @@
2966
3281
  switchTab(e.target.dataset.tab);
2967
3282
  }
2968
3283
  });
3284
+
3285
+ // --- Feature flag banners ---------------------------------------------
3286
+ var FLAG_DISMISS_KEY = 'agentmemory.viewer.flags.dismissed.v1';
3287
+ function loadDismissedFlags() {
3288
+ try {
3289
+ var raw = localStorage.getItem(FLAG_DISMISS_KEY);
3290
+ return raw ? JSON.parse(raw) : {};
3291
+ } catch (_) { return {}; }
3292
+ }
3293
+ function saveDismissedFlags(d) {
3294
+ try { localStorage.setItem(FLAG_DISMISS_KEY, JSON.stringify(d)); } catch (_) {}
3295
+ }
3296
+ function renderFlagBanners(cfg) {
3297
+ var host = document.getElementById('flag-banners');
3298
+ if (!host) return;
3299
+ var dismissed = loadDismissedFlags();
3300
+ var banners = [];
3301
+ // Per-flag banner (only for off flags, affecting current tab or dashboard)
3302
+ (cfg.flags || []).forEach(function(f) {
3303
+ if (f.enabled) return;
3304
+ if (dismissed[f.key]) return;
3305
+ var tabsAffected = (f.affects || []).map(function(t) { return t.toLowerCase(); });
3306
+ if (tabsAffected.length && tabsAffected.indexOf(state.activeTab) === -1 && state.activeTab !== 'dashboard') return;
3307
+ banners.push({
3308
+ kind: 'warn',
3309
+ icon: '&#9888;',
3310
+ title: f.label,
3311
+ keyLabel: f.key,
3312
+ desc: f.description + (f.needsLlm ? ' Requires an LLM provider key (ANTHROPIC_API_KEY, GEMINI_API_KEY, etc.).' : ''),
3313
+ enable: f.enableHow,
3314
+ docs: f.docsHref,
3315
+ dismissKey: f.key,
3316
+ });
3317
+ });
3318
+ if (cfg.provider === 'noop' && !dismissed['__provider_noop']) {
3319
+ banners.unshift({
3320
+ kind: 'warn',
3321
+ icon: '&#128274;',
3322
+ title: 'No LLM provider key set',
3323
+ keyLabel: 'ANTHROPIC_API_KEY',
3324
+ desc: 'Compression, summarization, and graph extraction stay disabled until a key is provided.',
3325
+ enable: 'export ANTHROPIC_API_KEY=sk-ant-...\n# then restart: npx @agentmemory/agentmemory',
3326
+ docs: 'https://github.com/rohitg00/agentmemory#quick-start',
3327
+ dismissKey: '__provider_noop',
3328
+ });
3329
+ }
3330
+ if (cfg.embeddingProvider === 'none' && !dismissed['__embedding_none']) {
3331
+ banners.push({
3332
+ kind: 'info',
3333
+ icon: '&#9881;',
3334
+ title: 'Running in BM25-only mode',
3335
+ keyLabel: 'OPENAI_API_KEY',
3336
+ desc: 'Semantic vector search is off. BM25 keyword search is active and good for exact matches.',
3337
+ enable: 'export OPENAI_API_KEY=sk-...\n# or VOYAGE_API_KEY, COHERE_API_KEY, OLLAMA_HOST',
3338
+ docs: 'https://github.com/rohitg00/agentmemory#embedding-providers',
3339
+ dismissKey: '__embedding_none',
3340
+ });
3341
+ }
3342
+ if (banners.length === 0) { host.innerHTML = ''; return; }
3343
+ var warnCount = banners.filter(function(b) { return b.kind === 'warn'; }).length;
3344
+ var infoCount = banners.filter(function(b) { return b.kind === 'info'; }).length;
3345
+ var expanded = host.getAttribute('data-expanded') === '1';
3346
+ var pills = '';
3347
+ if (warnCount) pills += '<span class="flag-pill">' + warnCount + ' off</span>';
3348
+ if (infoCount) pills += '<span class="flag-pill info">' + infoCount + ' note</span>';
3349
+ var escHtml = function(s) {
3350
+ return String(s).replace(/[<>&"]/g, function(c) {
3351
+ return { '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c];
3352
+ });
3353
+ };
3354
+ var listHtml = banners.map(function(b) {
3355
+ return '<div class="flag-banner ' + b.kind + '" data-flag="' + b.dismissKey + '">' +
3356
+ '<span class="flag-icon">' + b.icon + '</span>' +
3357
+ '<div class="flag-body">' +
3358
+ '<div class="flag-title">' + b.title + ' <code>' + b.keyLabel + '</code></div>' +
3359
+ '<div class="flag-desc">' + escHtml(b.desc) + '</div>' +
3360
+ '<code class="flag-enable">' + escHtml(b.enable) + '</code>' +
3361
+ (b.docs ? ' <a class="empty-link" href="' + b.docs + '" target="_blank" rel="noopener">Learn more &rarr;</a>' : '') +
3362
+ '</div>' +
3363
+ '<button class="flag-close" data-dismiss-flag="' + b.dismissKey + '" aria-label="Dismiss">&times;</button>' +
3364
+ '</div>';
3365
+ }).join('');
3366
+ host.innerHTML = '<button type="button" class="flag-summary" data-action="toggle-flags" aria-expanded="' + (expanded ? 'true' : 'false') + '" aria-controls="flag-list">' +
3367
+ pills +
3368
+ '<span class="flag-count">Feature flags</span>' +
3369
+ '<span style="color:var(--ink-faint);">— click to ' + (expanded ? 'collapse' : 'expand') + '</span>' +
3370
+ '<span class="flag-toggle" aria-hidden="true">' + (expanded ? '&#9650;' : '&#9660;') + '</span>' +
3371
+ '</button>' +
3372
+ '<div class="flag-list' + (expanded ? ' open' : '') + '" id="flag-list">' + listHtml + '</div>';
3373
+ }
3374
+ async function fetchFlags() {
3375
+ var res = await apiGet('config/flags');
3376
+ if (!res) return;
3377
+ state.flagsConfig = res;
3378
+ renderFlagBanners(res);
3379
+ updateFooter(res);
3380
+ }
3381
+ function updateFooter(cfg) {
3382
+ var vEl = document.getElementById('footer-version');
3383
+ if (vEl) vEl.textContent = 'v' + (cfg.version || '?');
3384
+ var fbEl = document.getElementById('footer-feedback');
3385
+ if (fbEl) {
3386
+ var flagSummary = (cfg.flags || []).map(function(f) { return f.key + '=' + (f.enabled ? 'on' : 'off'); }).join(', ');
3387
+ var body = encodeURIComponent(
3388
+ '**Version:** ' + (cfg.version || '?') + '\n' +
3389
+ '**Provider:** ' + (cfg.provider || '?') + '\n' +
3390
+ '**Embedding:** ' + (cfg.embeddingProvider || '?') + '\n' +
3391
+ '**Flags:** ' + flagSummary + '\n' +
3392
+ '**User agent:** ' + navigator.userAgent + '\n\n' +
3393
+ '### What went wrong\n\n' +
3394
+ '(describe the issue)\n\n' +
3395
+ '### Steps to reproduce\n\n' +
3396
+ '1. \n2. \n3. \n'
3397
+ );
3398
+ fbEl.href = 'https://github.com/rohitg00/agentmemory/issues/new?title=' +
3399
+ encodeURIComponent('[viewer] ') + '&body=' + body;
3400
+ }
3401
+ }
3402
+ document.addEventListener('click', function(e) {
3403
+ if (!(e.target instanceof Element)) return;
3404
+ var btn = e.target.closest('[data-dismiss-flag]');
3405
+ if (btn) {
3406
+ e.stopPropagation();
3407
+ var key = btn.getAttribute('data-dismiss-flag');
3408
+ var d = loadDismissedFlags();
3409
+ d[key] = true;
3410
+ saveDismissedFlags(d);
3411
+ if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
3412
+ return;
3413
+ }
3414
+ var toggle = e.target.closest('[data-action="toggle-flags"]');
3415
+ if (toggle) {
3416
+ var host = document.getElementById('flag-banners');
3417
+ var cur = host.getAttribute('data-expanded') === '1';
3418
+ host.setAttribute('data-expanded', cur ? '0' : '1');
3419
+ if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
3420
+ }
3421
+ });
3422
+ // Re-render banners when switching tabs so tab-specific banners appear
3423
+ var _origSwitchTab = switchTab;
3424
+ switchTab = function(tab) {
3425
+ _origSwitchTab(tab);
3426
+ if (state.flagsConfig) renderFlagBanners(state.flagsConfig);
3427
+ };
3428
+ fetchFlags();
2969
3429
  document.addEventListener('click', function(e) {
2970
3430
  if (!(e.target instanceof Element)) return;
2971
3431
  var target = e.target.closest('[data-action]');