@joshuaswarren/openclaw-engram 9.0.64 → 9.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -298,7 +298,7 @@ openclaw engram policy-status # Lifecycle policy snapshot
298
298
 
299
299
  Governance runs write durable artifacts under `{memoryDir}/state/memory-governance/runs/<runId>/`, including `summary.json`, `review-queue.json`, `kept-memories.json`, `applied-actions.json`, `metrics.json`, `manifest.json`, `report.md`, and `restore.json` for apply runs.
300
300
 
301
- When the local HTTP access server is running, Engram also serves a lightweight operator UI shell at `http://127.0.0.1:4318/engram/ui/`. Paste the same bearer token into the console UI and it will use the authenticated `/engram/v1/...` endpoints for memory browsing, recall inspection, governance review, entity exploration, and maintenance status.
301
+ When the local HTTP access server is running, Engram also serves a lightweight operator UI shell at `http://127.0.0.1:4318/engram/ui/`. Paste the same bearer token into the console UI and it will use the authenticated `/engram/v1/...` endpoints for memory browsing, recall inspection, quality review, governance review, entity exploration, and maintenance status. The packaged build now ships the admin console assets with `dist/`, so the UI is available from installed plugin bundles as well as a source checkout.
302
302
 
303
303
  ## Configuration
304
304
 
@@ -0,0 +1,499 @@
1
+ const tokenKey = "engram.adminConsole.token";
2
+ const browserState = {
3
+ sort: "updated_desc",
4
+ limit: 25,
5
+ offset: 0,
6
+ total: 0,
7
+ };
8
+
9
+ function $(id) {
10
+ return document.getElementById(id);
11
+ }
12
+
13
+ function readToken() {
14
+ return window.sessionStorage.getItem(tokenKey) || "";
15
+ }
16
+
17
+ function writeToken(token) {
18
+ if (token) {
19
+ window.sessionStorage.setItem(tokenKey, token);
20
+ } else {
21
+ window.sessionStorage.removeItem(tokenKey);
22
+ }
23
+ }
24
+
25
+ function setStatus(id, message, tone = "default") {
26
+ const el = $(id);
27
+ if (!el) return;
28
+ el.textContent = message;
29
+ el.className = tone === "default" ? "status" : `status ${tone}`;
30
+ }
31
+
32
+ function clearChildren(el) {
33
+ if (!el) return;
34
+ while (el.firstChild) {
35
+ el.removeChild(el.firstChild);
36
+ }
37
+ }
38
+
39
+ function appendPill(container, value) {
40
+ if (!container || !value) return;
41
+ const pill = document.createElement("span");
42
+ pill.className = "pill";
43
+ pill.textContent = value;
44
+ container.appendChild(pill);
45
+ }
46
+
47
+ function renderEmptyState(container, message) {
48
+ clearChildren(container);
49
+ if (!container) return;
50
+ const item = document.createElement("div");
51
+ item.className = "item";
52
+ const strong = document.createElement("strong");
53
+ strong.textContent = message;
54
+ item.appendChild(strong);
55
+ container.appendChild(item);
56
+ }
57
+
58
+ function createItem() {
59
+ const article = document.createElement("article");
60
+ article.className = "item";
61
+ return article;
62
+ }
63
+
64
+ async function fetchJson(url, options = {}) {
65
+ const token = readToken();
66
+ const headers = new Headers(options.headers || {});
67
+ if (token) headers.set("Authorization", `Bearer ${token}`);
68
+ if (options.body && !headers.has("Content-Type")) {
69
+ headers.set("Content-Type", "application/json");
70
+ }
71
+ const response = await fetch(url, { ...options, headers });
72
+ const text = await response.text();
73
+ let payload = {};
74
+ try {
75
+ payload = text ? JSON.parse(text) : {};
76
+ } catch {
77
+ payload = { raw: text };
78
+ }
79
+ if (!response.ok) {
80
+ const error = new Error(payload.error || `HTTP ${response.status}`);
81
+ error.payload = payload;
82
+ throw error;
83
+ }
84
+ return payload;
85
+ }
86
+
87
+ function syncBrowserControls() {
88
+ const prevButton = $("memoryPrevButton");
89
+ const nextButton = $("memoryNextButton");
90
+ if (prevButton) prevButton.disabled = browserState.offset <= 0;
91
+ if (nextButton) nextButton.disabled = browserState.offset + browserState.limit >= browserState.total;
92
+
93
+ const pageStatus = $("memoryPageStatus");
94
+ if (!pageStatus) return;
95
+ if (browserState.total === 0) {
96
+ pageStatus.textContent = "No results";
97
+ return;
98
+ }
99
+ const pageOffset = Math.min(
100
+ browserState.offset,
101
+ Math.max(0, browserState.total - 1),
102
+ );
103
+ const start = pageOffset + 1;
104
+ const end = Math.min(pageOffset + browserState.limit, browserState.total);
105
+ pageStatus.textContent = `${start}-${end} of ${browserState.total}`;
106
+ }
107
+
108
+ function readMemoryPageSize() {
109
+ return Number.parseInt($("memoryPageSize")?.value || String(browserState.limit || 25), 10) || 25;
110
+ }
111
+
112
+ function stepMemoryPage(direction) {
113
+ const pageSize = readMemoryPageSize();
114
+ browserState.limit = pageSize;
115
+ browserState.offset = Math.max(0, browserState.offset + direction * pageSize);
116
+ }
117
+
118
+ function renderMemoryList(memories) {
119
+ const list = $("memoryList");
120
+ if (!list) return;
121
+ if (!Array.isArray(memories) || memories.length === 0) {
122
+ renderEmptyState(list, "No memories matched.");
123
+ return;
124
+ }
125
+ clearChildren(list);
126
+ memories.forEach((memory) => {
127
+ const article = createItem();
128
+ const meta = document.createElement("div");
129
+ meta.className = "meta";
130
+ appendPill(meta, memory.category);
131
+ appendPill(meta, memory.status);
132
+ appendPill(meta, memory.entityRef);
133
+ article.appendChild(meta);
134
+
135
+ const heading = document.createElement("h3");
136
+ heading.style.marginTop = "10px";
137
+ heading.textContent = memory.id;
138
+ article.appendChild(heading);
139
+
140
+ const pathText = document.createElement("div");
141
+ pathText.className = "status";
142
+ pathText.textContent = memory.path;
143
+ article.appendChild(pathText);
144
+
145
+ const preview = document.createElement("p");
146
+ preview.textContent = memory.preview;
147
+ article.appendChild(preview);
148
+
149
+ const button = document.createElement("button");
150
+ button.className = "memory-open-button";
151
+ button.dataset.memoryId = memory.id;
152
+ button.textContent = "Open Memory";
153
+ button.addEventListener("click", () => void loadMemoryDetail(memory.id));
154
+ article.appendChild(button);
155
+
156
+ list.appendChild(article);
157
+ });
158
+ }
159
+
160
+ function renderReviewQueue(response) {
161
+ const list = $("reviewQueueList");
162
+ if (!list) return;
163
+ if (!response?.found || !Array.isArray(response.reviewQueue) || response.reviewQueue.length === 0) {
164
+ renderEmptyState(list, "No review queue entries found.");
165
+ return;
166
+ }
167
+ clearChildren(list);
168
+ response.reviewQueue.forEach((entry) => {
169
+ const article = createItem();
170
+ const meta = document.createElement("div");
171
+ meta.className = "meta";
172
+ appendPill(meta, entry.reasonCode);
173
+ appendPill(meta, entry.severity);
174
+ appendPill(
175
+ meta,
176
+ entry.suggestedAction ? `${entry.suggestedAction}${entry.suggestedStatus ? `:${entry.suggestedStatus}` : ""}` : "",
177
+ );
178
+ article.appendChild(meta);
179
+
180
+ const heading = document.createElement("h3");
181
+ heading.style.marginTop = "10px";
182
+ heading.textContent = entry.memoryId;
183
+ article.appendChild(heading);
184
+
185
+ const pathText = document.createElement("div");
186
+ pathText.className = "status";
187
+ pathText.textContent = entry.path || "";
188
+ article.appendChild(pathText);
189
+
190
+ const toolbar = document.createElement("div");
191
+ toolbar.className = "toolbar";
192
+ toolbar.style.marginTop = "12px";
193
+
194
+ const inspectButton = document.createElement("button");
195
+ inspectButton.className = "secondary queue-open-button";
196
+ inspectButton.dataset.memoryId = entry.memoryId;
197
+ inspectButton.textContent = "Inspect";
198
+ inspectButton.addEventListener("click", () => void loadMemoryDetail(entry.memoryId));
199
+ toolbar.appendChild(inspectButton);
200
+
201
+ [
202
+ ["accent", "active", "Confirm"],
203
+ ["secondary", "rejected", "Reject"],
204
+ ["warn", "archived", "Archive"],
205
+ ].forEach(([className, nextStatus, label]) => {
206
+ const button = document.createElement("button");
207
+ button.className = `${className} queue-disposition-button`;
208
+ button.dataset.memoryId = entry.memoryId;
209
+ button.dataset.status = nextStatus;
210
+ button.textContent = label;
211
+ button.addEventListener("click", () => void applyDisposition(entry.memoryId, nextStatus));
212
+ toolbar.appendChild(button);
213
+ });
214
+
215
+ article.appendChild(toolbar);
216
+ list.appendChild(article);
217
+ });
218
+ }
219
+
220
+ function renderEntityList(entities) {
221
+ const list = $("entityList");
222
+ if (!list) return;
223
+ if (!Array.isArray(entities) || entities.length === 0) {
224
+ renderEmptyState(list, "No entities matched.");
225
+ return;
226
+ }
227
+ clearChildren(list);
228
+ entities.forEach((entity) => {
229
+ const article = createItem();
230
+ const meta = document.createElement("div");
231
+ meta.className = "meta";
232
+ appendPill(meta, entity.type);
233
+ (entity.aliases || []).forEach((alias) => appendPill(meta, alias));
234
+ article.appendChild(meta);
235
+
236
+ const heading = document.createElement("h3");
237
+ heading.style.marginTop = "10px";
238
+ heading.textContent = entity.name;
239
+ article.appendChild(heading);
240
+
241
+ const summary = document.createElement("div");
242
+ summary.className = "status";
243
+ summary.textContent = entity.summary || "No summary.";
244
+ article.appendChild(summary);
245
+
246
+ const button = document.createElement("button");
247
+ button.className = "entity-open-button";
248
+ button.dataset.entityName = entity.name;
249
+ button.textContent = "Open Entity";
250
+ button.addEventListener("click", () => void loadEntityDetail(entity.name));
251
+ article.appendChild(button);
252
+
253
+ list.appendChild(article);
254
+ });
255
+ }
256
+
257
+ function renderQuality(response) {
258
+ const summary = $("qualitySummary");
259
+ if (!summary) return;
260
+ clearChildren(summary);
261
+ const cards = [
262
+ ["Memories", String(response.totalMemories ?? 0)],
263
+ ["Pending Review", String(response.archivePressure?.pendingReview ?? 0)],
264
+ ["Archived", String(response.archivePressure?.archived ?? 0)],
265
+ ["Quality Score", typeof response.latestGovernanceRun?.qualityScore?.score === "number"
266
+ ? String(response.latestGovernanceRun.qualityScore.score)
267
+ : "n/a"],
268
+ ];
269
+ cards.forEach(([label, value]) => {
270
+ const card = document.createElement("div");
271
+ card.className = "quality-stat";
272
+ const strong = document.createElement("strong");
273
+ strong.textContent = value;
274
+ card.appendChild(strong);
275
+ const caption = document.createElement("div");
276
+ caption.className = "status";
277
+ caption.textContent = label;
278
+ card.appendChild(caption);
279
+ summary.appendChild(card);
280
+ });
281
+ const qualityJson = $("qualityJson");
282
+ if (qualityJson) {
283
+ qualityJson.textContent = JSON.stringify(response, null, 2);
284
+ }
285
+ }
286
+
287
+ async function loadMemoryBrowser(resetOffset = false) {
288
+ if (resetOffset) browserState.offset = 0;
289
+ browserState.sort = $("memorySort")?.value || "updated_desc";
290
+ browserState.limit = readMemoryPageSize();
291
+ setStatus("memoryBrowserStatus", "Loading memory browser...");
292
+ const params = new URLSearchParams();
293
+ const query = $("memoryQuery")?.value?.trim();
294
+ const status = $("memoryStatus")?.value?.trim();
295
+ const category = $("memoryCategory")?.value?.trim();
296
+ if (query) params.set("q", query);
297
+ if (status) params.set("status", status);
298
+ if (category) params.set("category", category);
299
+ params.set("sort", browserState.sort);
300
+ params.set("limit", String(browserState.limit));
301
+ params.set("offset", String(browserState.offset));
302
+ const response = await fetchJson(`/engram/v1/memories?${params.toString()}`);
303
+ browserState.total = response.total || 0;
304
+ const maxOffset = browserState.total > 0
305
+ ? Math.floor((browserState.total - 1) / browserState.limit) * browserState.limit
306
+ : 0;
307
+ if (!resetOffset && browserState.offset > maxOffset) {
308
+ browserState.offset = maxOffset;
309
+ return loadMemoryBrowser(false);
310
+ }
311
+ renderMemoryList(response.memories);
312
+ syncBrowserControls();
313
+ setStatus("memoryBrowserStatus", `Loaded ${response.count} of ${response.total} memories.`, "ok");
314
+ }
315
+
316
+ async function loadMemoryDetail(memoryId) {
317
+ if (!memoryId) return;
318
+ setStatus("memoryDetailStatus", `Loading ${memoryId}...`);
319
+ const [memory, timeline] = await Promise.all([
320
+ fetchJson(`/engram/v1/memories/${encodeURIComponent(memoryId)}`),
321
+ fetchJson(`/engram/v1/memories/${encodeURIComponent(memoryId)}/timeline?limit=50`),
322
+ ]);
323
+ $("memoryContent").textContent = JSON.stringify(memory.memory, null, 2);
324
+ $("memoryTimeline").textContent = JSON.stringify(timeline.timeline, null, 2);
325
+ $("memoryRawPath").value = memory.memory?.path || "";
326
+ const meta = $("memoryDetailMeta");
327
+ clearChildren(meta);
328
+ appendPill(meta, memory.memory.category);
329
+ appendPill(meta, memory.memory.status || "active");
330
+ appendPill(meta, memory.memory.path);
331
+ setStatus("memoryDetailStatus", `Loaded ${memoryId}.`, "ok");
332
+ }
333
+
334
+ async function runRecallDebugger() {
335
+ const query = $("recallQuery")?.value?.trim() || "";
336
+ const sessionKey = $("recallSessionKey")?.value?.trim() || "admin-console";
337
+ setStatus("recallStatus", "Running recall...");
338
+ const recall = await fetchJson("/engram/v1/recall", {
339
+ method: "POST",
340
+ body: JSON.stringify({ query, sessionKey }),
341
+ });
342
+ const explain = await fetchJson("/engram/v1/recall/explain", {
343
+ method: "POST",
344
+ body: JSON.stringify({ sessionKey }),
345
+ });
346
+ $("recallContext").textContent = JSON.stringify(recall, null, 2);
347
+ $("recallExplain").textContent = JSON.stringify(explain, null, 2);
348
+ setStatus("recallStatus", `Recall completed for ${sessionKey}.`, "ok");
349
+ }
350
+
351
+ async function loadReviewQueue() {
352
+ setStatus("reviewQueueStatus", "Loading latest governance review queue...");
353
+ const response = await fetchJson("/engram/v1/review-queue");
354
+ renderReviewQueue(response);
355
+ setStatus(
356
+ "reviewQueueStatus",
357
+ response?.found
358
+ ? `Loaded run ${response.runId} with ${response.reviewQueue.length} queue entries.`
359
+ : "No governance review queue artifacts found.",
360
+ response?.found ? "ok" : "default",
361
+ );
362
+ }
363
+
364
+ async function applyDisposition(memoryId, status) {
365
+ if (!memoryId || !status) return;
366
+ setStatus("reviewQueueStatus", `Applying ${status} to ${memoryId}...`);
367
+ await fetchJson("/engram/v1/review-disposition", {
368
+ method: "POST",
369
+ body: JSON.stringify({
370
+ memoryId,
371
+ status,
372
+ reasonCode: status === "active" ? "operator_confirmed" : "operator_review",
373
+ }),
374
+ });
375
+ await Promise.all([
376
+ loadReviewQueue(),
377
+ loadMemoryBrowser(),
378
+ loadMemoryDetail(memoryId).catch(() => {}),
379
+ loadQuality(),
380
+ loadMaintenance(),
381
+ ]);
382
+ setStatus("reviewQueueStatus", `Applied ${status} to ${memoryId}.`, "ok");
383
+ }
384
+
385
+ async function loadEntities() {
386
+ setStatus("entityStatus", "Loading entities...");
387
+ const params = new URLSearchParams();
388
+ const query = $("entityQuery")?.value?.trim();
389
+ if (query) params.set("q", query);
390
+ const response = await fetchJson(`/engram/v1/entities?${params.toString()}`);
391
+ renderEntityList(response.entities);
392
+ setStatus("entityStatus", `Loaded ${response.count} of ${response.total} entities.`, "ok");
393
+ }
394
+
395
+ async function loadEntityDetail(name) {
396
+ if (!name) return;
397
+ const response = await fetchJson(`/engram/v1/entities/${encodeURIComponent(name)}`);
398
+ $("entityDetail").textContent = JSON.stringify(response.entity, null, 2);
399
+ }
400
+
401
+ async function loadQuality() {
402
+ setStatus("qualityStatus", "Loading quality dashboard...");
403
+ const response = await fetchJson("/engram/v1/quality");
404
+ renderQuality(response);
405
+ setStatus(
406
+ "qualityStatus",
407
+ response.latestGovernanceRun?.found
408
+ ? `Loaded quality summary for ${response.totalMemories} memories and governance run ${response.latestGovernanceRun.runId}.`
409
+ : `Loaded quality summary for ${response.totalMemories} memories.`,
410
+ "ok",
411
+ );
412
+ }
413
+
414
+ async function loadMaintenance() {
415
+ setStatus("maintenanceStatus", "Loading maintenance summary...");
416
+ const response = await fetchJson("/engram/v1/maintenance");
417
+ $("maintenanceJson").textContent = JSON.stringify(response, null, 2);
418
+ setStatus("maintenanceStatus", "Maintenance summary loaded.", "ok");
419
+ }
420
+
421
+ async function connectAndBootstrap() {
422
+ const input = $("tokenInput");
423
+ const token = input?.value?.trim() || readToken();
424
+ if (!token) {
425
+ setStatus("authStatus", "Enter a bearer token first.", "error");
426
+ return;
427
+ }
428
+ writeToken(token);
429
+ if (input) input.value = token;
430
+ setStatus("authStatus", "Connecting...", "default");
431
+ try {
432
+ await fetchJson("/engram/v1/health");
433
+ setStatus("authStatus", "Connected to Engram access API.", "ok");
434
+ await Promise.allSettled([
435
+ loadMemoryBrowser(true),
436
+ loadReviewQueue(),
437
+ loadEntities(),
438
+ loadQuality(),
439
+ loadMaintenance(),
440
+ ]);
441
+ } catch (error) {
442
+ setStatus("authStatus", error.message || String(error), "error");
443
+ }
444
+ }
445
+
446
+ function copyMemoryPath() {
447
+ const rawPathField = $("memoryRawPath");
448
+ const value = rawPathField?.value?.trim();
449
+ if (!value) {
450
+ setStatus("memoryDetailStatus", "No memory path to copy.", "error");
451
+ return;
452
+ }
453
+ if (!navigator.clipboard?.writeText) {
454
+ setStatus("memoryDetailStatus", "Clipboard API is unavailable in this browser.", "error");
455
+ return;
456
+ }
457
+ navigator.clipboard.writeText(value)
458
+ .then(() => {
459
+ setStatus("memoryDetailStatus", "Copied raw memory path.", "ok");
460
+ })
461
+ .catch((error) => {
462
+ setStatus("memoryDetailStatus", error.message || String(error), "error");
463
+ });
464
+ }
465
+
466
+ function bootstrap() {
467
+ const remembered = readToken();
468
+ if (remembered && $("tokenInput")) {
469
+ $("tokenInput").value = remembered;
470
+ }
471
+
472
+ $("connectButton")?.addEventListener("click", () => void connectAndBootstrap());
473
+ $("clearTokenButton")?.addEventListener("click", () => {
474
+ writeToken("");
475
+ if ($("tokenInput")) $("tokenInput").value = "";
476
+ setStatus("authStatus", "Cleared stored token.", "default");
477
+ });
478
+ $("searchMemoriesButton")?.addEventListener("click", () => void loadMemoryBrowser(true));
479
+ $("memoryPrevButton")?.addEventListener("click", () => {
480
+ stepMemoryPage(-1);
481
+ void loadMemoryBrowser(false);
482
+ });
483
+ $("memoryNextButton")?.addEventListener("click", () => {
484
+ stepMemoryPage(1);
485
+ void loadMemoryBrowser(false);
486
+ });
487
+ $("runRecallButton")?.addEventListener("click", () => void runRecallDebugger());
488
+ $("refreshQueueButton")?.addEventListener("click", () => void loadReviewQueue());
489
+ $("searchEntitiesButton")?.addEventListener("click", () => void loadEntities());
490
+ $("copyMemoryPathButton")?.addEventListener("click", copyMemoryPath);
491
+
492
+ if (remembered) {
493
+ void connectAndBootstrap();
494
+ } else {
495
+ syncBrowserControls();
496
+ }
497
+ }
498
+
499
+ bootstrap();