@jungjaehoon/mama-os 0.63.0 → 0.64.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 (100) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/api/attachment-actions.d.ts.map +1 -1
  4. package/dist/api/attachment-actions.js +6 -2
  5. package/dist/api/attachment-actions.js.map +1 -1
  6. package/dist/api/drive-actions.d.ts +2 -1
  7. package/dist/api/drive-actions.d.ts.map +1 -1
  8. package/dist/api/drive-actions.js +1 -0
  9. package/dist/api/drive-actions.js.map +1 -1
  10. package/dist/api/drive-delivery.d.ts +16 -0
  11. package/dist/api/drive-delivery.d.ts.map +1 -0
  12. package/dist/api/drive-delivery.js +190 -0
  13. package/dist/api/drive-delivery.js.map +1 -0
  14. package/dist/api/file-delivery.d.ts +2 -2
  15. package/dist/api/file-delivery.d.ts.map +1 -1
  16. package/dist/api/file-delivery.js +5 -5
  17. package/dist/api/file-delivery.js.map +1 -1
  18. package/dist/api/security-events.d.ts +5 -4
  19. package/dist/api/security-events.d.ts.map +1 -1
  20. package/dist/api/security-events.js +25 -3
  21. package/dist/api/security-events.js.map +1 -1
  22. package/dist/api/viewer-data.d.ts +5 -1
  23. package/dist/api/viewer-data.d.ts.map +1 -1
  24. package/dist/api/viewer-server.d.ts +1 -1
  25. package/dist/api/viewer-server.d.ts.map +1 -1
  26. package/dist/api/viewer-server.js +82 -17
  27. package/dist/api/viewer-server.js.map +1 -1
  28. package/dist/api/wiki-actions.d.ts.map +1 -1
  29. package/dist/api/wiki-actions.js +44 -0
  30. package/dist/api/wiki-actions.js.map +1 -1
  31. package/dist/cli/commands/daemon.d.ts.map +1 -1
  32. package/dist/cli/commands/daemon.js +14 -7
  33. package/dist/cli/commands/daemon.js.map +1 -1
  34. package/dist/gateways/discord.d.ts.map +1 -1
  35. package/dist/gateways/discord.js +1 -1
  36. package/dist/gateways/discord.js.map +1 -1
  37. package/dist/gateways/slack.d.ts.map +1 -1
  38. package/dist/gateways/slack.js +1 -1
  39. package/dist/gateways/slack.js.map +1 -1
  40. package/dist/gateways/telegram.d.ts.map +1 -1
  41. package/dist/gateways/telegram.js +1 -1
  42. package/dist/gateways/telegram.js.map +1 -1
  43. package/dist/replay/replay-source-catalog.js +1 -1
  44. package/dist/replay/replay-source-catalog.js.map +1 -1
  45. package/dist/runtime/action-surface.d.ts +4 -1
  46. package/dist/runtime/action-surface.d.ts.map +1 -1
  47. package/dist/runtime/action-surface.js +16 -1
  48. package/dist/runtime/action-surface.js.map +1 -1
  49. package/dist/runtime/config.d.ts +20 -0
  50. package/dist/runtime/config.d.ts.map +1 -1
  51. package/dist/runtime/config.js +48 -3
  52. package/dist/runtime/config.js.map +1 -1
  53. package/dist/runtime/outbound-attempts.d.ts +4 -2
  54. package/dist/runtime/outbound-attempts.d.ts.map +1 -1
  55. package/dist/runtime/outbound-attempts.js +17 -1
  56. package/dist/runtime/outbound-attempts.js.map +1 -1
  57. package/dist/runtime/owner-runtime.d.ts +6 -1
  58. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  59. package/dist/runtime/owner-runtime.js +4 -1
  60. package/dist/runtime/owner-runtime.js.map +1 -1
  61. package/dist/runtime/owner-system-prompt.d.ts +1 -1
  62. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  63. package/dist/runtime/owner-system-prompt.js +10 -4
  64. package/dist/runtime/owner-system-prompt.js.map +1 -1
  65. package/dist/runtime/report-scheduler.d.ts +2 -1
  66. package/dist/runtime/report-scheduler.d.ts.map +1 -1
  67. package/dist/runtime/report-scheduler.js +1 -1
  68. package/dist/runtime/report-scheduler.js.map +1 -1
  69. package/dist/runtime/stimulus-delivery.js +1 -1
  70. package/dist/runtime/stimulus-delivery.js.map +1 -1
  71. package/dist/runtime/turn-orders.js +1 -1
  72. package/dist/runtime/turn-orders.js.map +1 -1
  73. package/dist/wiki/obsidian-writer.d.ts +0 -3
  74. package/dist/wiki/obsidian-writer.d.ts.map +1 -1
  75. package/dist/wiki/obsidian-writer.js +8 -108
  76. package/dist/wiki/obsidian-writer.js.map +1 -1
  77. package/dist/wiki/path-safety.d.ts +1 -1
  78. package/dist/wiki/path-safety.d.ts.map +1 -1
  79. package/dist/wiki/path-safety.js +2 -2
  80. package/dist/wiki/path-safety.js.map +1 -1
  81. package/dist/wiki/wiki-move.d.ts +12 -0
  82. package/dist/wiki/wiki-move.d.ts.map +1 -0
  83. package/dist/wiki/wiki-move.js +95 -0
  84. package/dist/wiki/wiki-move.js.map +1 -0
  85. package/dist/wiki/wiki-read.d.ts +7 -0
  86. package/dist/wiki/wiki-read.d.ts.map +1 -1
  87. package/dist/wiki/wiki-read.js +38 -17
  88. package/dist/wiki/wiki-read.js.map +1 -1
  89. package/package.json +2 -2
  90. package/public/viewer/js/modules/graph.js +177 -115
  91. package/public/viewer/js/modules/memory-timeline.js +6 -5
  92. package/public/viewer/js/modules/wiki.js +4 -4
  93. package/public/viewer/js/utils/api.js +3 -3
  94. package/public/viewer/src/modules/graph.ts +205 -136
  95. package/public/viewer/src/modules/memory-timeline.ts +11 -3
  96. package/public/viewer/src/modules/wiki.ts +4 -4
  97. package/public/viewer/src/types/global.d.ts +1 -1
  98. package/public/viewer/src/utils/api.ts +8 -3
  99. package/public/viewer/viewer.css +2 -2
  100. package/public/viewer/viewer.html +12 -12
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Graph Module - one memory record and what it links to
2
+ * Graph Module - the filtered memory as a graph
3
3
  * @module modules/graph
4
4
  *
5
- * The memory view picks a record from the saved timeline. This draws that record with its direct
6
- * links (the source messages it rests on, what it builds on, what it replaced), names every colour
7
- * and line in plain words, and shows the detail of whichever dot is clicked.
5
+ * The memory view's list and graph show the same records: whatever the period, kind and search
6
+ * select. One dot per record, one per work item for its updates in the period, and faded dots for
7
+ * the records they link to outside the filter. Lines are the links between records; the source
8
+ * messages a record rests on are listed in its detail.
8
9
  */
9
10
  /* eslint-env browser */
10
11
  /* global vis */
@@ -12,8 +13,9 @@ import { escapeHtml, getElementByIdOrNull } from '../utils/dom.js';
12
13
  import { DebugLogger } from '../utils/debug-logger.js';
13
14
  import { API } from '../utils/api.js';
14
15
  import { renderSafeMarkdown } from '../utils/markdown.js';
16
+ import { groupInfo } from './memory-timeline.js';
15
17
  const logger = new DebugLogger('Graph');
16
- /** What each dot colour stands for, in the words the legend shows. */
18
+ /** What a record's stored kind is called in the detail panel. */
17
19
  export const KIND_LABELS = {
18
20
  decision: 'Decision',
19
21
  preference: 'Preference',
@@ -42,33 +44,107 @@ export const RELATION_LABELS = {
42
44
  next_action_for: 'next step for',
43
45
  blocks: 'blocks',
44
46
  };
47
+ const OUTSIDE_COLOR = '#D1D5DB';
45
48
  const LABEL_LENGTH = 40;
46
49
  function shortLabel(text) {
47
50
  const flat = text.replace(/\s+/g, ' ').trim();
48
51
  return flat.length > LABEL_LENGTH ? `${flat.slice(0, LABEL_LENGTH - 1)}…` : flat;
49
52
  }
53
+ /**
54
+ * The graph of what the filters selected. A work item is one dot for all its updates; a link
55
+ * between two updates of one item is not drawn, and a link that touches nothing selected is left
56
+ * out. Days arrive newest first, so an item's first update seen is its latest.
57
+ */
58
+ export function composeFilterGraph(timeline, links) {
59
+ const nodes = new Map();
60
+ const recordNode = {};
61
+ for (const day of timeline.days) {
62
+ for (const group of day.groups) {
63
+ for (const record of group.records ?? []) {
64
+ nodes.set(record.id, {
65
+ id: record.id,
66
+ group: group.group,
67
+ kind: record.kind,
68
+ label: record.topic,
69
+ detailId: record.id,
70
+ updates: 0,
71
+ outside: false,
72
+ });
73
+ recordNode[record.id] = record.id;
74
+ }
75
+ for (const item of group.items ?? []) {
76
+ const id = `item:${item.commitmentId}`;
77
+ const node = nodes.get(id) ?? {
78
+ id,
79
+ group: 'work',
80
+ kind: 'commitment',
81
+ label: item.title ?? item.topic,
82
+ detailId: item.revisions[0].id,
83
+ updates: 0,
84
+ outside: false,
85
+ };
86
+ node.updates += item.revisions.length;
87
+ nodes.set(id, node);
88
+ for (const revision of item.revisions)
89
+ recordNode[revision.id] = id;
90
+ }
91
+ }
92
+ }
93
+ // Links name both ends with a node; a work update outside the filter is drawn as its item.
94
+ const dotFor = (ref) => {
95
+ const listed = recordNode[ref];
96
+ if (listed !== undefined)
97
+ return listed;
98
+ const commitmentId = links.nodes[ref].commitmentId;
99
+ return commitmentId === null ? ref : `item:${commitmentId}`;
100
+ };
101
+ const edges = [];
102
+ const seen = new Set();
103
+ const outside = [];
104
+ for (const link of links.edges) {
105
+ const from = dotFor(link.from);
106
+ const to = dotFor(link.to);
107
+ const key = `${from}\0${to}\0${link.relation}`;
108
+ if (from === to || seen.has(key) || (!nodes.has(from) && !nodes.has(to)))
109
+ continue;
110
+ seen.add(key);
111
+ edges.push({ from, to, relation: link.relation });
112
+ for (const [dot, ref] of [
113
+ [from, link.from],
114
+ [to, link.to],
115
+ ]) {
116
+ if (nodes.has(dot))
117
+ continue;
118
+ const node = {
119
+ id: dot,
120
+ group: 'outside',
121
+ kind: links.nodes[ref].kind,
122
+ label: links.nodes[ref].label,
123
+ detailId: ref,
124
+ updates: 0,
125
+ outside: true,
126
+ };
127
+ nodes.set(dot, node);
128
+ outside.push(node);
129
+ }
130
+ }
131
+ return {
132
+ nodes: [...nodes.values()].filter((node) => !node.outside).concat(outside),
133
+ edges,
134
+ recordNode,
135
+ };
136
+ }
50
137
  /**
51
138
  * Graph Module Class
52
139
  */
53
140
  export class GraphModule {
54
141
  network = null;
55
- graphData = { nodes: [], edges: [], meta: {} };
56
- centerId = null;
57
- // A slower earlier read must not draw or describe a record the owner has moved away from.
58
- recordRequest = 0;
142
+ graph = { nodes: [], edges: [], recordNode: {} };
143
+ // The links between records do not depend on the filter: read once per page load.
144
+ links = null;
145
+ // A slower earlier read must not draw or describe what the owner has moved away from.
146
+ drawRequest = 0;
59
147
  detailRequest = 0;
60
- nodeColors = {
61
- decision: '#60A5FA',
62
- preference: '#C084FC',
63
- constraint: '#FB7185',
64
- lesson: '#FBBF24',
65
- workflow: '#F472B6',
66
- fact: '#2DD4BF',
67
- commitment: '#FB923C',
68
- observation: '#94A3B8',
69
- memory: '#CBD5E1', // Legacy memories may have no stored classification.
70
- registry: '#A3E635',
71
- };
72
148
  edgeStyles = {
73
149
  supersedes: { color: '#666666', dashes: false, width: 2 },
74
150
  mentions: { color: '#6B4C9A', dashes: false, width: 2 },
@@ -81,74 +157,74 @@ export class GraphModule {
81
157
  synthesizes: { color: '#6B4C9A', width: 3, dashes: false },
82
158
  };
83
159
  /**
84
- * Draw a record and its direct links, then show its detail.
160
+ * Draw what the filters selected, from the same timeline answer the list shows.
85
161
  */
86
- async showRecord(id) {
87
- const request = ++this.recordRequest;
88
- const empty = getElementByIdOrNull('graph-empty');
89
- const loading = getElementByIdOrNull('graph-loading');
90
- if (empty)
91
- empty.style.display = 'none';
92
- if (loading)
93
- loading.style.display = 'flex';
94
- try {
95
- const data = (await API.getGraphNeighbors(id));
96
- if (request !== this.recordRequest)
97
- return;
98
- this.centerId = id;
99
- this.init(data);
100
- const center = data.nodes.find((node) => String(node.id) === id);
101
- if (center)
102
- void this.showDetail(center);
103
- }
104
- finally {
105
- if (loading && request === this.recordRequest)
106
- loading.style.display = 'none';
107
- }
162
+ async showSelection(timeline) {
163
+ const request = ++this.drawRequest;
164
+ // A failed read is not kept: the next filter change reads the links again.
165
+ this.links ??= API.getMemoryLinks().catch((error) => {
166
+ this.links = null;
167
+ throw error;
168
+ });
169
+ const links = await this.links;
170
+ if (request !== this.drawRequest)
171
+ return;
172
+ this.init(composeFilterGraph(timeline, links));
108
173
  }
109
174
  /**
110
- * Initialize vis-network over one record's neighbourhood.
175
+ * A listed record was picked: select its dot and open the record's own detail.
111
176
  */
112
- init(data) {
177
+ focusRecord(recordId) {
178
+ const dot = this.graph.recordNode[recordId];
179
+ const node = this.graph.nodes.find((candidate) => candidate.id === dot);
180
+ if (this.network && dot !== undefined) {
181
+ this.network.selectNodes([dot]);
182
+ this.network.focus(dot, { scale: 1.2, animation: { duration: 400 } });
183
+ }
184
+ void this.showDetail({
185
+ detailId: recordId,
186
+ label: node?.label ?? recordId,
187
+ kind: node?.kind ?? null,
188
+ });
189
+ }
190
+ init(graph) {
113
191
  const container = getElementByIdOrNull('graph-canvas');
114
192
  if (!container) {
115
193
  logger.error('graph-canvas element not found');
116
194
  return;
117
195
  }
118
- this.graphData = data;
196
+ this.graph = graph;
119
197
  this.network?.destroy?.();
120
- const nodes = data.nodes.map((n) => {
121
- const kind = n.kind ?? 'memory';
122
- const center = String(n.id) === this.centerId;
123
- const color = this.getNodeColor(kind);
198
+ const empty = getElementByIdOrNull('graph-empty');
199
+ if (empty) {
200
+ empty.style.display = graph.nodes.length === 0 ? '' : 'none';
201
+ empty.textContent = 'Nothing saved for this filter.';
202
+ }
203
+ const nodes = graph.nodes.map((node) => {
204
+ const color = node.outside ? OUTSIDE_COLOR : groupInfo(node.group).color;
124
205
  return {
125
- id: n.id,
126
- // Source messages stay unlabelled dots: their time and text are on hover and click.
127
- label: kind === 'observation' && !center
128
- ? undefined
129
- : shortLabel(String(n.decision_preview ?? n.decision ?? n.topic ?? n.id)),
130
- title: this.createNodeTooltip(n),
206
+ id: node.id,
207
+ label: shortLabel(node.label),
208
+ title: this.createNodeTooltip(node),
131
209
  color: {
132
210
  background: color,
133
- border: center ? '#131313' : color,
211
+ border: color,
134
212
  highlight: { background: color, border: '#131313' },
135
213
  },
136
- size: center ? 22 : kind === 'observation' ? 9 : 14,
137
- borderWidth: center ? 3 : 1,
138
- font: { color: '#131313', size: center ? 15 : 13 },
139
- data: n,
214
+ size: node.outside ? 7 : node.updates > 0 ? 10 + Math.min(node.updates, 20) / 2 : 11,
215
+ font: { color: node.outside ? '#9CA3AF' : '#131313', size: 12 },
140
216
  };
141
217
  });
142
- const edges = data.edges.map((e) => {
143
- const style = this.getEdgeStyle(e.relationship);
218
+ const edges = graph.edges.map((edge) => {
219
+ const style = this.getEdgeStyle(edge.relation);
144
220
  return {
145
- from: e.from,
146
- to: e.to,
221
+ from: edge.from,
222
+ to: edge.to,
147
223
  arrows: { to: { enabled: true, scaleFactor: 0.5 } },
148
224
  color: style.color,
149
225
  dashes: style.dashes,
150
226
  width: style.width,
151
- title: this.relationLabel(e.relationship),
227
+ title: this.relationLabel(edge.relation),
152
228
  };
153
229
  });
154
230
  this.network = new vis.Network(container, { nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges) }, {
@@ -157,79 +233,63 @@ export class GraphModule {
157
233
  physics: {
158
234
  enabled: true,
159
235
  barnesHut: {
160
- gravitationalConstant: -6000,
161
- centralGravity: 0.4,
162
- springLength: 140,
236
+ gravitationalConstant: -4000,
237
+ centralGravity: 0.5,
238
+ springLength: 120,
163
239
  springConstant: 0.04,
164
240
  damping: 0.09,
165
- avoidOverlap: 1,
241
+ avoidOverlap: 0.6,
166
242
  },
167
243
  stabilization: { enabled: true, iterations: 200, updateInterval: 25 },
168
244
  },
169
245
  interaction: { hover: true, tooltipDelay: 100, zoomView: true, dragView: true },
170
246
  });
171
247
  this.network.on('click', (params) => {
172
- const targetId = params.nodes[0];
173
- if (targetId === undefined)
174
- return;
175
- const node = data.nodes.find((n) => String(n.id) === String(targetId));
248
+ const node = graph.nodes.find((candidate) => candidate.id === String(params.nodes[0]));
176
249
  if (node)
177
250
  void this.showDetail(node);
178
251
  });
179
- // Stepping to a linked record redraws the graph around it.
180
- this.network.on('doubleClick', (params) => {
181
- const targetId = params.nodes[0];
182
- if (targetId !== undefined && String(targetId) !== this.centerId) {
183
- void this.showRecord(String(targetId));
184
- }
185
- });
186
252
  this.network.on('stabilizationIterationsDone', () => {
187
253
  this.network?.setOptions?.({ physics: { enabled: false } });
188
254
  });
189
255
  this.renderLegend();
190
- const legend = getElementByIdOrNull('legend-panel');
191
- legend?.classList.add('visible');
256
+ getElementByIdOrNull('legend-panel')?.classList.add('visible');
192
257
  const stats = getElementByIdOrNull('graph-stats');
193
258
  if (stats) {
259
+ const selected = graph.nodes.filter((node) => !node.outside).length;
260
+ const outside = graph.nodes.length - selected;
194
261
  stats.style.display = '';
195
- stats.textContent = `${data.nodes.length} records and source messages · ${data.edges.length} links · click a dot for its detail, double-click to step to it`;
262
+ stats.textContent = `${selected} records and work items · ${graph.edges.length} links${outside === 0 ? '' : ` · ${outside} linked records outside this filter, faded`} · click a dot for its detail`;
196
263
  }
197
264
  }
198
265
  // =============================================
199
266
  // Styling and legend
200
267
  // =============================================
201
- getNodeColor(kind = 'memory') {
202
- return this.nodeColors[kind] ?? '#CBD5E1';
203
- }
204
268
  getEdgeStyle(relationship) {
205
269
  return (this.edgeStyles[relationship ?? 'default'] ?? { color: '#4a4a6a', dashes: false, width: 2 });
206
270
  }
207
271
  kindLabel(kind) {
208
- return KIND_LABELS[kind] ?? kind;
272
+ return kind === null ? KIND_LABELS.memory : (KIND_LABELS[kind] ?? kind);
209
273
  }
210
274
  relationLabel(relationship) {
211
- return relationship === undefined ? 'link' : (RELATION_LABELS[relationship] ?? relationship);
275
+ return RELATION_LABELS[relationship] ?? relationship;
212
276
  }
213
- /** Count what is drawn, by kind and by relationship, with the words the legend shows. */
277
+ /** Count what is drawn, by group and by relationship, with the words the legend shows. */
214
278
  getLegendEntries() {
215
- const kinds = new Map();
279
+ const groups = new Map();
216
280
  const relations = new Map();
217
- for (const node of this.graphData.nodes) {
218
- const kind = node.kind ?? 'memory';
219
- kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
281
+ for (const node of this.graph.nodes) {
282
+ groups.set(node.group, (groups.get(node.group) ?? 0) + 1);
220
283
  }
221
- for (const edge of this.graphData.edges) {
222
- const relation = edge.relationship ?? 'default';
223
- relations.set(relation, (relations.get(relation) ?? 0) + 1);
284
+ for (const edge of this.graph.edges) {
285
+ relations.set(edge.relation, (relations.get(edge.relation) ?? 0) + 1);
224
286
  }
225
287
  return {
226
- nodes: [...kinds]
227
- .sort(([, a], [, b]) => b - a)
228
- .map(([kind, count]) => ({
229
- kind,
230
- label: this.kindLabel(kind),
288
+ nodes: [...groups].map(([group, count]) => ({
289
+ group,
290
+ label: group === 'outside' ? 'Linked, outside this filter' : groupInfo(group).label,
231
291
  count,
232
- color: this.getNodeColor(kind),
292
+ color: group === 'outside' ? OUTSIDE_COLOR : groupInfo(group).color,
233
293
  })),
234
294
  edges: [...relations]
235
295
  .sort(([, a], [, b]) => b - a)
@@ -251,7 +311,7 @@ export class GraphModule {
251
311
  : `<div><div class="legend-heading">${title}</div>${rows.join('')}</div>`;
252
312
  const row = (sample, label, count) => `<div class="legend-row">${sample}<span class="legend-label">${escapeHtml(label)}</span><span class="legend-count">${count}</span></div>`;
253
313
  container.innerHTML =
254
- section('Dots', entries.nodes.map(({ label, count, color }) => row(`<span class="legend-dot" style="background:${color}"></span>`, label, count))) +
314
+ section('Dots (a work item is one dot for its updates)', entries.nodes.map(({ label, count, color }) => row(`<span class="legend-dot" style="background:${color}"></span>`, label, count))) +
255
315
  section('Lines (arrow points at what the record links to)', entries.edges.map(({ label, count, color, dashes, width }) => {
256
316
  const dash = Array.isArray(dashes) ? dashes.join(' ') : dashes ? '5 5' : 'none';
257
317
  return row(`<svg width="28" height="10" class="shrink-0" aria-hidden="true"><line x1="0" y1="5" x2="28" y2="5" stroke="${color}" stroke-width="${width}" stroke-dasharray="${dash}" /></svg>`, label, count);
@@ -260,14 +320,14 @@ export class GraphModule {
260
320
  container.textContent = 'Nothing drawn';
261
321
  }
262
322
  createNodeTooltip(node) {
263
- const kind = this.kindLabel(node.kind ?? 'memory');
264
- const text = String(node.decision_preview ?? node.decision ?? '');
265
- return `${kind}${node.topic ? ` · ${node.topic}` : ''}\n${text.slice(0, 160)}`;
323
+ const group = node.outside ? 'Linked, outside this filter' : groupInfo(node.group).label;
324
+ const updates = node.updates > 0 ? `\n${node.updates} updates in this period` : '';
325
+ return `${group} · ${this.kindLabel(node.kind)}\n${node.label.slice(0, 160)}${updates}`;
266
326
  }
267
327
  // =============================================
268
328
  // Detail Panel
269
329
  // =============================================
270
- async showDetail(node) {
330
+ async showDetail(target) {
271
331
  const panel = getElementByIdOrNull('decision-detail-modal');
272
332
  const topicEl = getElementByIdOrNull('detail-topic');
273
333
  const kindEl = getElementByIdOrNull('detail-kind');
@@ -279,21 +339,23 @@ export class GraphModule {
279
339
  return;
280
340
  }
281
341
  if (topicEl)
282
- topicEl.textContent = node.topic || this.kindLabel(node.kind ?? 'memory');
342
+ topicEl.textContent = target.label;
283
343
  if (kindEl)
284
- kindEl.textContent = this.kindLabel(node.kind ?? 'memory');
285
- if (createdEl) {
286
- createdEl.textContent = node.created_at ? new Date(node.created_at).toLocaleString() : '-';
287
- }
344
+ kindEl.textContent = this.kindLabel(target.kind);
345
+ if (createdEl)
346
+ createdEl.textContent = '-';
288
347
  decisionEl.innerHTML = '<span class="loading-similar">Loading details...</span>';
289
348
  reasoningEl.innerHTML = '';
290
349
  panel.classList.add('visible');
291
350
  const request = ++this.detailRequest;
292
351
  try {
293
- const detail = await API.getGraphDetail(String(node.id));
352
+ const detail = await API.getGraphDetail(target.detailId);
294
353
  if (request !== this.detailRequest)
295
354
  return;
296
355
  const detailNode = detail.node;
356
+ if (createdEl && detailNode.created_at) {
357
+ createdEl.textContent = new Date(detailNode.created_at).toLocaleString();
358
+ }
297
359
  decisionEl.innerHTML = renderSafeMarkdown(String(detailNode.decision || detailNode.decision_preview || '-'));
298
360
  reasoningEl.innerHTML = renderSafeMarkdown(String(detailNode.reasoning || '-'));
299
361
  }
@@ -35,7 +35,7 @@ const OPERATION_LABELS = {
35
35
  revise: 'updated',
36
36
  withdraw: 'withdrawn',
37
37
  };
38
- function groupInfo(group) {
38
+ export function groupInfo(group) {
39
39
  return GROUPS[group] ?? { label: group, hint: `Records of kind ${group}`, color: '#CBD5E1' };
40
40
  }
41
41
  function dayLabel(day) {
@@ -48,7 +48,7 @@ function dayLabel(day) {
48
48
  }).format(new Date(`${day}T00:00:00Z`));
49
49
  }
50
50
  export class MemoryTimelineModule {
51
- onSelect;
51
+ hooks;
52
52
  period = '7d';
53
53
  day = null;
54
54
  query = '';
@@ -57,8 +57,8 @@ export class MemoryTimelineModule {
57
57
  request = 0;
58
58
  bound = false;
59
59
  debouncedLoad = debounce(() => void this.load(), 300);
60
- constructor(onSelect) {
61
- this.onSelect = onSelect;
60
+ constructor(hooks) {
61
+ this.hooks = hooks;
62
62
  }
63
63
  /** Wire the controls once, then read the current period. */
64
64
  async init() {
@@ -114,7 +114,7 @@ export class MemoryTimelineModule {
114
114
  document
115
115
  .querySelector(`#memory-timeline [data-record-id="${CSS.escape(id)}"]`)
116
116
  ?.classList.add('selected');
117
- this.onSelect(id);
117
+ this.hooks.onSelect(id);
118
118
  }
119
119
  params() {
120
120
  return {
@@ -135,6 +135,7 @@ export class MemoryTimelineModule {
135
135
  if (request !== this.request)
136
136
  return;
137
137
  this.render(data);
138
+ this.hooks.onRender(data);
138
139
  }
139
140
  catch (error) {
140
141
  if (request !== this.request)
@@ -154,11 +154,11 @@ export class WikiModule {
154
154
  });
155
155
  });
156
156
  document.getElementById('wiki-new-btn')?.addEventListener('click', () => this.promptNewPage());
157
- // Auto-open index page only on initial load (no page selected yet)
157
+ // Auto-open the agent's table of contents only on initial load (no page selected yet)
158
158
  if (!this.currentPath) {
159
- const indexNode = tree.find((n) => n.name === 'index.md');
160
- if (!mobile && indexNode) {
161
- this.openPage(indexNode.path);
159
+ const homeNode = tree.find((n) => n.name === 'Home.md');
160
+ if (!mobile && homeNode) {
161
+ this.openPage(homeNode.path);
162
162
  }
163
163
  }
164
164
  }
@@ -124,9 +124,9 @@ export class API {
124
124
  // =============================================
125
125
  // Graph API
126
126
  // =============================================
127
- /** One record and the records and source messages it links to directly. */
128
- static async getGraphNeighbors(id) {
129
- return this.get('/api/graph/neighbors', { id });
127
+ /** Every link between records, with each end's kind, label and work item. */
128
+ static async getMemoryLinks() {
129
+ return this.get('/api/memory/links');
130
130
  }
131
131
  /** What was saved in a period, by day, kind and item. */
132
132
  static async getMemoryTimeline(params) {