superlocalmemory 4.0.6 → 4.0.8
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/CHANGELOG.md +128 -4
- package/README.md +6 -11
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +14 -0
- package/src/superlocalmemory/cli/main.py +9 -0
- package/src/superlocalmemory/cli/summary_cmd.py +215 -0
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +26 -0
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +14 -3
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +212 -0
- package/src/superlocalmemory/code_graph/config.py +65 -1
- package/src/superlocalmemory/core/consolidation_engine.py +14 -15
- package/src/superlocalmemory/core/fact_consolidator.py +24 -1
- package/src/superlocalmemory/core/maintenance.py +51 -1
- package/src/superlocalmemory/core/recall_worker.py +4 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +16 -1
- package/src/superlocalmemory/hooks/hook_handlers.py +38 -11
- package/src/superlocalmemory/learning/pattern_miner.py +12 -7
- package/src/superlocalmemory/mcp/profiles.py +10 -3
- package/src/superlocalmemory/mcp/server.py +7 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +47 -3
- package/src/superlocalmemory/mcp/tools_summaries.py +147 -0
- package/src/superlocalmemory/server/consolidation_runner.py +140 -0
- package/src/superlocalmemory/server/recall_serializer.py +34 -2
- package/src/superlocalmemory/server/routes/agents.py +52 -8
- package/src/superlocalmemory/server/routes/brain.py +108 -0
- package/src/superlocalmemory/server/routes/memories.py +214 -0
- package/src/superlocalmemory/server/routes/v3_api.py +24 -46
- package/src/superlocalmemory/server/unified_daemon.py +107 -0
- package/src/superlocalmemory/storage/schema_code_graph.py +44 -1
- package/src/superlocalmemory/summaries/base.py +159 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +55 -8
- package/src/superlocalmemory/summaries/project_work_log.py +23 -7
- package/src/superlocalmemory/summaries/session_summary.py +9 -5
- package/src/superlocalmemory/ui/index.html +10 -4
- package/src/superlocalmemory/ui/js/fact-detail.js +61 -0
- package/src/superlocalmemory/ui/js/od-boundedloops.js +324 -0
- package/src/superlocalmemory/ui/js/od-memories.js +337 -12
- package/src/superlocalmemory/ui/js/od-mesh.js +97 -5
- package/src/superlocalmemory/ui/js/od-operations.js +1 -150
- package/src/superlocalmemory/ui/js/od-optimize.js +36 -9
- package/src/superlocalmemory/ui/js/od-shell.js +10 -0
|
@@ -96,7 +96,8 @@
|
|
|
96
96
|
|
|
97
97
|
var _st = {
|
|
98
98
|
rootId: null,
|
|
99
|
-
|
|
99
|
+
// Must match the pane carrying `active` in _scaffold(). Recall Lab leads.
|
|
100
|
+
activeTab: 'recall',
|
|
100
101
|
category: null, // null = all; string = filtered category
|
|
101
102
|
sort: 'created',
|
|
102
103
|
page: 0,
|
|
@@ -119,8 +120,11 @@
|
|
|
119
120
|
if (!container) return;
|
|
120
121
|
_injectCSS();
|
|
121
122
|
var id = 'od-mem-' + Math.random().toString(36).slice(2, 8);
|
|
123
|
+
// activeTab is reset explicitly: _scaffold always emits the Recall pane as the
|
|
124
|
+
// active one, so leaving a stale value here (from a tab click before the user
|
|
125
|
+
// navigated away and back) would desync state from what is actually on screen.
|
|
122
126
|
_st = Object.assign({}, _st, {
|
|
123
|
-
rootId: id, page: 0, category: null, sort: 'created',
|
|
127
|
+
rootId: id, page: 0, category: null, sort: 'created', activeTab: 'recall',
|
|
124
128
|
memories: [], catCounts: {}, cluLoaded: false, searchQ: null, requestSeq: 0,
|
|
125
129
|
});
|
|
126
130
|
// Clear per-id timeline cache for this render instance
|
|
@@ -141,31 +145,65 @@
|
|
|
141
145
|
'<h2>Everything you\'ve remembered</h2>' +
|
|
142
146
|
'<p id="' + id + '-sub" style="color:var(--fg-2);margin-top:5px">Loading…</p>' +
|
|
143
147
|
'</div>' +
|
|
148
|
+
// Tab order is deliberate (owner request, 4.0.8): the two tabs that ANSWER
|
|
149
|
+
// a question come first, the three that BROWSE data come after. Someone who
|
|
150
|
+
// opens this page usually wants "what do you know about X" or "what did I do
|
|
151
|
+
// today" — not a 3,600-row table sorted by insert time.
|
|
144
152
|
'<div class="tabs" id="' + id + '-tabs">' +
|
|
145
|
-
|
|
153
|
+
// Recall Lab — recall-lab.js is loaded in index.html and uses document-level
|
|
154
|
+
// click/keydown delegation keyed on the IDs below, so it survives re-renders.
|
|
155
|
+
'<button class="tab active" data-od-act="tab" data-tab="recall">Recall Lab</button>' +
|
|
156
|
+
'<button class="tab" data-od-act="tab" data-tab="summary">Summaries</button>' +
|
|
157
|
+
'<button class="tab" data-od-act="tab" data-tab="all">' +
|
|
146
158
|
'All memories <span class="cnt" id="' + id + '-cnt-all">…</span></button>' +
|
|
147
159
|
'<button class="tab" data-od-act="tab" data-tab="timeline">Creation timeline</button>' +
|
|
148
160
|
'<button class="tab" data-od-act="tab" data-tab="clusters">' +
|
|
149
161
|
'Knowledge clusters <span class="cnt" id="' + id + '-cnt-clusters">…</span></button>' +
|
|
150
|
-
// Recall Lab tab — restored: recall-lab.js is already loaded in index.html and uses
|
|
151
|
-
// document-level click/keydown delegation keyed on IDs below, so it survives re-renders.
|
|
152
|
-
'<button class="tab" data-od-act="tab" data-tab="recall">Recall Lab</button>' +
|
|
153
162
|
'</div>' +
|
|
154
|
-
'<div class="tabpane
|
|
163
|
+
'<div class="tabpane" id="' + id + '-pane-all">' + _allScaffold(id) + '</div>' +
|
|
155
164
|
'<div class="tabpane" id="' + id + '-pane-timeline">' + _tlScaffold(id) + '</div>' +
|
|
156
165
|
'<div class="tabpane" id="' + id + '-pane-clusters">' +
|
|
157
166
|
'<div class="launch-grid" id="' + id + '-clu-grid">' +
|
|
158
167
|
_loading('Loading clusters…') +
|
|
159
168
|
'</div>' +
|
|
160
169
|
'</div>' +
|
|
170
|
+
'<div class="tabpane" id="' + id + '-pane-summary" style="padding-top:12px">' +
|
|
171
|
+
'<p style="font-size:13px;color:var(--fg-2);margin-bottom:12px">'+
|
|
172
|
+
'A readable view of what you recorded. Every summary states how much '+
|
|
173
|
+
'of the underlying data it could actually cover.' +
|
|
174
|
+
'</p>' +
|
|
175
|
+
// "This project" used to be a button sending an empty target, which the
|
|
176
|
+
// API rejected every single time with "project requires target". It could
|
|
177
|
+
// not have worked: SLM runs as one global daemon and this is a browser
|
|
178
|
+
// tab — there is no current working directory to mean "this". The server
|
|
179
|
+
// lists the projects it has actually seen and the user picks one.
|
|
180
|
+
'<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px">' +
|
|
181
|
+
'<button class="tab" data-od-act="sum" data-kind="day" data-target="today">Today</button>' +
|
|
182
|
+
'<button class="tab" data-od-act="sum" data-kind="day" data-target="yesterday">Yesterday</button>' +
|
|
183
|
+
'<select id="' + id + '-sum-proj" data-od-act="sum-proj" ' +
|
|
184
|
+
'title="Projects SuperLocalMemory has recorded activity in" ' +
|
|
185
|
+
'style="padding:7px 10px;border:1px solid var(--border);' +
|
|
186
|
+
'border-radius:var(--r-md);background:var(--card-2);color:var(--fg);' +
|
|
187
|
+
'font-size:13px;max-width:340px">' +
|
|
188
|
+
'<option value="">Loading projects…</option>' +
|
|
189
|
+
'</select>' +
|
|
190
|
+
'</div>' +
|
|
191
|
+
'<div id="' + id + '-sum-out" style="font-size:13px;color:var(--fg-2)">Pick a summary above.</div>' +
|
|
192
|
+
'</div>' +
|
|
161
193
|
// Recall Lab pane — exact IDs required by recall-lab.js:
|
|
162
194
|
// #recall-lab-query (input), #recall-lab-search (button — click check),
|
|
163
195
|
// #recall-lab-per-page (select, optional), #recall-lab-meta, #recall-lab-results.
|
|
164
196
|
// Backend: POST /api/v3/recall/trace
|
|
165
|
-
|
|
197
|
+
//
|
|
198
|
+
// This is now the landing pane, so it has to explain itself to someone who
|
|
199
|
+
// has never heard the word "recall" in this sense. An empty search box as a
|
|
200
|
+
// first impression tells a non-technical user nothing about what SLM does.
|
|
201
|
+
'<div class="tabpane active" id="' + id + '-pane-recall" style="padding-top:12px">' +
|
|
166
202
|
'<div style="margin-bottom:14px">' +
|
|
167
203
|
'<p style="font-size:13px;color:var(--fg-2);margin-bottom:10px">' +
|
|
168
|
-
'
|
|
204
|
+
'Ask your memory a question and watch it answer. This shows you the ' +
|
|
205
|
+
'same result an AI assistant gets — plus <em>why</em> each memory was ' +
|
|
206
|
+
'chosen, how strongly it matched, and how long the lookup took.' +
|
|
169
207
|
'</p>' +
|
|
170
208
|
'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">' +
|
|
171
209
|
'<input id="recall-lab-query" placeholder="Enter recall query…" autocomplete="off" ' +
|
|
@@ -189,8 +227,11 @@
|
|
|
189
227
|
// #recall-lab-meta — written by recall-lab.js before results (timing, count, etc.)
|
|
190
228
|
'<div id="recall-lab-meta" ' +
|
|
191
229
|
'style="font-size:12px;color:var(--fg-2);margin-bottom:8px"></div>' +
|
|
192
|
-
// #recall-lab-results — written by recall-lab.js (result cards, pagination)
|
|
193
|
-
|
|
230
|
+
// #recall-lab-results — written by recall-lab.js (result cards, pagination).
|
|
231
|
+
// Seeded with a starter state because this pane loads first: a bare box
|
|
232
|
+
// gives a first-time user no idea what to type. recall-lab.js replaces
|
|
233
|
+
// the whole node on the first search, so this costs nothing afterwards.
|
|
234
|
+
'<div id="recall-lab-results">' + _recallStarter() + '</div>' +
|
|
194
235
|
'</div>' +
|
|
195
236
|
'</div>'
|
|
196
237
|
);
|
|
@@ -271,6 +312,43 @@
|
|
|
271
312
|
_esc(msg) + '</div>';
|
|
272
313
|
}
|
|
273
314
|
|
|
315
|
+
/* Starter state for Recall Lab.
|
|
316
|
+
*
|
|
317
|
+
* Recall Lab is the landing pane, so this is the first thing most people see in
|
|
318
|
+
* the dashboard. It has to answer "what is this and what do I type" without
|
|
319
|
+
* assuming the reader knows what a recall trace is.
|
|
320
|
+
*
|
|
321
|
+
* The examples are deliberately static. Seeding them from real memories was the
|
|
322
|
+
* first idea and it is worse: stored content is things like
|
|
323
|
+
* "[claude][SLM 4.0.8 CHECKPOINT] RELEASE STATE: local", which teaches exactly
|
|
324
|
+
* the wrong query shape. These teach the shape; the store answers.
|
|
325
|
+
*/
|
|
326
|
+
var RECALL_EGS = [
|
|
327
|
+
'what did I decide about the database',
|
|
328
|
+
'what am I working on',
|
|
329
|
+
'what problems have I hit before',
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
function _recallStarter() {
|
|
333
|
+
var chips = RECALL_EGS.map(function (q) {
|
|
334
|
+
return '<button class="tab" data-od-act="recall-eg" data-q="' + _esc(q) + '">' +
|
|
335
|
+
_esc(q) + '</button>';
|
|
336
|
+
}).join('');
|
|
337
|
+
return (
|
|
338
|
+
'<div id="od-recall-starter" style="border:1px dashed var(--border);' +
|
|
339
|
+
'border-radius:var(--r-md);padding:22px;color:var(--fg-2);font-size:13px">' +
|
|
340
|
+
'<div style="font-weight:600;color:var(--fg);margin-bottom:6px">' +
|
|
341
|
+
'Ask a question above to search your memory</div>' +
|
|
342
|
+
'<p style="margin:0 0 14px;line-height:1.55">' +
|
|
343
|
+
'It searches by meaning, not keywords — asking “what database are we using” ' +
|
|
344
|
+
'can surface a memory that says “switched to Postgres”, even though the two ' +
|
|
345
|
+
'share no words. Try one:' +
|
|
346
|
+
'</p>' +
|
|
347
|
+
'<div style="display:flex;gap:8px;flex-wrap:wrap">' + chips + '</div>' +
|
|
348
|
+
'</div>'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
274
352
|
// ── Tab switching ────────────────────────────────────────────────────────────
|
|
275
353
|
|
|
276
354
|
function _switchTab(id, tab) {
|
|
@@ -285,6 +363,139 @@
|
|
|
285
363
|
if (pane) pane.classList.add('active');
|
|
286
364
|
if (tab === 'timeline') _loadTimeline(id);
|
|
287
365
|
if (tab === 'clusters') _loadClusters(id);
|
|
366
|
+
if (tab === 'summary') _loadProjectOptions(id);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Populate the project picker from projects SLM has actually recorded.
|
|
370
|
+
*
|
|
371
|
+
* Loaded lazily on first visit to Summaries rather than at render time — the
|
|
372
|
+
* Memories page opens on Recall Lab, and most visits never reach this tab.
|
|
373
|
+
*/
|
|
374
|
+
function _loadProjectOptions(id) {
|
|
375
|
+
var sel = document.getElementById(id + '-sum-proj');
|
|
376
|
+
if (!sel) return;
|
|
377
|
+
// Guard on the element, not a module flag. A module-level "already loaded"
|
|
378
|
+
// boolean outlives the DOM it described: navigate away and back, the pane
|
|
379
|
+
// re-renders with a fresh <select> still reading "Loading projects…", and the
|
|
380
|
+
// stale flag suppresses the fetch that would fill it. Caught in review by
|
|
381
|
+
// exactly that sequence.
|
|
382
|
+
if (sel.dataset.loaded === '1' || sel.dataset.loading === '1') return;
|
|
383
|
+
sel.dataset.loading = '1';
|
|
384
|
+
fetch('/api/summary/projects')
|
|
385
|
+
.then(function (r) { return r.json(); })
|
|
386
|
+
.then(function (d) {
|
|
387
|
+
var list = (d && d.projects) || [];
|
|
388
|
+
sel.dataset.loaded = '1';
|
|
389
|
+
delete sel.dataset.loading;
|
|
390
|
+
if (!list.length) {
|
|
391
|
+
sel.innerHTML = '<option value="">No projects recorded yet</option>';
|
|
392
|
+
sel.disabled = true;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
sel.innerHTML =
|
|
396
|
+
'<option value="">Summarise a project…</option>' +
|
|
397
|
+
list.map(function (p) {
|
|
398
|
+
return '<option value="' + _esc(p.path) + '" title="' + _esc(p.path) + '">' +
|
|
399
|
+
_esc(p.label) + ' (' + p.events + ')</option>';
|
|
400
|
+
}).join('');
|
|
401
|
+
})
|
|
402
|
+
.catch(function () {
|
|
403
|
+
// Fail visibly but harmlessly: a silent empty dropdown reads as "you have
|
|
404
|
+
// no projects", which is a different and wrong statement. Clearing the
|
|
405
|
+
// in-flight marker leaves the next tab visit free to retry.
|
|
406
|
+
delete sel.dataset.loading;
|
|
407
|
+
sel.innerHTML = '<option value="">Could not load projects — retry</option>';
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Summaries pane (4.0.8, issue #113).
|
|
412
|
+
*
|
|
413
|
+
* Reads GET /api/summary. The generators shipped in 4.0.6 with no caller at
|
|
414
|
+
* all; 4.0.7 added the CLI; this is the surface for someone who is already
|
|
415
|
+
* looking at the dashboard rather than a terminal.
|
|
416
|
+
*
|
|
417
|
+
* Coverage is rendered on every result, never only on bad ones — a summary
|
|
418
|
+
* that hides how much of the data it saw is the failure mode #113 named.
|
|
419
|
+
*/
|
|
420
|
+
function _loadSummary(id, kind, target) {
|
|
421
|
+
var out = document.getElementById(id + '-sum-out');
|
|
422
|
+
if (!out) return;
|
|
423
|
+
out.textContent = 'Building summary…';
|
|
424
|
+
|
|
425
|
+
var q = '/api/summary?kind=' + encodeURIComponent(kind);
|
|
426
|
+
// The daemon resolves an empty target for "day" (defaults to today) but not
|
|
427
|
+
// for project or session, which need an explicit one from the picker.
|
|
428
|
+
if (target) q += '&target=' + encodeURIComponent(target);
|
|
429
|
+
|
|
430
|
+
fetch(q)
|
|
431
|
+
.then(function (r) {
|
|
432
|
+
if (!r.ok) return r.json().then(function (e) { throw new Error(e.detail || r.status); });
|
|
433
|
+
return r.json();
|
|
434
|
+
})
|
|
435
|
+
.then(function (d) {
|
|
436
|
+
out.textContent = '';
|
|
437
|
+
|
|
438
|
+
var body = document.createElement('pre');
|
|
439
|
+
body.style.cssText = 'white-space:pre-wrap;font-family:inherit;font-size:13px;' +
|
|
440
|
+
'line-height:1.65;color:var(--fg);background:var(--card-2);border:1px solid var(--border);' +
|
|
441
|
+
'border-radius:var(--r-md);padding:14px;margin:0';
|
|
442
|
+
body.textContent = d.summary || '(nothing recorded)';
|
|
443
|
+
out.appendChild(body);
|
|
444
|
+
|
|
445
|
+
var meta = document.createElement('div');
|
|
446
|
+
meta.style.cssText = 'margin-top:10px;font-size:12px;color:var(--fg-3);line-height:1.6';
|
|
447
|
+
meta.textContent = _summaryProvenance(d);
|
|
448
|
+
out.appendChild(meta);
|
|
449
|
+
})
|
|
450
|
+
.catch(function (e) {
|
|
451
|
+
out.textContent = 'Could not build summary: ' + (e && e.message ? e.message : 'error');
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Plain-English provenance line under a summary.
|
|
456
|
+
*
|
|
457
|
+
* The old line read "Built from 0 memories · coverage: full · method: llm_b".
|
|
458
|
+
* Three problems in one string: it contradicted itself (nothing, covered
|
|
459
|
+
* fully), and "coverage" and "llm_b" are internal vocabulary. Someone who has
|
|
460
|
+
* not read the source cannot tell whether that summary is trustworthy — which
|
|
461
|
+
* is the entire job of a provenance line.
|
|
462
|
+
*/
|
|
463
|
+
var _COVERAGE_TEXT = {
|
|
464
|
+
full: 'Covers everything recorded for this period.',
|
|
465
|
+
partial: 'Partial view — some of what was recorded is not reflected here.',
|
|
466
|
+
insufficient: 'Too little was recorded to summarise properly.',
|
|
467
|
+
no_session: 'That session has no memories attached to it.',
|
|
468
|
+
unavailable: 'The underlying data could not be read.',
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
var _METHOD_TEXT = {
|
|
472
|
+
extractive: 'Assembled directly from your own notes — no AI involved.',
|
|
473
|
+
llm_b: 'Written by the AI model running locally on this machine.',
|
|
474
|
+
llm_c: 'Written by your configured cloud AI model.',
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
function _summaryProvenance(d) {
|
|
478
|
+
var n = d.source_count || 0;
|
|
479
|
+
var md = d.metadata || {};
|
|
480
|
+
var parts = [];
|
|
481
|
+
|
|
482
|
+
if (n > 0) {
|
|
483
|
+
parts.push('Based on ' + n + ' memor' + (n === 1 ? 'y' : 'ies') + '.');
|
|
484
|
+
} else if (md.event_count) {
|
|
485
|
+
// A project can have plenty of recorded activity and no stored facts. Saying
|
|
486
|
+
// "0 memories" and stopping makes that look like an error rather than a
|
|
487
|
+
// description of what actually happened.
|
|
488
|
+
parts.push('Based on ' + md.event_count + ' recorded actions; no facts were ' +
|
|
489
|
+
'saved for this project.');
|
|
490
|
+
} else {
|
|
491
|
+
parts.push('No stored memories matched.');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
parts.push(_COVERAGE_TEXT[d.coverage] || 'Coverage unknown.');
|
|
495
|
+
if (d.generated_by && _METHOD_TEXT[d.generated_by]) {
|
|
496
|
+
parts.push(_METHOD_TEXT[d.generated_by]);
|
|
497
|
+
}
|
|
498
|
+
return parts.join(' ');
|
|
288
499
|
}
|
|
289
500
|
|
|
290
501
|
// ── Category counts (pre-fetch real totals) ──────────────────────────────────
|
|
@@ -752,6 +963,7 @@
|
|
|
752
963
|
'</div>' +
|
|
753
964
|
'<h3 style="font-size:13px;margin:20px 0 8px">Atomic facts</h3>' +
|
|
754
965
|
'<div id="od-drawer-facts" style="color:var(--fg-3);font-size:13px">Loading facts…</div>' +
|
|
966
|
+
'<div id="od-drawer-code"></div>' +
|
|
755
967
|
'<div style="display:flex;gap:8px;margin-top:20px;padding-top:12px;border-top:1px solid var(--border)">' +
|
|
756
968
|
'<button data-od-act="edit-mem" data-mid="' + _esc(mem.id) + '" style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--card-2);color:var(--fg);cursor:pointer;font-size:13px">Edit</button>' +
|
|
757
969
|
'<button data-od-act="del-mem" data-mid="' + _esc(mem.id) + '" style="padding:6px 14px;border:1px solid var(--danger);border-radius:6px;background:transparent;color:var(--danger);cursor:pointer;font-size:13px">Delete</button>' +
|
|
@@ -759,7 +971,28 @@
|
|
|
759
971
|
|
|
760
972
|
drawer.classList.add('open');
|
|
761
973
|
scrim.classList.add('on');
|
|
762
|
-
|
|
974
|
+
// A row in this pane is an ATOMIC FACT, not a source memory: /api/memories
|
|
975
|
+
// reports total 3603 on a store holding 830 memories and 3603 facts, and
|
|
976
|
+
// every row id resolves through /api/facts/{id}. The row carries its parent
|
|
977
|
+
// separately as memory_id.
|
|
978
|
+
//
|
|
979
|
+
// "Atomic facts" therefore needs the PARENT id — it lists the other facts
|
|
980
|
+
// extracted from the same source memory. Passing mem.id asked
|
|
981
|
+
// /api/memories/{fact_id}/facts, which has no children to return, so this
|
|
982
|
+
// section read "No atomic facts recorded for this memory" for every memory
|
|
983
|
+
// on every store. Not an empty state: the wrong identifier.
|
|
984
|
+
if (mem.memory_id) {
|
|
985
|
+
_loadFacts(mem.memory_id);
|
|
986
|
+
} else {
|
|
987
|
+
// No parent row: this record was stored directly as a fact rather than
|
|
988
|
+
// extracted from a longer memory (198 of 3,608 on a real store). Saying
|
|
989
|
+
// "no atomic facts recorded" there is wrong in a way that matters — it
|
|
990
|
+
// reads as data missing, when the fact IS the record.
|
|
991
|
+
var fx = document.getElementById('od-drawer-facts');
|
|
992
|
+
if (fx) fx.textContent = 'Stored directly — this record is itself the atomic fact.';
|
|
993
|
+
}
|
|
994
|
+
// Code links hang off the FACT, so this one takes the row id.
|
|
995
|
+
if (mem.id) _loadCodeLinks(mem.id);
|
|
763
996
|
}
|
|
764
997
|
|
|
765
998
|
function _closeDrawer() {
|
|
@@ -792,6 +1025,81 @@
|
|
|
792
1025
|
.catch(function () { if (el) el.textContent = 'Could not load facts.'; });
|
|
793
1026
|
}
|
|
794
1027
|
|
|
1028
|
+
/* Code this memory refers to (4.0.8).
|
|
1029
|
+
*
|
|
1030
|
+
* Renders into the memory drawer — the panel actually used to inspect a
|
|
1031
|
+
* memory. 4.0.7 put this in js/fact-detail.js, which only binds to
|
|
1032
|
+
* `.fact-result-item` rows in the search-results view, so the feature was
|
|
1033
|
+
* invisible to anyone browsing Memories.
|
|
1034
|
+
*
|
|
1035
|
+
* KEYED ON THE ROW'S OWN ID, which is an ATOMIC FACT id. The Memories pane
|
|
1036
|
+
* lists atomic facts — /api/memories returns total 3603 on a store with 830
|
|
1037
|
+
* memories and 3603 facts, and each row id resolves through /api/facts/{id}.
|
|
1038
|
+
* (That mismatch is also why the drawer's "Atomic facts" block always reads
|
|
1039
|
+
* "No atomic facts recorded": it asks /api/memories/{fact_id}/facts, which
|
|
1040
|
+
* has no children to return. Left alone here — separate defect, separate fix.)
|
|
1041
|
+
*
|
|
1042
|
+
* Reads /api/facts/{id}, which serves links from code_graph.db. Recall never
|
|
1043
|
+
* opens that database, so nothing here can affect retrieval.
|
|
1044
|
+
*
|
|
1045
|
+
* textContent throughout: every value is a qualified name or path read out of
|
|
1046
|
+
* an indexed repository, i.e. untrusted input as far as this panel goes.
|
|
1047
|
+
*/
|
|
1048
|
+
function _loadCodeLinks(factId) {
|
|
1049
|
+
var host = document.getElementById('od-drawer-code');
|
|
1050
|
+
if (!host || !factId) return;
|
|
1051
|
+
fetch('/api/facts/' + encodeURIComponent(factId))
|
|
1052
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
1053
|
+
.then(function (d) {
|
|
1054
|
+
var links = (d && d.code_links) || [];
|
|
1055
|
+
if (!links.length) return; // silent when there is nothing to show
|
|
1056
|
+
|
|
1057
|
+
var wrap = document.createElement('div');
|
|
1058
|
+
wrap.style.cssText = 'margin-top:6px;padding-top:6px;border-top:1px dashed var(--border)';
|
|
1059
|
+
|
|
1060
|
+
var label = document.createElement('div');
|
|
1061
|
+
label.style.cssText = 'font-size:11px;color:var(--fg-3);margin-bottom:4px';
|
|
1062
|
+
label.textContent = 'Code referenced (' + links.length + ')';
|
|
1063
|
+
wrap.appendChild(label);
|
|
1064
|
+
|
|
1065
|
+
links.slice(0, 6).forEach(function (l) {
|
|
1066
|
+
var row = document.createElement('div');
|
|
1067
|
+
row.style.cssText = 'font-size:12px;line-height:1.5';
|
|
1068
|
+
|
|
1069
|
+
var code = document.createElement('code');
|
|
1070
|
+
code.style.cssText = 'color:var(--violet)';
|
|
1071
|
+
code.textContent = l.qualified_name || l.name || '?';
|
|
1072
|
+
row.appendChild(code);
|
|
1073
|
+
|
|
1074
|
+
if (l.kind) {
|
|
1075
|
+
var kind = document.createElement('span');
|
|
1076
|
+
kind.style.cssText = 'color:var(--fg-3);font-size:10px;margin-left:6px';
|
|
1077
|
+
kind.textContent = l.kind;
|
|
1078
|
+
row.appendChild(kind);
|
|
1079
|
+
}
|
|
1080
|
+
// A stale link points at code that has moved or gone. Saying so is the
|
|
1081
|
+
// entire value of tracking staleness.
|
|
1082
|
+
if (l.is_stale) {
|
|
1083
|
+
var stale = document.createElement('span');
|
|
1084
|
+
stale.className = 'badge';
|
|
1085
|
+
stale.style.cssText = 'font-size:9px;margin-left:6px';
|
|
1086
|
+
stale.textContent = 'code changed';
|
|
1087
|
+
row.appendChild(stale);
|
|
1088
|
+
}
|
|
1089
|
+
wrap.appendChild(row);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
if (links.length > 6) {
|
|
1093
|
+
var more = document.createElement('div');
|
|
1094
|
+
more.style.cssText = 'font-size:11px;color:var(--fg-3);margin-top:2px';
|
|
1095
|
+
more.textContent = '… and ' + (links.length - 6) + ' more';
|
|
1096
|
+
wrap.appendChild(more);
|
|
1097
|
+
}
|
|
1098
|
+
host.appendChild(wrap);
|
|
1099
|
+
})
|
|
1100
|
+
.catch(function () { /* display extra: never surface an error here */ });
|
|
1101
|
+
}
|
|
1102
|
+
|
|
795
1103
|
function _startEdit(mid) { // show inline edit form in drawer
|
|
796
1104
|
var m = _st.memories.filter(function (x) { return x.id === mid; })[0];
|
|
797
1105
|
var el = document.getElementById('od-mem-act'); if (!el || !m) return;
|
|
@@ -809,6 +1117,20 @@
|
|
|
809
1117
|
if (!el) return;
|
|
810
1118
|
var act = el.dataset.odAct;
|
|
811
1119
|
|
|
1120
|
+
if (act === 'sum') {
|
|
1121
|
+
_loadSummary(id, el.dataset.kind, el.dataset.target || '');
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
// Example query chip in the Recall Lab starter state. recall-lab.js owns the
|
|
1125
|
+
// search itself and listens on #recall-lab-search, so fill the box and let it
|
|
1126
|
+
// run — duplicating its fetch here would be a second, divergent code path.
|
|
1127
|
+
if (act === 'recall-eg') {
|
|
1128
|
+
var qBox = document.getElementById('recall-lab-query');
|
|
1129
|
+
var qBtn = document.getElementById('recall-lab-search');
|
|
1130
|
+
if (qBox) qBox.value = el.dataset.q || '';
|
|
1131
|
+
if (qBtn) qBtn.click();
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
812
1134
|
if (act === 'tab') {
|
|
813
1135
|
_switchTab(id, el.dataset.tab);
|
|
814
1136
|
return;
|
|
@@ -893,6 +1215,9 @@
|
|
|
893
1215
|
var wq = (_st.searchQ || '').trim();
|
|
894
1216
|
if (wq) _doSearch(id, wq); // re-run active search with the new window
|
|
895
1217
|
}
|
|
1218
|
+
if (e.target.dataset.odAct === 'sum-proj' && e.target.value) {
|
|
1219
|
+
_loadSummary(id, 'project', e.target.value);
|
|
1220
|
+
}
|
|
896
1221
|
});
|
|
897
1222
|
container.addEventListener('input', function (e) {
|
|
898
1223
|
if (e.target.dataset.odAct === 'search') _doSearch(id, e.target.value.trim());
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
// Two-column: peer cards (left) + conversation panel (right)
|
|
136
136
|
'<div style="display:grid;grid-template-columns:1.15fr 1fr;gap:20px;align-items:start">' +
|
|
137
137
|
|
|
138
|
-
// LEFT — peer card grid
|
|
138
|
+
// LEFT — peer card grid + recent activity
|
|
139
139
|
'<div>' +
|
|
140
140
|
'<div class="launch-grid" id="od-mesh-peers-grid"' +
|
|
141
141
|
' style="grid-template-columns:1fr 1fr">' +
|
|
@@ -144,6 +144,27 @@
|
|
|
144
144
|
'<div style="font-size:15px;margin-bottom:6px">Loading peers…</div>' +
|
|
145
145
|
'</div>' +
|
|
146
146
|
'</div>' +
|
|
147
|
+
|
|
148
|
+
// Recent activity, from GET /mesh/events.
|
|
149
|
+
//
|
|
150
|
+
// The pane used to render live peers and nothing else. Peers
|
|
151
|
+
// deregister when their session ends, so between sessions it showed
|
|
152
|
+
// "No mesh sessions connected" and threw away every registration
|
|
153
|
+
// that had ever happened — on this store, six of them, four from
|
|
154
|
+
// bounded-loops. That reads as "the mesh has never worked".
|
|
155
|
+
//
|
|
156
|
+
// Live peers answer "who can I message right now". This answers
|
|
157
|
+
// "has the mesh ever done anything", which is the question someone
|
|
158
|
+
// looking at an empty grid is actually asking.
|
|
159
|
+
'<div class="card" id="od-mesh-activity-card" style="margin-top:16px">' +
|
|
160
|
+
'<div class="card-head">' +
|
|
161
|
+
'<h3>Recent mesh activity</h3>' +
|
|
162
|
+
'<span class="sub" id="od-mesh-activity-sub">sessions that have joined</span>' +
|
|
163
|
+
'</div>' +
|
|
164
|
+
'<div class="card-pad" id="od-mesh-activity">' +
|
|
165
|
+
'<div style="color:var(--fg-2);font-size:13px">Loading activity…</div>' +
|
|
166
|
+
'</div>' +
|
|
167
|
+
'</div>' +
|
|
147
168
|
'</div>' +
|
|
148
169
|
|
|
149
170
|
// RIGHT — conversation panel (sticky, full-height card)
|
|
@@ -240,11 +261,13 @@
|
|
|
240
261
|
'justify-content:center;margin-bottom:16px;color:var(--fg-3)">' +
|
|
241
262
|
meshEmptyIcon() +
|
|
242
263
|
'</div>' +
|
|
243
|
-
'<h3 style="font-size:15px;margin-bottom:6px">No
|
|
264
|
+
'<h3 style="font-size:15px;margin-bottom:6px">No sessions connected right now</h3>' +
|
|
244
265
|
'<p style="font-size:12.5px;color:var(--fg-2);' +
|
|
245
|
-
'max-width:
|
|
246
|
-
'
|
|
247
|
-
'
|
|
266
|
+
'max-width:38ch;line-height:1.55">' +
|
|
267
|
+
'Messaging needs a peer that is currently live — sessions leave the mesh ' +
|
|
268
|
+
'when they end, so this is empty between sessions rather than broken. ' +
|
|
269
|
+
'Past sessions are listed below. Start another agent session and it will ' +
|
|
270
|
+
'appear here automatically.' +
|
|
248
271
|
'</p>' +
|
|
249
272
|
'</div>';
|
|
250
273
|
return;
|
|
@@ -454,6 +477,75 @@
|
|
|
454
477
|
selectPeerById(null);
|
|
455
478
|
}
|
|
456
479
|
});
|
|
480
|
+
|
|
481
|
+
meshFetch('/mesh/events')
|
|
482
|
+
.then(function (r) { return r.ok ? r.json() : { events: [] }; })
|
|
483
|
+
.catch(function () { return { events: [] }; })
|
|
484
|
+
.then(function (d) {
|
|
485
|
+
if (_requestSeq !== requestSeq) return;
|
|
486
|
+
renderActivity(Array.isArray(d && d.events) ? d.events : []);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* Recent mesh activity from the event log.
|
|
491
|
+
*
|
|
492
|
+
* Grouped by project rather than listed raw: a session that registers,
|
|
493
|
+
* heartbeats and leaves produces several events, and twenty rows describing
|
|
494
|
+
* four projects is less informative than four rows. Read-only — this never
|
|
495
|
+
* writes, so it cannot disturb a live mesh.
|
|
496
|
+
*/
|
|
497
|
+
function renderActivity(events) {
|
|
498
|
+
var box = document.getElementById('od-mesh-activity');
|
|
499
|
+
var sub = document.getElementById('od-mesh-activity-sub');
|
|
500
|
+
if (!box) return;
|
|
501
|
+
|
|
502
|
+
if (!events.length) {
|
|
503
|
+
box.innerHTML = '<div style="color:var(--fg-2);font-size:13px;line-height:1.6">' +
|
|
504
|
+
'No mesh activity recorded yet. Sessions join the mesh automatically when ' +
|
|
505
|
+
'an agent calls the <code>mesh_summary</code> tool — nothing to configure.' +
|
|
506
|
+
'</div>';
|
|
507
|
+
if (sub) sub.textContent = 'no history';
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
var byProject = {};
|
|
512
|
+
events.forEach(function (e) {
|
|
513
|
+
var payload = {};
|
|
514
|
+
try { payload = JSON.parse(e.payload || '{}'); } catch (err) { payload = {}; }
|
|
515
|
+
var key = payload.project_path || '(unknown project)';
|
|
516
|
+
var cur = byProject[key];
|
|
517
|
+
if (!cur) {
|
|
518
|
+
byProject[key] = { path: key, count: 1, last: e.created_at, type: e.event_type };
|
|
519
|
+
} else {
|
|
520
|
+
cur.count += 1;
|
|
521
|
+
if (String(e.created_at) > String(cur.last)) {
|
|
522
|
+
cur.last = e.created_at;
|
|
523
|
+
cur.type = e.event_type;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
var rows = Object.keys(byProject).map(function (k) { return byProject[k]; });
|
|
529
|
+
rows.sort(function (a, b) { return String(b.last).localeCompare(String(a.last)); });
|
|
530
|
+
|
|
531
|
+
if (sub) {
|
|
532
|
+
sub.textContent = events.length + ' event' + (events.length === 1 ? '' : 's') +
|
|
533
|
+
' · ' + rows.length + ' project' + (rows.length === 1 ? '' : 's');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
box.innerHTML = rows.slice(0, 12).map(function (r) {
|
|
537
|
+
// Full paths are long and share prefixes; the tail identifies the project.
|
|
538
|
+
var parts = String(r.path).split('/').filter(Boolean);
|
|
539
|
+
var label = parts.length ? parts.slice(-2).join('/') : r.path;
|
|
540
|
+
return '<div class="list-row" style="align-items:baseline">' +
|
|
541
|
+
'<span class="mono" style="flex:1;font-size:12.5px" title="' + escapeHtml(r.path) + '">' +
|
|
542
|
+
escapeHtml(label) + '</span>' +
|
|
543
|
+
'<span style="font-size:12px;color:var(--fg-2);margin:0 10px">' +
|
|
544
|
+
escapeHtml(String(r.type || '').replace(/_/g, ' ')) + '</span>' +
|
|
545
|
+
'<span style="font-size:12px;color:var(--fg-3)">' +
|
|
546
|
+
escapeHtml(timeAgo(r.last)) + '</span>' +
|
|
547
|
+
'</div>';
|
|
548
|
+
}).join('');
|
|
457
549
|
}
|
|
458
550
|
|
|
459
551
|
// ── Event wiring (CSP-safe delegation — no onXxx= attributes) ─────────────
|