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.
Files changed (63) hide show
  1. package/CHANGELOG.md +128 -4
  2. package/README.md +6 -11
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/pyproject.toml +1 -1
  25. package/src/superlocalmemory/__init__.py +1 -1
  26. package/src/superlocalmemory/cli/commands.py +14 -0
  27. package/src/superlocalmemory/cli/main.py +9 -0
  28. package/src/superlocalmemory/cli/summary_cmd.py +215 -0
  29. package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +26 -0
  30. package/src/superlocalmemory/code_graph/bridge/event_listeners.py +14 -3
  31. package/src/superlocalmemory/code_graph/bridge/maintenance.py +212 -0
  32. package/src/superlocalmemory/code_graph/config.py +65 -1
  33. package/src/superlocalmemory/core/consolidation_engine.py +14 -15
  34. package/src/superlocalmemory/core/fact_consolidator.py +24 -1
  35. package/src/superlocalmemory/core/maintenance.py +51 -1
  36. package/src/superlocalmemory/core/recall_worker.py +4 -0
  37. package/src/superlocalmemory/evolution/skill_evolver.py +16 -1
  38. package/src/superlocalmemory/hooks/hook_handlers.py +38 -11
  39. package/src/superlocalmemory/learning/pattern_miner.py +12 -7
  40. package/src/superlocalmemory/mcp/profiles.py +10 -3
  41. package/src/superlocalmemory/mcp/server.py +7 -0
  42. package/src/superlocalmemory/mcp/tools_code_graph.py +47 -3
  43. package/src/superlocalmemory/mcp/tools_summaries.py +147 -0
  44. package/src/superlocalmemory/server/consolidation_runner.py +140 -0
  45. package/src/superlocalmemory/server/recall_serializer.py +34 -2
  46. package/src/superlocalmemory/server/routes/agents.py +52 -8
  47. package/src/superlocalmemory/server/routes/brain.py +108 -0
  48. package/src/superlocalmemory/server/routes/memories.py +214 -0
  49. package/src/superlocalmemory/server/routes/v3_api.py +24 -46
  50. package/src/superlocalmemory/server/unified_daemon.py +107 -0
  51. package/src/superlocalmemory/storage/schema_code_graph.py +44 -1
  52. package/src/superlocalmemory/summaries/base.py +159 -0
  53. package/src/superlocalmemory/summaries/daily_reflection.py +55 -8
  54. package/src/superlocalmemory/summaries/project_work_log.py +23 -7
  55. package/src/superlocalmemory/summaries/session_summary.py +9 -5
  56. package/src/superlocalmemory/ui/index.html +10 -4
  57. package/src/superlocalmemory/ui/js/fact-detail.js +61 -0
  58. package/src/superlocalmemory/ui/js/od-boundedloops.js +324 -0
  59. package/src/superlocalmemory/ui/js/od-memories.js +337 -12
  60. package/src/superlocalmemory/ui/js/od-mesh.js +97 -5
  61. package/src/superlocalmemory/ui/js/od-operations.js +1 -150
  62. package/src/superlocalmemory/ui/js/od-optimize.js +36 -9
  63. 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
- activeTab: 'all',
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
- '<button class="tab active" data-od-act="tab" data-tab="all">' +
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 active" id="' + id + '-pane-all">' + _allScaffold(id) + '</div>' +
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
- '<div class="tabpane" id="' + id + '-pane-recall" style="padding-top:12px">' +
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
- 'Trace how a recall query is resolved matching algorithm, scoring, and final result set.' +
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
- '<div id="recall-lab-results"></div>' +
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
- if (mem.id) _loadFacts(mem.id);
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 mesh sessions connected</h3>' +
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:30ch;line-height:1.5">' +
246
- 'Start another agent session or connect a trusted LAN peer. Sessions register via the' +
247
- ' <code>mesh_summary</code> MCP tool.' +
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) ─────────────