@sesamespace/hivemind 0.8.13 → 0.10.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 (54) hide show
  1. package/README.md +2 -1
  2. package/dist/{chunk-MLY4VFOO.js → chunk-BHCDOHSK.js} +3 -3
  3. package/dist/{chunk-PFZO67E2.js → chunk-DPLCEMEC.js} +2 -2
  4. package/dist/{chunk-HTLHMXAL.js → chunk-FBQBBAPZ.js} +2 -2
  5. package/dist/{chunk-NSTTILSN.js → chunk-FK6WYXRM.js} +79 -2
  6. package/dist/chunk-FK6WYXRM.js.map +1 -0
  7. package/dist/{chunk-LJHJGDKY.js → chunk-ICSJNKI6.js} +62 -2
  8. package/dist/chunk-ICSJNKI6.js.map +1 -0
  9. package/dist/{chunk-4Y7A25UG.js → chunk-IXBIAX76.js} +2 -2
  10. package/dist/{chunk-ZM7RK5YV.js → chunk-M3A2WRXM.js} +560 -37
  11. package/dist/chunk-M3A2WRXM.js.map +1 -0
  12. package/dist/commands/fleet.js +3 -3
  13. package/dist/commands/init.js +3 -3
  14. package/dist/commands/start.js +3 -3
  15. package/dist/commands/upgrade.js +1 -1
  16. package/dist/commands/watchdog.js +3 -3
  17. package/dist/dashboard.html +873 -131
  18. package/dist/index.js +2 -2
  19. package/dist/main.js +375 -7
  20. package/dist/main.js.map +1 -1
  21. package/dist/start.js +1 -1
  22. package/install.sh +162 -0
  23. package/package.json +24 -23
  24. package/packages/memory/Cargo.lock +6480 -0
  25. package/packages/memory/Cargo.toml +21 -0
  26. package/packages/memory/src/src/context.rs +179 -0
  27. package/packages/memory/src/src/embeddings.rs +51 -0
  28. package/packages/memory/src/src/main.rs +887 -0
  29. package/packages/memory/src/src/promotion.rs +808 -0
  30. package/packages/memory/src/src/scoring.rs +142 -0
  31. package/packages/memory/src/src/store.rs +460 -0
  32. package/packages/memory/src/src/tasks.rs +321 -0
  33. package/.pnpmrc.json +0 -1
  34. package/AUTO-DEBUG-DESIGN.md +0 -267
  35. package/DASHBOARD-PLAN.md +0 -206
  36. package/MEMORY-ENHANCEMENT-PLAN.md +0 -211
  37. package/TOOL-USE-DESIGN.md +0 -173
  38. package/dist/chunk-LJHJGDKY.js.map +0 -1
  39. package/dist/chunk-NSTTILSN.js.map +0 -1
  40. package/dist/chunk-ZM7RK5YV.js.map +0 -1
  41. package/docs/TOOL-PARITY-PLAN.md +0 -191
  42. package/src/memory/dashboard-integration.ts +0 -295
  43. package/src/memory/index.ts +0 -187
  44. package/src/memory/performance-test.ts +0 -208
  45. package/src/memory/processors/agent-sync.ts +0 -312
  46. package/src/memory/processors/command-learner.ts +0 -298
  47. package/src/memory/processors/memory-api-client.ts +0 -105
  48. package/src/memory/processors/message-flow-integration.ts +0 -168
  49. package/src/memory/processors/research-digester.ts +0 -204
  50. package/test-caitlin-access.md +0 -11
  51. /package/dist/{chunk-MLY4VFOO.js.map → chunk-BHCDOHSK.js.map} +0 -0
  52. /package/dist/{chunk-PFZO67E2.js.map → chunk-DPLCEMEC.js.map} +0 -0
  53. /package/dist/{chunk-HTLHMXAL.js.map → chunk-FBQBBAPZ.js.map} +0 -0
  54. /package/dist/{chunk-4Y7A25UG.js.map → chunk-IXBIAX76.js.map} +0 -0
@@ -12,23 +12,43 @@
12
12
  }
13
13
  * { margin: 0; padding: 0; box-sizing: border-box; }
14
14
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); display: flex; height: 100vh; overflow: hidden; }
15
-
15
+
16
16
  /* Sidebar */
17
17
  .sidebar { width: 200px; background: var(--bg2); border-right: 1px solid var(--border); padding: 16px 0; flex-shrink: 0; display: flex; flex-direction: column; }
18
- .sidebar h1 { font-size: 16px; padding: 0 16px 16px; border-bottom: 1px solid var(--border); color: var(--accent); }
19
- .sidebar nav { padding: 8px 0; }
18
+ .sidebar-header { padding: 0 16px 12px; border-bottom: 1px solid var(--border); }
19
+ .sidebar-header h1 { font-size: 16px; color: var(--accent); margin-bottom: 6px; }
20
+ .health-indicator { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text2); }
21
+ .health-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text2); flex-shrink: 0; }
22
+ .health-dot.ok { background: var(--green); }
23
+ .health-dot.degraded { background: var(--yellow); }
24
+ .health-dot.offline { background: var(--red); }
25
+ .sidebar nav { padding: 8px 0; flex: 1; }
20
26
  .sidebar a { display: block; padding: 8px 16px; color: var(--text2); text-decoration: none; font-size: 14px; cursor: pointer; }
21
27
  .sidebar a:hover, .sidebar a.active { color: var(--text); background: var(--bg3); }
22
-
28
+
23
29
  /* Main */
24
30
  .main { flex: 1; overflow-y: auto; padding: 24px; }
25
-
31
+
32
+ /* Toast notifications */
33
+ .toast-container { position: fixed; top: 16px; right: 16px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
34
+ .toast { padding: 10px 16px; border-radius: 6px; font-size: 13px; color: #fff; animation: toastIn 0.3s ease; max-width: 400px; cursor: pointer; }
35
+ .toast.info { background: var(--accent2); }
36
+ .toast.success { background: #238636; }
37
+ .toast.warning { background: #9e6a03; }
38
+ .toast.error { background: #da3633; }
39
+ @keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: none; } }
40
+
26
41
  /* Filters */
27
- .filters { display: flex; gap: 12px; margin-bottom: 16px; align-items: center; }
42
+ .filters { display: flex; gap: 12px; margin-bottom: 16px; align-items: center; flex-wrap: wrap; }
28
43
  .filters input, .filters select { background: var(--bg2); border: 1px solid var(--border); color: var(--text); padding: 6px 10px; border-radius: 6px; font-size: 13px; }
29
44
  .filters input:focus, .filters select:focus { outline: none; border-color: var(--accent); }
30
45
  .filters button { background: var(--accent2); color: #fff; border: none; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }
31
-
46
+ .filters button:hover { background: var(--accent); }
47
+
48
+ /* Auto-refresh toggle */
49
+ .auto-refresh-btn { background: var(--bg3) !important; border: 1px solid var(--border) !important; color: var(--text2) !important; }
50
+ .auto-refresh-btn.active { border-color: var(--green) !important; color: var(--green) !important; }
51
+
32
52
  /* Request list */
33
53
  .req-row { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.15s; }
34
54
  .req-row:hover { border-color: var(--accent); }
@@ -39,14 +59,14 @@
39
59
  .req-summary .model { color: var(--text2); }
40
60
  .req-summary .latency { color: var(--yellow); text-align: right; }
41
61
  .req-summary .tokens { color: var(--text2); text-align: right; }
42
-
62
+
43
63
  /* Detail */
44
64
  .req-detail { padding: 0 14px 14px; border-top: 1px solid var(--border); display: none; }
45
65
  .req-detail.open { display: block; }
46
66
  .section { margin-top: 12px; }
47
67
  .section-header { font-size: 12px; font-weight: 600; color: var(--accent); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; cursor: pointer; user-select: none; }
48
- .section-header::before { content: ' '; }
49
- .section-header.open::before { content: ' '; }
68
+ .section-header::before { content: '\25B8 '; }
69
+ .section-header.open::before { content: '\25BE '; }
50
70
  .section-body { display: none; }
51
71
  .section-body.open { display: block; }
52
72
  .section-body pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; max-height: 400px; overflow-y: auto; }
@@ -54,78 +74,187 @@
54
74
  .episode .meta { color: var(--text2); font-size: 11px; margin-bottom: 2px; }
55
75
  .episode .score { color: var(--yellow); }
56
76
  .l3-entry { background: var(--bg); border-left: 3px solid var(--purple); padding: 6px 10px; margin-bottom: 4px; font-size: 12px; }
57
-
58
- /* Token bar */
59
- .token-bar { display: flex; height: 20px; border-radius: 4px; overflow: hidden; margin-top: 4px; font-size: 10px; }
60
- .token-bar div { display: flex; align-items: center; justify-content: center; color: #fff; min-width: 30px; }
77
+
78
+ /* Token cards + bar */
79
+ .token-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 8px; }
80
+ .token-card { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 6px 8px; text-align: center; }
81
+ .token-card .label { font-size: 10px; color: var(--text2); text-transform: uppercase; }
82
+ .token-card .value { font-size: 16px; font-weight: 600; margin-top: 2px; }
83
+ .token-card .pct { font-size: 10px; color: var(--text2); }
84
+ .token-bar { display: flex; height: 20px; border-radius: 4px; overflow: hidden; font-size: 10px; }
85
+ .token-bar div { display: flex; align-items: center; justify-content: center; color: #fff; min-width: 30px; position: relative; }
86
+ .token-bar div[title] { cursor: help; }
61
87
  .tb-sys { background: #58a6ff; }
62
88
  .tb-hist { background: #bc8cff; }
63
89
  .tb-user { background: #3fb950; }
64
-
90
+
65
91
  /* Config */
66
92
  .config-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
67
93
  .config-item { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 8px; text-align: center; }
68
94
  .config-item .label { font-size: 10px; color: var(--text2); text-transform: uppercase; }
69
95
  .config-item .value { font-size: 14px; font-weight: 600; margin-top: 2px; }
70
-
96
+
97
+ /* Status cards grid */
98
+ .status-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
99
+ .status-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
100
+ .status-card .label { font-size: 11px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.5px; }
101
+ .status-card .value { font-size: 24px; font-weight: 700; margin-top: 4px; }
102
+ .status-card .sub { font-size: 11px; color: var(--text2); margin-top: 2px; }
103
+
71
104
  /* Memory browser */
72
105
  .ctx-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 8px; }
73
106
  .ctx-card h3 { font-size: 14px; color: var(--accent); margin-bottom: 4px; }
74
107
  .ctx-card .meta { font-size: 12px; color: var(--text2); }
108
+ .ctx-card .actions { display: flex; gap: 6px; margin-top: 8px; }
75
109
  .btn-sm { background: var(--bg3); border: 1px solid var(--border); color: var(--text2); padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; }
76
110
  .btn-sm:hover { border-color: var(--accent); color: var(--text); }
77
111
  .btn-danger { border-color: var(--red); color: var(--red); }
78
112
  .btn-danger:hover { background: var(--red); color: #fff; }
79
-
113
+ .btn-primary { background: var(--accent2); border-color: var(--accent2); color: #fff; }
114
+ .btn-primary:hover { background: var(--accent); }
115
+
80
116
  /* Pagination */
81
117
  .pagination { display: flex; gap: 8px; margin-top: 16px; justify-content: center; align-items: center; font-size: 13px; color: var(--text2); }
82
118
  .pagination button { background: var(--bg2); border: 1px solid var(--border); color: var(--text); padding: 4px 12px; border-radius: 4px; cursor: pointer; }
83
119
  .pagination button:disabled { opacity: 0.4; cursor: default; }
84
-
120
+
85
121
  .hidden { display: none; }
86
122
  .view { display: none; }
87
123
  .view.active { display: block; }
88
-
124
+
89
125
  /* Memory browser sub-nav */
90
126
  .mem-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
91
127
  .mem-tab { padding: 6px 14px; border-radius: 6px; background: var(--bg2); border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 13px; }
92
128
  .mem-tab.active { border-color: var(--accent); color: var(--accent); }
129
+
130
+ /* Sub-tabs for promotion view */
131
+ .sub-tabs { display: flex; gap: 8px; margin-bottom: 16px; }
132
+ .sub-tab { padding: 6px 14px; border-radius: 6px; background: var(--bg2); border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 13px; }
133
+ .sub-tab.active { border-color: var(--accent); color: var(--accent); }
134
+
135
+ /* Progress bar */
136
+ .progress-bar { background: var(--bg3); border-radius: 4px; height: 16px; overflow: hidden; position: relative; }
137
+ .progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #fff; }
138
+ .progress-fill.green { background: var(--green); }
139
+ .progress-fill.yellow { background: var(--yellow); }
140
+ .progress-fill.purple { background: var(--purple); }
141
+
142
+ /* Search results */
143
+ .search-result { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; padding: 10px 14px; margin-bottom: 8px; }
144
+ .search-result .meta { font-size: 11px; color: var(--text2); margin-bottom: 4px; }
145
+ .search-result .content { font-size: 13px; }
146
+ .search-group-header { font-size: 14px; font-weight: 600; color: var(--purple); margin: 16px 0 8px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
147
+
148
+ /* L3 edit textarea */
149
+ .l3-edit-area { width: 100%; min-height: 80px; background: var(--bg); border: 1px solid var(--accent); color: var(--text); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 12px; resize: vertical; }
150
+
151
+ /* Decay curve SVG */
152
+ .decay-curve { margin-top: 8px; }
153
+
154
+ /* Table */
155
+ .data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
156
+ .data-table th { text-align: left; padding: 8px; border-bottom: 2px solid var(--border); color: var(--text2); font-size: 11px; text-transform: uppercase; }
157
+ .data-table td { padding: 8px; border-bottom: 1px solid var(--border); }
158
+ .data-table tr:hover td { background: var(--bg3); }
159
+
160
+ /* L1 message */
161
+ .l1-msg { padding: 8px 12px; margin-bottom: 4px; border-radius: 6px; font-size: 13px; }
162
+ .l1-msg.user { background: var(--bg3); border-left: 3px solid var(--green); }
163
+ .l1-msg.assistant { background: var(--bg2); border-left: 3px solid var(--accent); }
164
+ .l1-msg .role { font-size: 11px; font-weight: 600; color: var(--text2); margin-bottom: 2px; text-transform: uppercase; }
93
165
  </style>
94
166
  </head>
95
167
  <body>
96
168
  <div class="sidebar">
97
- <h1>🧠 Hivemind</h1>
169
+ <div class="sidebar-header">
170
+ <h1>Hivemind</h1>
171
+ <div class="health-indicator">
172
+ <span class="health-dot" id="health-dot"></span>
173
+ <span id="health-text">Checking...</span>
174
+ </div>
175
+ </div>
98
176
  <nav>
99
- <a data-view="requests" class="active">Requests</a>
177
+ <a data-view="health" class="active">Health</a>
178
+ <a data-view="requests">Requests</a>
100
179
  <a data-view="memory">Memory</a>
180
+ <a data-view="search">Search</a>
181
+ <a data-view="promotion">Promotion</a>
101
182
  <a data-view="contexts">Contexts</a>
102
183
  </nav>
103
184
  </div>
104
185
  <div class="main">
186
+
187
+ <!-- Toast Container -->
188
+ <div class="toast-container" id="toast-container"></div>
189
+
190
+ <!-- Health View -->
191
+ <div id="v-health" class="view active">
192
+ <h2 style="margin-bottom:16px">System Status</h2>
193
+ <div class="status-grid" id="health-cards"></div>
194
+ <h3 style="margin:24px 0 12px">Context Summary</h3>
195
+ <table class="data-table" id="ctx-summary-table">
196
+ <thead><tr><th>Context</th><th>Episodes</th><th>L3 Entries</th><th>Created</th></tr></thead>
197
+ <tbody id="ctx-summary-body"></tbody>
198
+ </table>
199
+ <h3 style="margin:24px 0 12px">Scoring Configuration</h3>
200
+ <div id="scoring-config-section"></div>
201
+ </div>
202
+
105
203
  <!-- Requests View -->
106
- <div id="v-requests" class="view active">
204
+ <div id="v-requests" class="view">
107
205
  <div class="filters">
108
- <input type="text" id="f-context" placeholder="Filter context" />
109
- <input type="text" id="f-sender" placeholder="Filter sender" />
206
+ <input type="text" id="f-context" placeholder="Filter context..." />
207
+ <input type="text" id="f-sender" placeholder="Filter sender..." />
110
208
  <button onclick="loadRequests()">Filter</button>
209
+ <button class="auto-refresh-btn" id="ar-requests" onclick="toggleAutoRefresh('requests')">Auto-refresh</button>
111
210
  </div>
112
211
  <div id="req-list"></div>
113
212
  <div class="pagination" id="req-pagination"></div>
114
213
  </div>
115
-
214
+
116
215
  <!-- Memory View -->
117
216
  <div id="v-memory" class="view">
118
217
  <div class="mem-tabs">
119
218
  <div class="mem-tab active" data-mtab="l3">L3 Knowledge</div>
120
219
  <div class="mem-tab" data-mtab="l2">L2 Episodes</div>
220
+ <div class="mem-tab" data-mtab="l1">L1 History</div>
121
221
  </div>
122
- <div id="mem-ctx-select" style="margin-bottom:12px">
123
- <select id="mem-context"><option value="">Select context…</option></select>
222
+ <div id="mem-ctx-select" style="margin-bottom:12px;display:flex;gap:12px;align-items:center">
223
+ <select id="mem-context"><option value="">Select context...</option></select>
224
+ <button class="auto-refresh-btn" id="ar-memory" onclick="toggleAutoRefresh('memory')">Auto-refresh</button>
124
225
  </div>
125
226
  <div id="mem-l3" class="view active"></div>
126
227
  <div id="mem-l2" class="view"></div>
228
+ <div id="mem-l1" class="view"></div>
127
229
  </div>
128
-
230
+
231
+ <!-- Search View -->
232
+ <div id="v-search" class="view">
233
+ <h2 style="margin-bottom:16px">Cross-Context Search</h2>
234
+ <div class="filters">
235
+ <input type="text" id="search-query" placeholder="Search query..." style="flex:1;min-width:200px" />
236
+ <select id="search-scope">
237
+ <option value="all">All Contexts</option>
238
+ </select>
239
+ <input type="number" id="search-limit" value="20" min="1" max="100" style="width:60px" />
240
+ <button onclick="runSearch()">Search</button>
241
+ </div>
242
+ <div id="search-results"></div>
243
+ </div>
244
+
245
+ <!-- Promotion View -->
246
+ <div id="v-promotion" class="view">
247
+ <h2 style="margin-bottom:16px">Promotion & Scoring</h2>
248
+ <div class="sub-tabs">
249
+ <div class="sub-tab active" data-ptab="thresholds">Thresholds</div>
250
+ <div class="sub-tab" data-ptab="candidates">Candidates</div>
251
+ <div class="sub-tab" data-ptab="scoring">Scoring</div>
252
+ </div>
253
+ <div id="promo-thresholds" class="view active"></div>
254
+ <div id="promo-candidates" class="view"></div>
255
+ <div id="promo-scoring" class="view"></div>
256
+ </div>
257
+
129
258
  <!-- Contexts View -->
130
259
  <div id="v-contexts" class="view">
131
260
  <div id="ctx-list"></div>
@@ -136,19 +265,104 @@
136
265
  const API = '';
137
266
  let reqOffset = 0;
138
267
  const REQ_LIMIT = 30;
268
+ let l2Page = 0;
269
+ const L2_PAGE_SIZE = 50;
270
+ let l2AllEpisodes = [];
271
+ let l2SearchMode = false;
272
+ let autoRefreshTimers = {};
273
+ let activeMemTab = 'l3';
274
+
275
+ // ===== Toast Notifications =====
276
+ function showNotification(message, type = 'info') {
277
+ const container = document.getElementById('toast-container');
278
+ const toast = document.createElement('div');
279
+ toast.className = 'toast ' + type;
280
+ toast.textContent = message;
281
+ toast.addEventListener('click', () => toast.remove());
282
+ container.appendChild(toast);
283
+ setTimeout(() => { if (toast.parentNode) toast.remove(); }, 5000);
284
+ }
139
285
 
140
- // Navigation
286
+ // ===== Health Polling =====
287
+ let daemonStatus = 'offline';
288
+ let daemonVersion = '';
289
+
290
+ async function checkHealth() {
291
+ const dot = document.getElementById('health-dot');
292
+ const text = document.getElementById('health-text');
293
+ try {
294
+ const res = await fetch(API + '/api/health');
295
+ if (res.ok) {
296
+ const data = await res.json();
297
+ daemonStatus = data.status || 'ok';
298
+ daemonVersion = data.version || '';
299
+ dot.className = 'health-dot ok';
300
+ text.textContent = 'v' + daemonVersion;
301
+ } else {
302
+ daemonStatus = 'degraded';
303
+ dot.className = 'health-dot degraded';
304
+ text.textContent = 'Degraded';
305
+ }
306
+ } catch {
307
+ daemonStatus = 'offline';
308
+ dot.className = 'health-dot offline';
309
+ text.textContent = 'Daemon offline';
310
+ }
311
+ }
312
+ checkHealth();
313
+ setInterval(checkHealth, 10000);
314
+
315
+ // ===== Auto-Refresh =====
316
+ function toggleAutoRefresh(view) {
317
+ const btn = document.getElementById('ar-' + view);
318
+ if (autoRefreshTimers[view]) {
319
+ clearInterval(autoRefreshTimers[view]);
320
+ autoRefreshTimers[view] = null;
321
+ btn.classList.remove('active');
322
+ localStorage.removeItem('ar-' + view);
323
+ } else {
324
+ autoRefreshTimers[view] = setInterval(() => refreshView(view), 5000);
325
+ btn.classList.add('active');
326
+ localStorage.setItem('ar-' + view, '1');
327
+ }
328
+ }
329
+
330
+ function refreshView(view) {
331
+ if (view === 'requests') loadRequests();
332
+ else if (view === 'memory') {
333
+ const ctx = document.getElementById('mem-context').value;
334
+ if (ctx) {
335
+ if (activeMemTab === 'l3') loadL3(ctx);
336
+ else if (activeMemTab === 'l2') loadL2(ctx);
337
+ else if (activeMemTab === 'l1') loadL1(ctx);
338
+ }
339
+ }
340
+ }
341
+
342
+ // Restore auto-refresh state
343
+ ['requests', 'memory'].forEach(v => {
344
+ if (localStorage.getItem('ar-' + v)) {
345
+ const btn = document.getElementById('ar-' + v);
346
+ if (btn) {
347
+ autoRefreshTimers[v] = setInterval(() => refreshView(v), 5000);
348
+ btn.classList.add('active');
349
+ }
350
+ }
351
+ });
352
+
353
+ // ===== Navigation =====
141
354
  document.querySelectorAll('.sidebar a').forEach(a => {
142
355
  a.addEventListener('click', () => {
143
356
  document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
144
357
  a.classList.add('active');
145
- document.querySelectorAll('.view').forEach(v => {
146
- if (v.closest('.main')) v.classList.remove('active');
147
- });
358
+ document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
148
359
  const viewId = 'v-' + a.dataset.view;
149
360
  document.getElementById(viewId)?.classList.add('active');
361
+ if (a.dataset.view === 'health') loadHealthView();
150
362
  if (a.dataset.view === 'contexts') loadContexts();
151
363
  if (a.dataset.view === 'memory') loadMemoryContexts();
364
+ if (a.dataset.view === 'search') loadSearchContexts();
365
+ if (a.dataset.view === 'promotion') loadPromotion();
152
366
  });
153
367
  });
154
368
 
@@ -157,85 +371,189 @@ document.querySelectorAll('.mem-tab').forEach(t => {
157
371
  t.addEventListener('click', () => {
158
372
  document.querySelectorAll('.mem-tab').forEach(x => x.classList.remove('active'));
159
373
  t.classList.add('active');
160
- document.getElementById('mem-l3').classList.toggle('active', t.dataset.mtab === 'l3');
161
- document.getElementById('mem-l2').classList.toggle('active', t.dataset.mtab === 'l2');
374
+ activeMemTab = t.dataset.mtab;
375
+ document.getElementById('mem-l3').classList.toggle('active', activeMemTab === 'l3');
376
+ document.getElementById('mem-l2').classList.toggle('active', activeMemTab === 'l2');
377
+ document.getElementById('mem-l1').classList.toggle('active', activeMemTab === 'l1');
378
+ const ctx = document.getElementById('mem-context').value;
379
+ if (ctx) {
380
+ if (activeMemTab === 'l1') loadL1(ctx);
381
+ }
382
+ });
383
+ });
384
+
385
+ // Promotion sub-tabs
386
+ document.querySelectorAll('.sub-tab').forEach(t => {
387
+ t.addEventListener('click', () => {
388
+ document.querySelectorAll('.sub-tab').forEach(x => x.classList.remove('active'));
389
+ t.classList.add('active');
390
+ document.getElementById('promo-thresholds').classList.toggle('active', t.dataset.ptab === 'thresholds');
391
+ document.getElementById('promo-candidates').classList.toggle('active', t.dataset.ptab === 'candidates');
392
+ document.getElementById('promo-scoring').classList.toggle('active', t.dataset.ptab === 'scoring');
393
+ if (t.dataset.ptab === 'candidates') loadCandidates();
394
+ if (t.dataset.ptab === 'scoring') loadScoringTab();
162
395
  });
163
396
  });
164
397
 
165
398
  document.getElementById('mem-context').addEventListener('change', () => {
166
399
  const ctx = document.getElementById('mem-context').value;
167
- if (ctx) { loadL3(ctx); loadL2(ctx); }
400
+ if (ctx) { loadL3(ctx); loadL2(ctx); if (activeMemTab === 'l1') loadL1(ctx); }
168
401
  });
169
402
 
170
- // Requests
403
+ // ===== Health & System Status View =====
404
+ async function loadHealthView() {
405
+ const cards = document.getElementById('health-cards');
406
+ const tbody = document.getElementById('ctx-summary-body');
407
+ const scoringEl = document.getElementById('scoring-config-section');
408
+
409
+ try {
410
+ const [statsRes, ctxRes] = await Promise.all([
411
+ fetch(API + '/api/stats'),
412
+ fetch(API + '/api/contexts'),
413
+ ]);
414
+
415
+ if (!statsRes.ok || !ctxRes.ok) {
416
+ showNotification('Failed to load system stats', 'error');
417
+ cards.innerHTML = '<p style="color:var(--red)">Failed to load stats (daemon may be offline)</p>';
418
+ return;
419
+ }
420
+
421
+ const stats = await statsRes.json();
422
+ const ctxData = await ctxRes.json();
423
+ const contexts = ctxData.contexts || [];
424
+
425
+ const totalEpisodes = Object.values(stats.total_episodes || {}).reduce((a, b) => a + b, 0);
426
+ const reqCountRes = await fetch(API + '/api/requests?limit=1');
427
+ const reqCount = reqCountRes.ok ? (await reqCountRes.json()).total || 0 : '?';
428
+
429
+ cards.innerHTML = `
430
+ <div class="status-card"><div class="label">Daemon Status</div><div class="value" style="color:var(${daemonStatus === 'ok' ? '--green' : '--red'})">${daemonStatus === 'ok' ? 'Online' : daemonStatus}</div><div class="sub">v${esc(daemonVersion)}</div></div>
431
+ <div class="status-card"><div class="label">Embedding Model</div><div class="value" style="font-size:14px">${esc(stats.embedding_model)}</div></div>
432
+ <div class="status-card"><div class="label">Total Episodes</div><div class="value">${totalEpisodes}</div><div class="sub">${contexts.length} contexts</div></div>
433
+ <div class="status-card"><div class="label">L3 Entries</div><div class="value">${stats.total_l3_entries}</div></div>
434
+ <div class="status-card"><div class="label">Access Records</div><div class="value">${stats.total_access_records}</div></div>
435
+ <div class="status-card"><div class="label">Request Logs</div><div class="value">${reqCount}</div></div>
436
+ `;
437
+
438
+ // Context summary table
439
+ tbody.innerHTML = '';
440
+ for (const c of contexts) {
441
+ const epCount = stats.total_episodes?.[c.name] ?? c.episode_count ?? 0;
442
+ const tr = document.createElement('tr');
443
+ tr.innerHTML = `<td style="color:var(--accent)">${esc(c.name)}</td><td>${epCount}</td><td>-</td><td>${c.created_at ? new Date(c.created_at).toLocaleDateString() : '-'}</td>`;
444
+ tbody.appendChild(tr);
445
+ }
446
+
447
+ // Scoring section
448
+ const defaultHL = stats.default_half_life_hours || 48;
449
+ scoringEl.innerHTML = `
450
+ <p style="color:var(--text2);font-size:13px;margin-bottom:12px">Default half-life: <strong>${defaultHL}h</strong>. Episodes lose ~50% relevance weight every ${defaultHL} hours.</p>
451
+ ${renderDecayCurve(defaultHL)}
452
+ `;
453
+
454
+ } catch (err) {
455
+ cards.innerHTML = '<p style="color:var(--red)">Cannot connect to memory daemon</p>';
456
+ showNotification('Health view failed: ' + err.message, 'error');
457
+ }
458
+ }
459
+
460
+ function renderDecayCurve(halfLife) {
461
+ const w = 300, h = 100, pad = 30;
462
+ const points = [];
463
+ const maxH = halfLife * 4;
464
+ for (let t = 0; t <= maxH; t += maxH / 50) {
465
+ const weight = Math.exp(-Math.LN2 / halfLife * t);
466
+ const x = pad + (t / maxH) * (w - pad * 2);
467
+ const y = pad + (1 - weight) * (h - pad * 2);
468
+ points.push(`${x},${y}`);
469
+ }
470
+ return `<svg width="${w}" height="${h}" class="decay-curve" style="background:var(--bg);border:1px solid var(--border);border-radius:4px">
471
+ <polyline points="${points.join(' ')}" fill="none" stroke="var(--accent)" stroke-width="2"/>
472
+ <text x="${pad}" y="${pad - 8}" fill="var(--text2)" font-size="10">1.0</text>
473
+ <text x="${pad}" y="${h - 8}" fill="var(--text2)" font-size="10">0.0</text>
474
+ <text x="${w - pad}" y="${h - 8}" fill="var(--text2)" font-size="10">${maxH}h</text>
475
+ <line x1="${pad + (halfLife / maxH) * (w - pad * 2)}" y1="${pad}" x2="${pad + (halfLife / maxH) * (w - pad * 2)}" y2="${h - pad}" stroke="var(--yellow)" stroke-dasharray="4"/>
476
+ <text x="${pad + (halfLife / maxH) * (w - pad * 2) - 10}" y="${h - 8}" fill="var(--yellow)" font-size="9">${halfLife}h</text>
477
+ </svg>`;
478
+ }
479
+
480
+ // ===== Requests =====
171
481
  async function loadRequests() {
172
482
  const ctx = document.getElementById('f-context').value;
173
483
  const sender = document.getElementById('f-sender').value;
174
484
  const params = new URLSearchParams({ limit: REQ_LIMIT, offset: reqOffset });
175
485
  if (ctx) params.set('context', ctx);
176
486
  if (sender) params.set('sender', sender);
177
-
178
- const res = await fetch(`${API}/api/requests?${params}`);
179
- const data = await res.json();
180
-
181
- const list = document.getElementById('req-list');
182
- list.innerHTML = '';
183
-
184
- for (const r of data.requests) {
185
- const row = document.createElement('div');
186
- row.className = 'req-row';
187
- const ts = new Date(r.timestamp).toLocaleString();
188
- const latency = r.response_latency_ms + 'ms';
189
- const tokens = r.token_est_total;
190
- const model = r.response_model.split('/').pop().slice(0, 15);
191
-
192
- row.innerHTML = `
193
- <div class="req-summary">
194
- <span class="ts">${ts}</span>
195
- <span class="sender">${esc(r.sender_handle || 'stdin')}</span>
196
- <span class="ctx">${esc(r.context)}</span>
197
- <span class="model">${esc(model)}</span>
198
- <span class="latency">${latency}</span>
199
- <span class="tokens">~${tokens}t</span>
200
- </div>
201
- <div class="req-detail" id="detail-${r.id}"></div>
487
+
488
+ try {
489
+ const res = await fetch(API + '/api/requests?' + params);
490
+ if (!res.ok) { showNotification('Failed to load requests', 'error'); return; }
491
+ const data = await res.json();
492
+
493
+ const list = document.getElementById('req-list');
494
+ list.innerHTML = '';
495
+
496
+ for (const r of data.requests) {
497
+ const row = document.createElement('div');
498
+ row.className = 'req-row';
499
+ const ts = new Date(r.timestamp).toLocaleString();
500
+ const latency = r.response_latency_ms + 'ms';
501
+ const tokens = r.token_est_total;
502
+ const model = r.response_model.split('/').pop().slice(0, 15);
503
+
504
+ row.innerHTML = `
505
+ <div class="req-summary">
506
+ <span class="ts">${ts}</span>
507
+ <span class="sender">${esc(r.sender_handle || 'stdin')}</span>
508
+ <span class="ctx">${esc(r.context)}</span>
509
+ <span class="model">${esc(model)}</span>
510
+ <span class="latency">${latency}</span>
511
+ <span class="tokens">~${tokens}t</span>
512
+ </div>
513
+ <div class="req-detail" id="detail-${r.id}"></div>
514
+ `;
515
+
516
+ row.querySelector('.req-summary').addEventListener('click', () => {
517
+ const detail = document.getElementById('detail-' + r.id);
518
+ if (detail.classList.contains('open')) {
519
+ detail.classList.remove('open');
520
+ } else {
521
+ renderDetail(detail, r);
522
+ detail.classList.add('open');
523
+ }
524
+ });
525
+ list.appendChild(row);
526
+ }
527
+
528
+ // Pagination
529
+ const pag = document.getElementById('req-pagination');
530
+ const page = Math.floor(reqOffset / REQ_LIMIT) + 1;
531
+ const totalPages = Math.ceil(data.total / REQ_LIMIT);
532
+ pag.innerHTML = `
533
+ <button ${reqOffset === 0 ? 'disabled' : ''} onclick="reqOffset -= ${REQ_LIMIT}; loadRequests()">Prev</button>
534
+ <span>Page ${page} of ${totalPages} (${data.total} total)</span>
535
+ <button ${reqOffset + REQ_LIMIT >= data.total ? 'disabled' : ''} onclick="reqOffset += ${REQ_LIMIT}; loadRequests()">Next</button>
202
536
  `;
203
-
204
- row.querySelector('.req-summary').addEventListener('click', () => {
205
- const detail = document.getElementById('detail-' + r.id);
206
- if (detail.classList.contains('open')) {
207
- detail.classList.remove('open');
208
- } else {
209
- renderDetail(detail, r);
210
- detail.classList.add('open');
211
- }
212
- });
213
- list.appendChild(row);
537
+ } catch (err) {
538
+ showNotification('Failed to load requests: ' + err.message, 'error');
214
539
  }
215
-
216
- // Pagination
217
- const pag = document.getElementById('req-pagination');
218
- const page = Math.floor(reqOffset / REQ_LIMIT) + 1;
219
- const totalPages = Math.ceil(data.total / REQ_LIMIT);
220
- pag.innerHTML = `
221
- <button ${reqOffset === 0 ? 'disabled' : ''} onclick="reqOffset -= ${REQ_LIMIT}; loadRequests()">← Prev</button>
222
- <span>Page ${page} of ${totalPages} (${data.total} total)</span>
223
- <button ${reqOffset + REQ_LIMIT >= data.total ? 'disabled' : ''} onclick="reqOffset += ${REQ_LIMIT}; loadRequests()">Next →</button>
224
- `;
225
540
  }
226
541
 
227
542
  function renderDetail(el, r) {
228
543
  const comps = typeof r.system_prompt_components === 'string' ? JSON.parse(r.system_prompt_components) : r.system_prompt_components;
229
544
  const history = typeof r.conversation_history === 'string' ? JSON.parse(r.conversation_history) : r.conversation_history;
230
545
  const config = typeof r.config_snapshot === 'string' ? JSON.parse(r.config_snapshot) : r.config_snapshot;
231
-
546
+
232
547
  const totalTokens = r.token_est_total || 1;
233
- const sysPct = Math.round((r.token_est_system / totalTokens) * 100);
234
- const histPct = Math.round((r.token_est_history / totalTokens) * 100);
235
- const userPct = 100 - sysPct - histPct;
236
-
548
+ const sysTokens = r.token_est_system || 0;
549
+ const histTokens = r.token_est_history || 0;
550
+ const userTokens = r.token_est_user || 0;
551
+ const sysPct = Math.round((sysTokens / totalTokens) * 100);
552
+ const histPct = Math.round((histTokens / totalTokens) * 100);
553
+ const userPct = Math.max(100 - sysPct - histPct, 0);
554
+
237
555
  let html = '';
238
-
556
+
239
557
  // Config
240
558
  html += `<div class="section"><div class="config-grid">
241
559
  <div class="config-item"><div class="label">Model</div><div class="value">${esc(config?.model || r.response_model)}</div></div>
@@ -243,56 +561,62 @@ function renderDetail(el, r) {
243
561
  <div class="config-item"><div class="label">Top K</div><div class="value">${config?.topK ?? '?'}</div></div>
244
562
  <div class="config-item"><div class="label">Temperature</div><div class="value">${config?.temperature ?? '?'}</div></div>
245
563
  </div></div>`;
246
-
247
- // Token bar
564
+
565
+ // Token cards + bar
248
566
  html += `<div class="section">
249
567
  <div class="section-header open">Token Breakdown (~${totalTokens} total)</div>
250
568
  <div class="section-body open">
569
+ <div class="token-cards">
570
+ <div class="token-card"><div class="label">System</div><div class="value" style="color:var(--accent)">${sysTokens.toLocaleString()}</div><div class="pct">${sysPct}%</div></div>
571
+ <div class="token-card"><div class="label">History</div><div class="value" style="color:var(--purple)">${histTokens.toLocaleString()}</div><div class="pct">${histPct}%</div></div>
572
+ <div class="token-card"><div class="label">User</div><div class="value" style="color:var(--green)">${userTokens.toLocaleString()}</div><div class="pct">${userPct}%</div></div>
573
+ <div class="token-card"><div class="label">Total</div><div class="value">${totalTokens.toLocaleString()}</div><div class="pct">100%</div></div>
574
+ </div>
251
575
  <div class="token-bar">
252
- <div class="tb-sys" style="width:${sysPct}%">sys ${r.token_est_system}</div>
253
- <div class="tb-hist" style="width:${histPct}%">${r.token_est_history > 0 ? 'hist ' + r.token_est_history : ''}</div>
254
- <div class="tb-user" style="width:${Math.max(userPct, 5)}%">user ${r.token_est_user}</div>
576
+ <div class="tb-sys" style="width:${sysPct}%" title="System: ${sysTokens} tokens (${sysPct}%)">sys</div>
577
+ <div class="tb-hist" style="width:${histPct}%" title="History: ${histTokens} tokens (${histPct}%)">${histTokens > 0 ? 'hist' : ''}</div>
578
+ <div class="tb-user" style="width:${Math.max(userPct, 5)}%" title="User: ${userTokens} tokens (${userPct}%)">user</div>
255
579
  </div>
256
580
  </div>
257
581
  </div>`;
258
-
582
+
259
583
  // User message
260
584
  html += section('User Message', `<pre>${esc(r.user_message)}</pre>`, true);
261
-
585
+
262
586
  // Response
263
587
  const skipped = r.response_skipped ? ' <span style="color:var(--yellow)">[SKIPPED]</span>' : '';
264
588
  html += section('Response' + skipped, `<pre>${esc(r.response_content)}</pre>`, true);
265
-
589
+
266
590
  // L3 Knowledge
267
591
  if (comps?.l3Knowledge?.length > 0) {
268
592
  const l3Html = comps.l3Knowledge.map(k => `<div class="l3-entry">${esc(k)}</div>`).join('');
269
593
  html += section(`L3 Knowledge (${comps.l3Knowledge.length})`, l3Html, true);
270
594
  }
271
-
595
+
272
596
  // L2 Episodes
273
597
  if (comps?.l2Episodes?.length > 0) {
274
598
  const epHtml = comps.l2Episodes.map(ep => `
275
599
  <div class="episode">
276
- <div class="meta">${esc(ep.role)} · ${esc(ep.context_name)} · <span class="score">score: ${ep.score?.toFixed(3)}</span> · ${new Date(ep.timestamp).toLocaleString()}</div>
600
+ <div class="meta">${esc(ep.role)} &middot; ${esc(ep.context_name)} &middot; <span class="score">score: ${ep.score?.toFixed(3)}</span> &middot; ${new Date(ep.timestamp).toLocaleString()}</div>
277
601
  ${esc(ep.content)}
278
602
  </div>
279
603
  `).join('');
280
604
  html += section(`L2 Episodes (${comps.l2Episodes.length})`, epHtml, true);
281
605
  }
282
-
606
+
283
607
  // L1 History
284
608
  if (history?.length > 0) {
285
609
  const histHtml = history.map(m => `<div class="episode"><div class="meta">${esc(m.role)}</div>${esc(m.content)}</div>`).join('');
286
610
  html += section(`L1 History (${history.length} turns)`, histHtml, false);
287
611
  }
288
-
289
- // Identity (collapsed by default)
612
+
613
+ // Identity
290
614
  if (comps?.identity) {
291
615
  html += section('Identity Files', `<pre>${esc(comps.identity)}</pre>`, false);
292
616
  }
293
-
617
+
294
618
  el.innerHTML = html;
295
-
619
+
296
620
  // Wire section toggles
297
621
  el.querySelectorAll('.section-header').forEach(h => {
298
622
  h.addEventListener('click', () => {
@@ -309,10 +633,11 @@ function section(title, content, startOpen) {
309
633
  </div>`;
310
634
  }
311
635
 
312
- // Contexts
636
+ // ===== Contexts View =====
313
637
  async function loadContexts() {
314
638
  try {
315
- const res = await fetch(`${API}/api/contexts`);
639
+ const res = await fetch(API + '/api/contexts');
640
+ if (!res.ok) { showNotification('Failed to load contexts', 'error'); return; }
316
641
  const data = await res.json();
317
642
  const contexts = data.contexts || [];
318
643
  const list = document.getElementById('ctx-list');
@@ -323,8 +648,13 @@ async function loadContexts() {
323
648
  list.innerHTML = contexts.map(c => `
324
649
  <div class="ctx-card">
325
650
  <h3>${esc(c.name)}</h3>
326
- <div class="meta">${c.episode_count ?? '?'} episodes · created ${new Date(c.created_at).toLocaleDateString()}</div>
651
+ <div class="meta">${c.episode_count ?? '?'} episodes &middot; created ${c.created_at ? new Date(c.created_at).toLocaleDateString() : '?'}</div>
327
652
  ${c.description ? `<div class="meta">${esc(c.description)}</div>` : ''}
653
+ <div class="actions">
654
+ <button class="btn-sm btn-primary" onclick="drillToMemory('${esc(c.name)}')">Browse Episodes</button>
655
+ <button class="btn-sm" onclick="viewScoring('${esc(c.name)}')">Scoring</button>
656
+ ${c.name !== 'global' ? `<button class="btn-sm btn-danger" onclick="deleteContext('${esc(c.name)}')">Delete</button>` : ''}
657
+ </div>
328
658
  </div>
329
659
  `).join('');
330
660
  } catch (err) {
@@ -332,37 +662,100 @@ async function loadContexts() {
332
662
  }
333
663
  }
334
664
 
335
- // Memory browser
665
+ function drillToMemory(ctx) {
666
+ // Switch to memory view with context pre-selected
667
+ document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
668
+ document.querySelector('[data-view="memory"]').classList.add('active');
669
+ document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
670
+ document.getElementById('v-memory').classList.add('active');
671
+ loadMemoryContexts().then(() => {
672
+ document.getElementById('mem-context').value = ctx;
673
+ loadL3(ctx);
674
+ loadL2(ctx);
675
+ });
676
+ }
677
+
678
+ function viewScoring(ctx) {
679
+ // Switch to promotion > scoring
680
+ document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
681
+ document.querySelector('[data-view="promotion"]').classList.add('active');
682
+ document.querySelectorAll('.main > .view').forEach(v => v.classList.remove('active'));
683
+ document.getElementById('v-promotion').classList.add('active');
684
+ document.querySelectorAll('.sub-tab').forEach(x => x.classList.remove('active'));
685
+ document.querySelector('[data-ptab="scoring"]').classList.add('active');
686
+ document.getElementById('promo-thresholds').classList.remove('active');
687
+ document.getElementById('promo-candidates').classList.remove('active');
688
+ document.getElementById('promo-scoring').classList.add('active');
689
+ loadScoringTab();
690
+ }
691
+
692
+ async function deleteContext(name) {
693
+ if (!confirm(`Delete context "${name}" and all its episodes? This cannot be undone.`)) return;
694
+ try {
695
+ const res = await fetch(API + '/api/contexts/' + encodeURIComponent(name), { method: 'DELETE' });
696
+ if (res.ok || res.status === 204) {
697
+ showNotification('Context "' + name + '" deleted', 'success');
698
+ loadContexts();
699
+ } else {
700
+ showNotification('Failed to delete context', 'error');
701
+ }
702
+ } catch (err) {
703
+ showNotification('Delete failed: ' + err.message, 'error');
704
+ }
705
+ }
706
+
707
+ // ===== Memory Browser =====
336
708
  async function loadMemoryContexts() {
337
709
  try {
338
- const res = await fetch(`${API}/api/contexts`);
710
+ const res = await fetch(API + '/api/contexts');
711
+ if (!res.ok) {
712
+ showNotification('Failed to load memory contexts', 'error');
713
+ return;
714
+ }
339
715
  const data = await res.json();
340
716
  const sel = document.getElementById('mem-context');
341
717
  const current = sel.value;
342
- sel.innerHTML = '<option value="">Select context…</option>';
718
+ sel.innerHTML = '<option value="">Select context...</option>';
343
719
  for (const c of (data.contexts || [])) {
344
720
  sel.innerHTML += `<option value="${esc(c.name)}">${esc(c.name)} (${c.episode_count ?? '?'} eps)</option>`;
345
721
  }
346
722
  if (current) { sel.value = current; loadL3(current); loadL2(current); }
347
- } catch {}
723
+ } catch (err) {
724
+ showNotification('Failed to load contexts: ' + err.message, 'error');
725
+ }
348
726
  }
349
727
 
728
+ // ----- L3 Knowledge -----
350
729
  async function loadL3(ctx) {
351
730
  const el = document.getElementById('mem-l3');
352
731
  try {
353
- const res = await fetch(`${API}/api/contexts/${encodeURIComponent(ctx)}/l3`);
732
+ const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/l3');
733
+ if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load L3 data</p>'; showNotification('L3 load failed', 'error'); return; }
354
734
  const data = await res.json();
355
735
  const entries = data.entries || [];
736
+
737
+ let html = `<div style="margin-bottom:12px"><button class="btn-sm btn-primary" onclick="runPromotion('${esc(ctx)}')">Run Promotion</button></div>`;
738
+
356
739
  if (entries.length === 0) {
357
- el.innerHTML = '<p style="color:var(--text2)">No L3 knowledge entries</p>';
740
+ html += '<p style="color:var(--text2)">No L3 knowledge entries</p>';
741
+ el.innerHTML = html;
358
742
  return;
359
743
  }
360
- el.innerHTML = entries.map(e => `
361
- <div class="l3-entry" style="display:flex;justify-content:space-between;align-items:start;margin-bottom:8px">
362
- <div style="flex:1">${esc(e.content)}<br><span style="font-size:11px;color:var(--text2)">access: ${e.access_count} · density: ${e.connection_density?.toFixed(2)}</span></div>
363
- <button class="btn-sm btn-danger" onclick="deleteL3('${esc(e.id)}','${esc(ctx)}')">Delete</button>
744
+ html += entries.map(e => `
745
+ <div class="l3-entry" style="margin-bottom:8px;padding:10px" id="l3-${esc(e.id)}">
746
+ <div style="display:flex;justify-content:space-between;align-items:start">
747
+ <div style="flex:1" class="l3-content">${esc(e.content)}</div>
748
+ <div style="display:flex;gap:4px;margin-left:8px">
749
+ <button class="btn-sm" onclick="editL3('${esc(e.id)}','${esc(ctx)}')">Edit</button>
750
+ <button class="btn-sm btn-danger" onclick="deleteL3('${esc(e.id)}','${esc(ctx)}')">Delete</button>
751
+ </div>
752
+ </div>
753
+ <div style="font-size:11px;color:var(--text2);margin-top:4px">
754
+ source: ${esc(e.source_episode_id?.slice(0,8))} &middot; access: ${e.access_count} &middot; density: ${e.connection_density} &middot; promoted: ${e.promoted_at ? new Date(e.promoted_at).toLocaleString() : '?'}
755
+ </div>
364
756
  </div>
365
757
  `).join('');
758
+ el.innerHTML = html;
366
759
  } catch (err) {
367
760
  el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
368
761
  }
@@ -370,41 +763,390 @@ async function loadL3(ctx) {
370
763
 
371
764
  async function deleteL3(id, ctx) {
372
765
  if (!confirm('Delete this L3 entry?')) return;
373
- await fetch(`${API}/api/l3/${encodeURIComponent(id)}`, { method: 'DELETE' });
374
- loadL3(ctx);
766
+ try {
767
+ const res = await fetch(API + '/api/l3/' + encodeURIComponent(id), { method: 'DELETE' });
768
+ if (res.ok || res.status === 204) {
769
+ showNotification('L3 entry deleted', 'success');
770
+ loadL3(ctx);
771
+ } else {
772
+ const data = await res.json().catch(() => ({}));
773
+ showNotification('Delete failed: ' + (data.error || res.statusText), 'error');
774
+ }
775
+ } catch (err) {
776
+ showNotification('Delete failed: ' + err.message, 'error');
777
+ }
778
+ }
779
+
780
+ function editL3(id, ctx) {
781
+ const el = document.getElementById('l3-' + id);
782
+ const contentEl = el.querySelector('.l3-content');
783
+ const currentContent = contentEl.textContent;
784
+ contentEl.innerHTML = `
785
+ <textarea class="l3-edit-area" id="l3-edit-${id}">${esc(currentContent)}</textarea>
786
+ <div style="margin-top:6px;display:flex;gap:4px">
787
+ <button class="btn-sm btn-primary" onclick="saveL3('${esc(id)}','${esc(ctx)}')">Save</button>
788
+ <button class="btn-sm" onclick="loadL3('${esc(ctx)}')">Cancel</button>
789
+ </div>
790
+ `;
375
791
  }
376
792
 
793
+ async function saveL3(id, ctx) {
794
+ const textarea = document.getElementById('l3-edit-' + id);
795
+ const content = textarea.value;
796
+ try {
797
+ const res = await fetch(API + '/api/l3/' + encodeURIComponent(id), {
798
+ method: 'PATCH',
799
+ headers: { 'Content-Type': 'application/json' },
800
+ body: JSON.stringify({ content }),
801
+ });
802
+ if (res.ok || res.status === 204) {
803
+ showNotification('L3 entry updated', 'success');
804
+ loadL3(ctx);
805
+ } else {
806
+ showNotification('Update failed: ' + res.statusText, 'error');
807
+ }
808
+ } catch (err) {
809
+ showNotification('Update failed: ' + err.message, 'error');
810
+ }
811
+ }
812
+
813
+ async function runPromotion(ctx) {
814
+ try {
815
+ const res = await fetch(API + '/api/promotion/run?context=' + encodeURIComponent(ctx), { method: 'POST' });
816
+ if (res.ok) {
817
+ const data = await res.json();
818
+ showNotification(`Promotion complete: ${data.promoted_count} episodes promoted`, 'success');
819
+ loadL3(ctx);
820
+ } else {
821
+ showNotification('Promotion failed: ' + res.statusText, 'error');
822
+ }
823
+ } catch (err) {
824
+ showNotification('Promotion failed: ' + err.message, 'error');
825
+ }
826
+ }
827
+
828
+ // ----- L2 Episodes -----
377
829
  async function loadL2(ctx) {
378
830
  const el = document.getElementById('mem-l2');
831
+ l2SearchMode = false;
832
+ l2Page = 0;
379
833
  try {
380
- const res = await fetch(`${API}/api/contexts/${encodeURIComponent(ctx)}/episodes`);
834
+ const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/episodes');
835
+ if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load episodes</p>'; return; }
381
836
  const data = await res.json();
382
- const episodes = Array.isArray(data) ? data : (data.episodes || []);
383
- if (episodes.length === 0) {
384
- el.innerHTML = '<p style="color:var(--text2)">No episodes</p>';
837
+ l2AllEpisodes = Array.isArray(data) ? data : (data.episodes || []);
838
+ renderL2(ctx);
839
+ } catch (err) {
840
+ el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
841
+ }
842
+ }
843
+
844
+ function renderL2(ctx) {
845
+ const el = document.getElementById('mem-l2');
846
+ const start = l2Page * L2_PAGE_SIZE;
847
+ const pageEpisodes = l2AllEpisodes.slice(start, start + L2_PAGE_SIZE);
848
+ const totalPages = Math.ceil(l2AllEpisodes.length / L2_PAGE_SIZE);
849
+
850
+ let html = `
851
+ <div class="filters" style="margin-bottom:12px">
852
+ <input type="text" id="l2-search" placeholder="Semantic search..." style="flex:1" />
853
+ <button onclick="searchL2('${esc(ctx)}')">Search</button>
854
+ ${l2SearchMode ? `<button onclick="loadL2('${esc(ctx)}')">Clear Search</button>` : ''}
855
+ </div>
856
+ `;
857
+
858
+ if (l2AllEpisodes.length === 0) {
859
+ html += '<p style="color:var(--text2)">No episodes</p>';
860
+ el.innerHTML = html;
861
+ return;
862
+ }
863
+
864
+ html += pageEpisodes.map(ep => `
865
+ <div class="episode">
866
+ <div class="meta">
867
+ ${esc(ep.role)} &middot; ${new Date(ep.timestamp).toLocaleString()} &middot; ${esc(ep.id?.slice(0,8) || '')}
868
+ ${ep.score !== undefined ? ` &middot; <span class="score">score: ${ep.score.toFixed(4)}</span>` : ''}
869
+ </div>
870
+ ${esc(ep.content)}
871
+ </div>
872
+ `).join('');
873
+
874
+ if (totalPages > 1) {
875
+ html += `<div class="pagination">
876
+ <button ${l2Page === 0 ? 'disabled' : ''} onclick="l2Page--; renderL2('${esc(ctx)}')">Prev</button>
877
+ <span>Page ${l2Page + 1} of ${totalPages} (${l2AllEpisodes.length} total)</span>
878
+ <button ${l2Page >= totalPages - 1 ? 'disabled' : ''} onclick="l2Page++; renderL2('${esc(ctx)}')">Next</button>
879
+ </div>`;
880
+ }
881
+
882
+ el.innerHTML = html;
883
+ }
884
+
885
+ async function searchL2(ctx) {
886
+ const query = document.getElementById('l2-search')?.value;
887
+ if (!query?.trim()) return;
888
+ const el = document.getElementById('mem-l2');
889
+ try {
890
+ const res = await fetch(API + '/api/search?q=' + encodeURIComponent(query) + '&context=' + encodeURIComponent(ctx) + '&limit=50');
891
+ if (!res.ok) { showNotification('Search failed', 'error'); return; }
892
+ const data = await res.json();
893
+ l2AllEpisodes = (data.episodes || []).map(e => ({...e.episode || e, score: e.score}));
894
+ l2SearchMode = true;
895
+ l2Page = 0;
896
+ renderL2(ctx);
897
+ } catch (err) {
898
+ showNotification('Search failed: ' + err.message, 'error');
899
+ }
900
+ }
901
+
902
+ // ----- L1 History -----
903
+ async function loadL1(ctx) {
904
+ const el = document.getElementById('mem-l1');
905
+ try {
906
+ const res = await fetch(API + '/api/l1/' + encodeURIComponent(ctx));
907
+ if (!res.ok) { el.innerHTML = '<p style="color:var(--text2)">L1 data unavailable</p>'; return; }
908
+ const data = await res.json();
909
+ const messages = data.messages || [];
910
+ if (messages.length === 0) {
911
+ el.innerHTML = '<p style="color:var(--text2)">No L1 conversation history for this context</p>';
385
912
  return;
386
913
  }
387
- el.innerHTML = episodes.slice(0, 100).map(ep => `
388
- <div class="episode">
389
- <div class="meta">${esc(ep.role)} · ${new Date(ep.timestamp).toLocaleString()} · layer: ${ep.layer || 'L2'}</div>
390
- ${esc(ep.content)}
914
+ el.innerHTML = `<p style="color:var(--text2);margin-bottom:12px">${messages.length} messages in working memory</p>` +
915
+ messages.map(m => `
916
+ <div class="l1-msg ${esc(m.role)}">
917
+ <div class="role">${esc(m.role)}</div>
918
+ ${esc(m.content)}
919
+ </div>
920
+ `).join('');
921
+ } catch (err) {
922
+ el.innerHTML = `<p style="color:var(--text2)">L1 data unavailable</p>`;
923
+ }
924
+ }
925
+
926
+ // ===== Search View =====
927
+ async function loadSearchContexts() {
928
+ try {
929
+ const res = await fetch(API + '/api/contexts');
930
+ if (!res.ok) return;
931
+ const data = await res.json();
932
+ const sel = document.getElementById('search-scope');
933
+ sel.innerHTML = '<option value="all">All Contexts</option>';
934
+ for (const c of (data.contexts || [])) {
935
+ sel.innerHTML += `<option value="${esc(c.name)}">${esc(c.name)}</option>`;
936
+ }
937
+ } catch {}
938
+ }
939
+
940
+ async function runSearch() {
941
+ const query = document.getElementById('search-query').value.trim();
942
+ if (!query) return;
943
+ const scope = document.getElementById('search-scope').value;
944
+ const limit = document.getElementById('search-limit').value || 20;
945
+ const el = document.getElementById('search-results');
946
+ el.innerHTML = '<p style="color:var(--text2)">Searching...</p>';
947
+
948
+ try {
949
+ let url, res;
950
+ if (scope === 'all') {
951
+ url = API + '/api/search/cross-context?q=' + encodeURIComponent(query) + '&limit=' + limit;
952
+ res = await fetch(url);
953
+ if (!res.ok) { showNotification('Search failed', 'error'); el.innerHTML = ''; return; }
954
+ const data = await res.json();
955
+ const results = data.results || [];
956
+ if (results.length === 0) {
957
+ el.innerHTML = '<p style="color:var(--text2)">No results found</p>';
958
+ return;
959
+ }
960
+ let html = '';
961
+ for (const group of results) {
962
+ html += `<div class="search-group-header">${esc(group.context)} (${group.episodes.length})</div>`;
963
+ for (const ep of group.episodes) {
964
+ html += `<div class="search-result">
965
+ <div class="meta">${esc(ep.episode?.role || ep.role)} &middot; ${esc(group.context)} &middot; <span class="score">score: ${(ep.score || 0).toFixed(4)}</span> &middot; ${ep.episode?.timestamp ? new Date(ep.episode.timestamp).toLocaleString() : ''}</div>
966
+ <div class="content">${esc(ep.episode?.content || ep.content)}</div>
967
+ </div>`;
968
+ }
969
+ }
970
+ el.innerHTML = html;
971
+ } else {
972
+ url = API + '/api/search?q=' + encodeURIComponent(query) + '&context=' + encodeURIComponent(scope) + '&limit=' + limit;
973
+ res = await fetch(url);
974
+ if (!res.ok) { showNotification('Search failed', 'error'); el.innerHTML = ''; return; }
975
+ const data = await res.json();
976
+ const episodes = data.episodes || [];
977
+ if (episodes.length === 0) {
978
+ el.innerHTML = '<p style="color:var(--text2)">No results found</p>';
979
+ return;
980
+ }
981
+ el.innerHTML = episodes.map(ep => `
982
+ <div class="search-result">
983
+ <div class="meta">${esc(ep.episode?.role || ep.role)} &middot; ${esc(scope)} &middot; <span class="score">score: ${(ep.score || 0).toFixed(4)}</span> &middot; ${ep.episode?.timestamp ? new Date(ep.episode.timestamp).toLocaleString() : ''}</div>
984
+ <div class="content">${esc(ep.episode?.content || ep.content)}</div>
985
+ </div>
986
+ `).join('');
987
+ }
988
+ } catch (err) {
989
+ showNotification('Search failed: ' + err.message, 'error');
990
+ el.innerHTML = '';
991
+ }
992
+ }
993
+
994
+ // ===== Promotion & Scoring View =====
995
+ async function loadPromotion() {
996
+ loadThresholds();
997
+ }
998
+
999
+ async function loadThresholds() {
1000
+ const el = document.getElementById('promo-thresholds');
1001
+ try {
1002
+ const res = await fetch(API + '/api/stats');
1003
+ if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load stats</p>'; return; }
1004
+ const stats = await res.json();
1005
+ const t = stats.promotion_thresholds || { access: 5, cooccurrence: 3 };
1006
+ el.innerHTML = `
1007
+ <div class="status-grid" style="margin-bottom:24px">
1008
+ <div class="status-card">
1009
+ <div class="label">Access Threshold</div>
1010
+ <div class="value">${t.access}</div>
1011
+ <div class="sub">Episode must be accessed at least ${t.access} times</div>
1012
+ </div>
1013
+ <div class="status-card">
1014
+ <div class="label">Co-occurrence Threshold</div>
1015
+ <div class="value">${t.cooccurrence}</div>
1016
+ <div class="sub">Episode must co-occur with others at least ${t.cooccurrence} times</div>
1017
+ </div>
391
1018
  </div>
392
- `).join('');
393
- if (episodes.length > 100) {
394
- el.innerHTML += `<p style="color:var(--text2)">Showing 100 of ${episodes.length}</p>`;
1019
+ <h3 style="margin-bottom:12px">How Promotion Works</h3>
1020
+ <div style="color:var(--text2);font-size:13px;line-height:1.8">
1021
+ <p><strong>L2 &rarr; L3 promotion</strong> happens when an episode meets <em>both</em> thresholds:</p>
1022
+ <ol style="margin:8px 0 8px 20px">
1023
+ <li>The episode has been accessed (retrieved in search results) at least <strong>${t.access} times</strong></li>
1024
+ <li>The episode has co-occurred with other episodes at least <strong>${t.cooccurrence} times total</strong> (connection density)</li>
1025
+ </ol>
1026
+ <p>Once promoted, the episode's content is stored as long-term semantic knowledge (L3) and included in future system prompts for that context.</p>
1027
+ </div>
1028
+ `;
1029
+ } catch (err) {
1030
+ el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
1031
+ }
1032
+ }
1033
+
1034
+ async function loadCandidates() {
1035
+ const el = document.getElementById('promo-candidates');
1036
+ el.innerHTML = '<p style="color:var(--text2)">Loading candidates...</p>';
1037
+ try {
1038
+ const res = await fetch(API + '/api/access/top?limit=50');
1039
+ if (!res.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load candidates</p>'; return; }
1040
+ const data = await res.json();
1041
+ const records = data.records || [];
1042
+
1043
+ if (records.length === 0) {
1044
+ el.innerHTML = '<p style="color:var(--text2)">No access records found. Episodes need to be searched/retrieved to generate access counts.</p>';
1045
+ return;
395
1046
  }
1047
+
1048
+ el.innerHTML = records.map(r => {
1049
+ const accessPct = Math.min((r.access_count / 5) * 100, 100);
1050
+ const densityPct = Math.min((r.connection_density / 3) * 100, 100);
1051
+ const promoted = r.is_promoted;
1052
+ return `
1053
+ <div class="ctx-card">
1054
+ <div style="display:flex;justify-content:space-between;align-items:start">
1055
+ <div style="flex:1">
1056
+ <div style="font-size:12px;color:var(--text2)">${esc(r.context_name || '?')} &middot; ${esc(r.episode_id.slice(0, 12))}${promoted ? ' &middot; <span style="color:var(--green)">PROMOTED</span>' : ''}</div>
1057
+ <div style="font-size:13px;margin-top:4px">${esc(r.content_preview || '(no content)')}</div>
1058
+ </div>
1059
+ </div>
1060
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px">
1061
+ <div>
1062
+ <div style="font-size:11px;color:var(--text2);margin-bottom:2px">Access: ${r.access_count}/5</div>
1063
+ <div class="progress-bar"><div class="progress-fill ${accessPct >= 100 ? 'green' : 'yellow'}" style="width:${accessPct}%">${r.access_count}/5</div></div>
1064
+ </div>
1065
+ <div>
1066
+ <div style="font-size:11px;color:var(--text2);margin-bottom:2px">Density: ${r.connection_density}/3</div>
1067
+ <div class="progress-bar"><div class="progress-fill ${densityPct >= 100 ? 'green' : 'purple'}" style="width:${densityPct}%">${r.connection_density}/3</div></div>
1068
+ </div>
1069
+ </div>
1070
+ </div>
1071
+ `;
1072
+ }).join('');
396
1073
  } catch (err) {
397
1074
  el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
398
1075
  }
399
1076
  }
400
1077
 
1078
+ async function loadScoringTab() {
1079
+ const el = document.getElementById('promo-scoring');
1080
+ try {
1081
+ const ctxRes = await fetch(API + '/api/contexts');
1082
+ if (!ctxRes.ok) { el.innerHTML = '<p style="color:var(--red)">Failed to load contexts</p>'; return; }
1083
+ const ctxData = await ctxRes.json();
1084
+ const contexts = ctxData.contexts || [];
1085
+
1086
+ // Fetch scoring for each context
1087
+ const scoringData = {};
1088
+ let defaultHL = 48;
1089
+ for (const c of contexts) {
1090
+ try {
1091
+ const sRes = await fetch(API + '/api/contexts/' + encodeURIComponent(c.name) + '/scoring');
1092
+ if (sRes.ok) {
1093
+ const sd = await sRes.json();
1094
+ scoringData[c.name] = sd;
1095
+ defaultHL = sd.default_half_life_hours || 48;
1096
+ }
1097
+ } catch {}
1098
+ }
1099
+
1100
+ let html = `<p style="color:var(--text2);font-size:13px;margin-bottom:16px">Default half-life: <strong>${defaultHL}h</strong>. Settings reset when daemon restarts (in-memory only).</p>`;
1101
+
1102
+ html += '<table class="data-table"><thead><tr><th>Context</th><th>Half-Life (hours)</th><th>Custom?</th><th>Decay Curve</th><th>Actions</th></tr></thead><tbody>';
1103
+ for (const c of contexts) {
1104
+ const sd = scoringData[c.name] || {};
1105
+ const hl = sd.half_life_hours || defaultHL;
1106
+ const isCustom = sd.is_default === false;
1107
+ html += `<tr>
1108
+ <td style="color:var(--accent)">${esc(c.name)}</td>
1109
+ <td><input type="number" value="${hl}" min="1" max="8760" step="1" style="width:80px;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:4px;border-radius:4px" id="hl-${esc(c.name)}" /></td>
1110
+ <td>${isCustom ? '<span style="color:var(--yellow)">Yes</span>' : 'Default'}</td>
1111
+ <td>${renderDecayCurve(hl)}</td>
1112
+ <td><button class="btn-sm" onclick="saveScoring('${esc(c.name)}')">Save</button></td>
1113
+ </tr>`;
1114
+ }
1115
+ html += '</tbody></table>';
1116
+ el.innerHTML = html;
1117
+ } catch (err) {
1118
+ el.innerHTML = `<p style="color:var(--red)">Failed: ${esc(err.message)}</p>`;
1119
+ }
1120
+ }
1121
+
1122
+ async function saveScoring(ctx) {
1123
+ const input = document.getElementById('hl-' + ctx);
1124
+ const hours = parseFloat(input.value);
1125
+ if (!hours || hours <= 0) { showNotification('Invalid half-life value', 'warning'); return; }
1126
+ try {
1127
+ const res = await fetch(API + '/api/contexts/' + encodeURIComponent(ctx) + '/scoring', {
1128
+ method: 'POST',
1129
+ headers: { 'Content-Type': 'application/json' },
1130
+ body: JSON.stringify({ half_life_hours: hours }),
1131
+ });
1132
+ if (res.ok || res.status === 204) {
1133
+ showNotification(`Half-life for "${ctx}" set to ${hours}h`, 'success');
1134
+ } else {
1135
+ showNotification('Failed to save scoring', 'error');
1136
+ }
1137
+ } catch (err) {
1138
+ showNotification('Failed: ' + err.message, 'error');
1139
+ }
1140
+ }
1141
+
1142
+ // ===== Utility =====
401
1143
  function esc(s) {
402
1144
  if (!s) return '';
403
1145
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
404
1146
  }
405
1147
 
406
- // Auto-load
407
- loadRequests();
1148
+ // ===== Auto-load =====
1149
+ loadHealthView();
408
1150
  </script>
409
1151
  </body>
410
1152
  </html>