agent-memory-brain 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. agent_memory_brain-0.2.0.dist-info/METADATA +98 -0
  2. agent_memory_brain-0.2.0.dist-info/RECORD +54 -0
  3. agent_memory_brain-0.2.0.dist-info/WHEEL +4 -0
  4. agent_memory_brain-0.2.0.dist-info/entry_points.txt +3 -0
  5. agent_memory_brain-0.2.0.dist-info/licenses/LICENSE +67 -0
  6. devmem/__init__.py +3 -0
  7. devmem/ado.py +433 -0
  8. devmem/agent_colors.py +74 -0
  9. devmem/cli.py +500 -0
  10. devmem/db.py +1069 -0
  11. devmem/dispatch.py +478 -0
  12. devmem/embed.py +111 -0
  13. devmem/filelock.py +53 -0
  14. devmem/git_sync.py +261 -0
  15. devmem/guard.py +214 -0
  16. devmem/hook_log.py +49 -0
  17. devmem/ingest/__init__.py +1 -0
  18. devmem/ingest/claude.py +87 -0
  19. devmem/ingest/codex.py +175 -0
  20. devmem/ingest/runner.py +318 -0
  21. devmem/ingest/subagents.py +250 -0
  22. devmem/maintenance.py +801 -0
  23. devmem/mcp_server.py +542 -0
  24. devmem/messages.py +684 -0
  25. devmem/panel/__init__.py +1 -0
  26. devmem/panel/app.py +971 -0
  27. devmem/panel/state.py +358 -0
  28. devmem/panel/static/base.css +27 -0
  29. devmem/panel/static/brain.css +473 -0
  30. devmem/panel/static/brain.js +1118 -0
  31. devmem/panel/static/brain3d.js +600 -0
  32. devmem/panel/static/chat.js +18 -0
  33. devmem/panel/static/common.js +208 -0
  34. devmem/panel/static/site.css +508 -0
  35. devmem/panel/static/tokens.css +19 -0
  36. devmem/panel/templates/ado.html +62 -0
  37. devmem/panel/templates/agents.html +35 -0
  38. devmem/panel/templates/audit.html +19 -0
  39. devmem/panel/templates/base.html +42 -0
  40. devmem/panel/templates/brain.html +101 -0
  41. devmem/panel/templates/maintenance.html +32 -0
  42. devmem/panel/templates/memory.html +71 -0
  43. devmem/panel/templates/message_thread.html +49 -0
  44. devmem/panel/templates/messages.html +8 -0
  45. devmem/panel/templates/overview.html +109 -0
  46. devmem/panel/templates/project.html +48 -0
  47. devmem/panel/templates/projects.html +21 -0
  48. devmem/panel/templates/session.html +49 -0
  49. devmem/panel/templates/sessions.html +67 -0
  50. devmem/panel/templates/subagent.html +27 -0
  51. devmem/procs.py +10 -0
  52. devmem/projects.py +17 -0
  53. devmem/secrets.py +9 -0
  54. devmem/skills.py +105 -0
@@ -0,0 +1,1118 @@
1
+ // devmem brain wall: SSE client, panels and the canvas "brain" network.
2
+ (function () {
3
+ "use strict";
4
+
5
+ var U = window.devmem;
6
+ var REDUCED = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
7
+ var AGENT_COLORS = { claude: "#d6c1a4", "claude-subagent": "#998a75", codex: "#6db0f4" };
8
+ var KIND_COLORS = { note: "#88a8c9", decision: "#aa9dc5", preference: "#c7959c", snippet: "#90b697", todo: "#ccb484" };
9
+ var LINK_COLORS = { blocks: "#f47c70", depends_on: "#ccb484", relates_to: "#9c9890", overlaps: "#66635d", fixes: "#90b697", supersedes: "#4a463f" };
10
+ var KIND_ORDER = ["decision", "preference", "note", "snippet", "todo"];
11
+ var STALE_S = 6 * 3600;
12
+ var $ = function (id) { return document.getElementById(id); };
13
+
14
+ function agentColor(agent) {
15
+ if (AGENT_COLORS[agent]) return AGENT_COLORS[agent];
16
+ agent = String(agent || "");
17
+ if (agent.indexOf("codex") >= 0) return AGENT_COLORS.codex;
18
+ if (agent.indexOf("claude") >= 0) return AGENT_COLORS.claude;
19
+ return "#9c9890";
20
+ }
21
+
22
+ function agentShort(agent) {
23
+ return agent === "claude-subagent" ? "subagent" : String(agent || "agent");
24
+ }
25
+
26
+ function esc(s) {
27
+ return String(s === null || s === undefined ? "" : s).replace(/[&<>"']/g, function (c) {
28
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
29
+ });
30
+ }
31
+
32
+ // ------------------------------------------------------------------ wall mode + clock
33
+
34
+ // The body state class is "wall-mode": plain "wall" collided with the .wall grid container.
35
+ var hintTimer = null;
36
+
37
+ function showWallHint() {
38
+ var hint = $("wall-hint");
39
+ hint.hidden = false;
40
+ hint.classList.add("show");
41
+ clearTimeout(hintTimer);
42
+ hintTimer = setTimeout(function () { hint.classList.remove("show"); }, 3000);
43
+ }
44
+
45
+ function isWall() { return document.body.classList.contains("wall-mode"); }
46
+
47
+ function setWall(on) {
48
+ document.body.classList.toggle("wall-mode", on);
49
+ try { localStorage.setItem("devmem.wall", on ? "1" : "0"); } catch (e) { /* storage unavailable */ }
50
+ if (on) showWallHint();
51
+ setTimeout(function () { if (window.devmemBrain) window.devmemBrain.resize(); }, 50);
52
+ }
53
+ (function initWall() {
54
+ var on = false;
55
+ try { on = localStorage.getItem("devmem.wall") === "1"; } catch (e) { /* ignore */ }
56
+ if (on) { document.body.classList.add("wall-mode"); showWallHint(); }
57
+ $("wall-toggle").addEventListener("click", function () { setWall(true); });
58
+ $("wall-exit").addEventListener("click", function () { setWall(false); });
59
+ document.addEventListener("keydown", function (e) {
60
+ if (e.ctrlKey || e.metaKey || e.altKey || /input|textarea|select/i.test(e.target.tagName)) return;
61
+ if (e.key === "w" || e.key === "W") setWall(!isWall());
62
+ else if (e.key === "Escape" && isWall()) setWall(false);
63
+ });
64
+ })();
65
+
66
+ // ------------------------------------------------------------------ gauges
67
+
68
+ var EXPECTED_LIMITS = [
69
+ { agent: "claude", label: "five_hour", window_minutes: 300 },
70
+ { agent: "claude", label: "seven_day", window_minutes: 10080 },
71
+ { agent: "codex", label: "primary", window_minutes: 300 },
72
+ { agent: "codex", label: "secondary", window_minutes: 10080 },
73
+ ];
74
+ var gaugeEls = {};
75
+ var ARC_R = 80, ARC_C = 2 * Math.PI * ARC_R, ARC_LEN = ARC_C * 0.75;
76
+
77
+ function windowName(l) {
78
+ var named = { five_hour: "5h", seven_day: "weekly" };
79
+ if (named[l.label]) return named[l.label];
80
+ if (/^seven_day_/.test(l.label)) return "weekly " + l.label.replace(/^seven_day_/, "").replace(/_/g, " ");
81
+ var m = l.window_minutes;
82
+ var base = m === 300 ? "5h" : m === 10080 ? "weekly" : m ? (m % 1440 === 0 ? m / 1440 + "d" : Math.round(m / 60) + "h") : l.label;
83
+ var prefix = l.label.indexOf(":") > 0 ? l.label.split(":")[0] + " " : "";
84
+ return prefix + base;
85
+ }
86
+
87
+ function gaugeColor(left) {
88
+ if (left === null || left === undefined) return "#66635d";
89
+ if (left <= 10) return "#f47c70";
90
+ if (left <= 25) return "#f97c3d";
91
+ if (left <= 50) return "#ebb25f";
92
+ return "#cecac3";
93
+ }
94
+
95
+ function makeGauge(key) {
96
+ var el = document.createElement("div");
97
+ el.className = "gauge";
98
+ el.innerHTML =
99
+ '<svg viewBox="0 0 200 178" role="img">' +
100
+ '<circle class="track" cx="100" cy="100" r="' + ARC_R + '" stroke-dasharray="' + ARC_LEN + " " + ARC_C + '" transform="rotate(135 100 100)"/>' +
101
+ '<circle class="val" cx="100" cy="100" r="' + ARC_R + '" stroke-dasharray="0 ' + ARC_C + '" transform="rotate(135 100 100)"/>' +
102
+ '<text class="pct" x="100" y="108" text-anchor="middle"><tspan class="n">—</tspan><tspan class="pct-unit" dx="2">%</tspan></text>' +
103
+ '<text class="sub" x="100" y="134" text-anchor="middle">left</text>' +
104
+ "</svg>" +
105
+ '<div class="g-label"></div><div class="g-reset"></div><div class="g-asof"></div>';
106
+ $("gauges").appendChild(el);
107
+ gaugeEls[key] = el;
108
+ return el;
109
+ }
110
+
111
+ var limitsData = [];
112
+
113
+ function renderLimits(limits) {
114
+ var byKey = {};
115
+ limits.forEach(function (l) { byKey[l.agent + ":" + l.label] = l; });
116
+ var list = EXPECTED_LIMITS.map(function (e) { return byKey[e.agent + ":" + e.label] || Object.assign({ missing: true }, e); });
117
+ limits.forEach(function (l) {
118
+ if (!EXPECTED_LIMITS.some(function (e) { return e.agent === l.agent && e.label === l.label; })) list.push(l);
119
+ });
120
+ limitsData = list;
121
+ list.forEach(function (l) {
122
+ var key = l.agent + ":" + l.label;
123
+ var el = gaugeEls[key] || makeGauge(key);
124
+ var left = l.missing ? null : l.left_percent;
125
+ var stale = !l.missing && (l.stale || (l.age_s !== null && l.age_s > STALE_S));
126
+ el.classList.toggle("nodata", !!l.missing);
127
+ el.classList.toggle("stale", !!stale);
128
+ var color = l.missing || stale ? "#66635d" : gaugeColor(left);
129
+ el.style.setProperty("--g", color);
130
+ el.querySelector(".val").setAttribute("stroke-dasharray", (left === null ? 0 : ARC_LEN * Math.max(0, Math.min(100, left)) / 100) + " " + ARC_C);
131
+ el.querySelector(".n").textContent = left === null ? "—" : Math.round(left);
132
+ el.querySelector(".sub").textContent = l.missing ? "no data" : stale ? "stale" : "left";
133
+ el.querySelector(".g-label").textContent = (l.agent === "codex" ? "Codex" : l.agent === "claude" ? "Claude" : l.agent) + " · " + windowName(l);
134
+ el.dataset.resets = l.resets_at || "";
135
+ el.dataset.asof = l.as_of || "";
136
+ el.dataset.missing = l.missing ? "1" : "";
137
+ });
138
+ tickGauges();
139
+ }
140
+
141
+ function tickGauges() {
142
+ Object.keys(gaugeEls).forEach(function (key) {
143
+ var el = gaugeEls[key];
144
+ var reset = el.querySelector(".g-reset"), asof = el.querySelector(".g-asof");
145
+ if (el.dataset.missing) { reset.textContent = "no data yet"; asof.textContent = ""; return; }
146
+ var r = U.parseTs(el.dataset.resets);
147
+ if (r) {
148
+ var s = (r.getTime() - Date.now()) / 1000;
149
+ reset.innerHTML = s > 0 ? "resets in " + U.fmtDuration(s) : '<span class="flag">window reset · awaiting update</span>';
150
+ } else reset.textContent = "reset time unknown";
151
+ asof.textContent = el.dataset.asof ? "as of " + U.relTime(el.dataset.asof) : "";
152
+ });
153
+ }
154
+
155
+ // ------------------------------------------------------------------ memory kinds
156
+
157
+ function renderCounts(counts) {
158
+ var by = counts.memories_by_kind || {};
159
+ $("mem-total").textContent = counts.memories_total || 0;
160
+ var max = Math.max(1, Math.max.apply(null, KIND_ORDER.map(function (k) { return by[k] || 0; })));
161
+ $("kinds").innerHTML = KIND_ORDER.map(function (k) {
162
+ return '<div class="kind-row" style="--c:' + KIND_COLORS[k] + '"><span class="k">' + k + '</span>' +
163
+ '<span class="bar"><i style="width:' + ((by[k] || 0) / max * 100).toFixed(1) + '%"></i></span><b>' + (by[k] || 0) + "</b></div>";
164
+ }).join("");
165
+ }
166
+
167
+ // ------------------------------------------------------------------ reminders
168
+
169
+ var remindersKey = null, reminderRows = [], finishedReminders = {};
170
+
171
+ function renderReminders(items) {
172
+ var visible = (items || []).filter(function (item) { return !finishedReminders[item.id]; });
173
+ var key = JSON.stringify(visible);
174
+ if (key === remindersKey) return;
175
+ remindersKey = key;
176
+ reminderRows = visible;
177
+ var list = $("reminders"), count = $("reminders-count");
178
+ count.textContent = visible.length || "";
179
+ list.replaceChildren();
180
+ if (!visible.length) {
181
+ var empty = document.createElement("li");
182
+ empty.className = "empty";
183
+ empty.textContent = "Nothing to come back to.";
184
+ list.appendChild(empty);
185
+ return;
186
+ }
187
+ visible.forEach(function (item) {
188
+ var row = document.createElement("li"), title = document.createElement("span"), button = document.createElement("button"), error = document.createElement("span");
189
+ title.className = "title";
190
+ title.textContent = item.title;
191
+ button.type = "button";
192
+ button.textContent = "Done";
193
+ button.setAttribute("aria-label", "Mark reminder done: " + item.title);
194
+ error.className = "error";
195
+ button.addEventListener("click", function () {
196
+ button.disabled = true;
197
+ error.textContent = "";
198
+ fetch("/memory/" + encodeURIComponent(item.id) + "/reminder-done", { method: "POST" }).then(function (response) {
199
+ if (!response.ok) throw new Error("reminder update failed");
200
+ finishedReminders[item.id] = true;
201
+ renderReminders(reminderRows.filter(function (reminder) { return reminder.id !== item.id; }));
202
+ }).catch(function () {
203
+ button.disabled = false;
204
+ error.textContent = "Could not mark done.";
205
+ });
206
+ });
207
+ row.append(title, button, error);
208
+ list.appendChild(row);
209
+ });
210
+ }
211
+
212
+ // ------------------------------------------------------------------ Azure DevOps card (the server only returns cached data here)
213
+
214
+ function renderAdoCard(snapshot) {
215
+ var card = $("ado-card");
216
+ if (!snapshot || !snapshot.configured) { card.hidden = true; return; }
217
+ card.hidden = false;
218
+ var heading = $("ado-heading");
219
+ if (snapshot.stale) {
220
+ heading.innerHTML = '<a href="/ado">ADO</a> <span class="badge ado-card-stale">stale</span>';
221
+ card.title = (snapshot.error || "ADO data is stale") + "; last refresh: " + (snapshot.refreshed_at || "unknown");
222
+ } else {
223
+ heading.innerHTML = '<a href="/ado">ADO</a>';
224
+ card.title = "";
225
+ }
226
+ var count = function (name) { return (snapshot[name] || []).length; };
227
+ $("ado-counts").innerHTML =
228
+ '<span><b>' + count("to_review") + '</b> to review</span>' +
229
+ '<span><b>' + count("my_approved_not_completed") + '</b> approved</span>' +
230
+ '<span><b>' + count("my_changes_requested") + '</b> changes</span>' +
231
+ '<span><b>' + count("my_waiting_on_others") + '</b> waiting</span>' +
232
+ '<span><b>' + count("open_tasks") + '</b> tasks</span>';
233
+ }
234
+
235
+ function refreshAdoCard() {
236
+ fetch("/api/ado").then(function (response) {
237
+ return response.ok ? response.json() : null;
238
+ }).then(renderAdoCard).catch(function (e) { console.warn("devmem: /api/ado failed", e); });
239
+ }
240
+
241
+ // ------------------------------------------------------------------ agents
242
+
243
+ function subagentRows(list) {
244
+ return '<ul class="subagents">' + list.map(function (x) {
245
+ var last = x.last_tool ? '<span class="tool">' + esc(x.last_tool) + "</span> " + esc(x.last_arg || "") : esc(x.description || "");
246
+ return '<li class="sub ' + esc(x.status) + '" title="' + esc(x.status) + '"><span class="dot"></span>' +
247
+ '<a class="s-type" href="/messages/agent/' + encodeURIComponent(x.agent_id) + '">' + esc(x.agent_type || "subagent") + "</a>" +
248
+ '<span class="s-last">' + last + "</span></li>";
249
+ }).join("") + "</ul>";
250
+ }
251
+
252
+ function renderAgents(a) {
253
+ var sessions = a.sessions || [], clients = a.connected || [], subs = a.subagents || {}, shownSubs = {};
254
+ var rows = sessions.slice(0, 8).map(function (s) {
255
+ // claude and claude-subagent rows share a session id: its subagents are listed once, under the first row.
256
+ var children = s.agent.indexOf("claude") === 0 && subs[s.session_id] && !shownSubs[s.session_id] ? subs[s.session_id] : null;
257
+ if (children) shownSubs[s.session_id] = 1;
258
+ var connected = clients.some(function (c) { return c.family === (s.agent.indexOf("codex") >= 0 ? "codex" : "claude") && c.project === s.project; });
259
+ var last = s.last_tool ? '<span class="tool">' + esc(s.last_tool) + "</span> " + esc(s.last_summary || "") :
260
+ (s.tokens_10m ? U.fmtTokens(s.tokens_10m) + " tokens in 10 min" : "");
261
+ return '<li class="agent' + (s.age_s !== null && s.age_s < 120 ? " active" : "") + '" style="--c:' + agentColor(s.agent) + '">' +
262
+ '<span class="pip"></span><span class="a-main"><b>' + esc(agentShort(s.agent)) + "</b> · " + esc(s.project_name) +
263
+ (connected ? ' <span class="hint">· mcp</span>' : "") + '</span><span class="a-age" data-ts="' + esc(s.last_ts) + '">' + U.relTime(s.last_ts) + "</span>" +
264
+ '<span class="a-last">' + last + "</span>" + (children ? subagentRows(children) : "") + "</li>";
265
+ });
266
+ clients.forEach(function (c) {
267
+ var shown = sessions.some(function (s) { return (s.agent.indexOf("codex") >= 0 ? "codex" : "claude") === c.family && s.project === c.project; });
268
+ if (shown) return;
269
+ rows.push('<li class="agent" style="--c:' + agentColor(c.family) + '"><span class="pip"></span>' +
270
+ '<span class="a-main"><b>' + esc(c.client_name || c.family) + "</b> · " + esc(c.project_name) + '</span><span class="a-age">connected</span>' +
271
+ '<span class="a-last">' + (c.last_tool ? 'memory: <span class="tool">' + esc(c.last_tool) + "</span>" : "idle") + "</span></li>");
272
+ });
273
+ $("agents").innerHTML = rows.length ? rows.join("") : '<li class="empty">No agent activity in the last 10 minutes.</li>';
274
+ $("agents-hint").textContent = clients.length + " connected · " + sessions.length + " active";
275
+ }
276
+
277
+ // ------------------------------------------------------------------ feed
278
+
279
+ var feedSeen = null;
280
+
281
+ var feedKey = null;
282
+
283
+ function renderFeed(items) {
284
+ var key = items.map(function (e) { return e.id; }).join(",");
285
+ if (key === feedKey) return;
286
+ feedKey = key;
287
+ var first = feedSeen === null;
288
+ var seen = feedSeen || {};
289
+ var next = {};
290
+ var ul = $("feed");
291
+ ul.innerHTML = items.map(function (e) {
292
+ next[e.id] = 1;
293
+ var fresh = !first && !seen[e.id] && !REDUCED;
294
+ return '<li class="' + esc(e.kind) + (fresh ? " enter" : "") + '" style="--c:' + agentColor(e.agent) + '">' +
295
+ '<span class="f-badge">' + esc(agentShort(e.agent)) + "</span>" +
296
+ '<span class="f-main"><span class="f-proj">' + esc(e.project_name) + '</span><span class="f-tool">' + esc(e.name) + "</span></span>" +
297
+ '<span class="f-time" data-ts="' + esc(e.ts) + '">' + U.relTime(e.ts) + "</span>" +
298
+ (e.summary ? '<span class="f-sum">' + esc(e.summary) + "</span>" : "") + "</li>";
299
+ }).join("") || '<li class="empty">No tool activity yet. It appears here as agents work.</li>';
300
+ feedSeen = next;
301
+ }
302
+
303
+ // ------------------------------------------------------------------ messages
304
+
305
+ var msgSeen = null;
306
+
307
+ function renderMessages(m) {
308
+ var items = m.items || [], first = msgSeen === null, seen = msgSeen || {}, next = {};
309
+ // Compact chat bubbles, newest first; each opens its chat on /messages.
310
+ $("msgs").innerHTML = items.map(function (x) {
311
+ next[x.id] = 1;
312
+ var fresh = !first && !seen[x.id] && !REDUCED;
313
+ var color = x.mine ? "var(--color-user)" : x.who === "Claude" ? "var(--color-claude)" : "oklch(78% 0.07 " + (x.hue || 0) + ")";
314
+ var href = x.chat ? "/messages?chat=" + encodeURIComponent(x.chat) : "/messages/thread/" + x.thread_id;
315
+ return '<li class="mb' + (x.mine ? " mine" : "") + (x.unread ? " unread" : "") + (fresh ? " enter" : "") + '" style="--c:' + color + '">' +
316
+ '<a href="' + href + '"><span class="m-line">' + esc((x.icon || "✉") + " " + (x.headline || x.text || x.line)) + "</span>" +
317
+ '<span class="m-time" data-ts="' + esc(x.created_at) + '">' + U.relTime(x.created_at) + "</span></a></li>";
318
+ }).join("") || '<li class="empty">No messages yet. Agents post with message_post; subagent tasks and reports show up here.</li>';
319
+ msgSeen = next;
320
+ $("msg-unread").textContent = m.unread ? m.unread + " unread" : "";
321
+ var badge = $("nav-msg-badge");
322
+ if (badge) { badge.textContent = m.badge || 0; badge.hidden = !m.badge; }
323
+ }
324
+
325
+ function tickRelTimes() {
326
+ document.querySelectorAll("[data-ts]").forEach(function (el) { el.textContent = U.relTime(el.dataset.ts); });
327
+ }
328
+
329
+ // ------------------------------------------------------------------ brain renderer
330
+
331
+ var Brain2D = (function () {
332
+ var canvas = $("brain"), wrap = $("brain-wrap"), tip = $("brain-tip");
333
+ var ctx = canvas.getContext("2d");
334
+ var W = 0, H = 0, DPR = 1, G = null; // size and geometry
335
+ var outline = null; // pre-rendered silhouette
336
+ var nodes = [], nodeById = {}, memLinks = [], agentLinks = [], projectLinks = [];
337
+ var sim = null, hasD3 = !!(window.d3 && window.d3.forceSimulation);
338
+ var impulses = [], flashes = {}, pendingBloom = {};
339
+ var sprites = {};
340
+ var running = false, lastFrame = 0, firstGraph = true, dirty = true, hover = null, active = true;
341
+ var start = performance.now();
342
+ var FONT = getComputedStyle(document.body).fontFamily;
343
+
344
+ function sprite(color) {
345
+ if (sprites[color]) return sprites[color];
346
+ var s = document.createElement("canvas"), n = 64;
347
+ s.width = s.height = n;
348
+ var c = s.getContext("2d"), g = c.createRadialGradient(n / 2, n / 2, 0, n / 2, n / 2, n / 2);
349
+ g.addColorStop(0, color);
350
+ g.addColorStop(0.25, color + "aa");
351
+ g.addColorStop(1, color + "00");
352
+ c.fillStyle = g;
353
+ c.fillRect(0, 0, n, n);
354
+ sprites[color] = s;
355
+ return s;
356
+ }
357
+
358
+ function geometry() {
359
+ var ry = Math.min(H * 0.37, W / 4.4), rx = ry * 0.93;
360
+ var cx = W / 2, cy = H * 0.5;
361
+ return { cx: cx, cy: cy, rx: rx, ry: ry, s: ry / 320,
362
+ hemis: [{ x: cx - rx * 0.95, y: cy }, { x: cx + rx * 0.95, y: cy }] };
363
+ }
364
+
365
+ function wobble(theta, seed) {
366
+ return 1 + 0.022 * Math.sin(7 * theta + seed) + 0.014 * Math.sin(13 * theta + seed * 2.3) + 0.008 * Math.sin(23 * theta + seed);
367
+ }
368
+
369
+ function renderOutline() {
370
+ outline = document.createElement("canvas");
371
+ outline.width = Math.max(1, Math.round(W * DPR));
372
+ outline.height = Math.max(1, Math.round(H * DPR));
373
+ var c = outline.getContext("2d");
374
+ c.scale(DPR, DPR);
375
+ var paths = G.hemis.map(function (h, i) {
376
+ var p = new Path2D();
377
+ for (var k = 0; k <= 160; k++) {
378
+ var t = (k / 160) * Math.PI * 2, w = wobble(t, i * 1.7 + 0.4);
379
+ // flatten the inner side so the two hemispheres meet along a fissure
380
+ var inner = (i === 0 ? Math.cos(t) > 0 : Math.cos(t) < 0) ? 0.97 : 1;
381
+ var x = h.x + Math.cos(t) * G.rx * w * inner, y = h.y + Math.sin(t) * G.ry * w;
382
+ if (k === 0) p.moveTo(x, y); else p.lineTo(x, y);
383
+ }
384
+ p.closePath();
385
+ return p;
386
+ });
387
+ // inner fill
388
+ paths.forEach(function (p, i) {
389
+ var h = G.hemis[i];
390
+ var g = c.createRadialGradient(h.x, h.y - G.ry * 0.2, G.ry * 0.1, h.x, h.y, G.ry * 1.1);
391
+ g.addColorStop(0, "rgba(239,235,228,0.025)");
392
+ g.addColorStop(1, "rgba(239,235,228,0.005)");
393
+ c.fillStyle = g;
394
+ c.fill(p);
395
+ });
396
+ // soft glow outline (shadowBlur only here, rendered once per resize)
397
+ c.lineWidth = 1.6;
398
+ c.strokeStyle = "rgba(239,235,228,0.22)";
399
+ c.shadowColor = "rgba(0,0,0,0)";
400
+ c.shadowBlur = 22 * G.s + 6;
401
+ paths.forEach(function (p) { c.stroke(p); });
402
+ c.shadowBlur = 0;
403
+ c.strokeStyle = "rgba(239,235,228,0.10)";
404
+ c.lineWidth = 1;
405
+ paths.forEach(function (p) { c.stroke(p); });
406
+ // gyri: faint curved strokes inside each hemisphere
407
+ var rnd = mulberry(7);
408
+ c.strokeStyle = "rgba(239,235,228,0.04)";
409
+ c.lineWidth = 1.2;
410
+ G.hemis.forEach(function (h) {
411
+ for (var k = 0; k < 16; k++) {
412
+ var a = rnd() * Math.PI * 2, d = 0.25 + rnd() * 0.6;
413
+ var x = h.x + Math.cos(a) * G.rx * d, y = h.y + Math.sin(a) * G.ry * d;
414
+ var len = (0.18 + rnd() * 0.25) * G.rx, dir = a + Math.PI / 2 + (rnd() - 0.5);
415
+ c.beginPath();
416
+ c.moveTo(x - Math.cos(dir) * len / 2, y - Math.sin(dir) * len / 2);
417
+ c.quadraticCurveTo(x + (rnd() - 0.5) * len * 0.6, y + (rnd() - 0.5) * len * 0.6, x + Math.cos(dir) * len / 2, y + Math.sin(dir) * len / 2);
418
+ c.stroke();
419
+ }
420
+ });
421
+ // longitudinal fissure
422
+ c.strokeStyle = "rgba(239,235,228,0.07)";
423
+ c.beginPath();
424
+ c.moveTo(G.cx, G.cy - G.ry * 0.92);
425
+ c.bezierCurveTo(G.cx - 6, G.cy - G.ry * 0.3, G.cx + 6, G.cy + G.ry * 0.3, G.cx, G.cy + G.ry * 0.92);
426
+ c.stroke();
427
+ }
428
+
429
+ function mulberry(a) {
430
+ return function () {
431
+ a |= 0; a = (a + 0x6d2b79f5) | 0;
432
+ var t = Math.imul(a ^ (a >>> 15), 1 | a);
433
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
434
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
435
+ };
436
+ }
437
+
438
+ function hash(str) {
439
+ var h = 2166136261;
440
+ for (var i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
441
+ return (h >>> 0) / 4294967296;
442
+ }
443
+
444
+ // ---- anchors: where each cluster lives inside the silhouette
445
+ function assignAnchors() {
446
+ var projects = nodes.filter(function (n) { return n.type === "project"; });
447
+ var real = projects.filter(function (p) { return p.id !== "other"; });
448
+ var halves = [[], []];
449
+ real.forEach(function (p, i) { halves[i % 2].push(p); });
450
+ halves.forEach(function (list, side) {
451
+ var h = G.hemis[side], n = list.length;
452
+ list.forEach(function (p, i) {
453
+ // spread over the outer side of each hemisphere; the inner (fissure) side holds global memories
454
+ var frac = n === 1 ? 0.5 : i / (n - 1);
455
+ var a = side === 0 ? Math.PI * (0.42 + 1.16 * frac) : Math.PI * (-0.58 + 1.16 * frac);
456
+ var d = n === 1 ? 0.3 : n === 2 ? 0.45 : (i % 2 ? 0.34 : 0.62);
457
+ p.ax = h.x + Math.cos(a) * G.rx * d;
458
+ p.ay = h.y + Math.sin(a) * G.ry * d;
459
+ });
460
+ });
461
+ projects.forEach(function (p) {
462
+ if (p.id === "other") { p.ax = G.cx; p.ay = G.cy + G.ry * 0.62; }
463
+ });
464
+ var hub = nodeById.__brain__;
465
+ if (hub) { hub.ax = G.cx; hub.ay = G.cy; hub.fx = G.cx; hub.fy = G.cy; }
466
+ nodes.forEach(function (n) {
467
+ if (n.type === "memory") {
468
+ var parent = nodeById[n.cluster === "global" ? "__brain__" : "p:" + n.cluster];
469
+ n.ax = parent ? parent.ax : G.cx;
470
+ n.ay = parent ? parent.ay : G.cy;
471
+ }
472
+ });
473
+ // agents on the rim: spread along an ellipse just outside the brain
474
+ var agents = nodes.filter(function (n) { return n.type === "agent"; });
475
+ var ex = G.rx * 1.95 + 30 * G.s, ey = G.ry * 1.17;
476
+ var top = agents.filter(function (a, i) { return i % 2 === 0; }), bottom = agents.filter(function (a, i) { return i % 2 === 1; });
477
+ [[top, -Math.PI / 2], [bottom, Math.PI / 2]].forEach(function (group) {
478
+ var list = group[0], centre = group[1], step = Math.min(0.55, 2.2 / Math.max(list.length, 1));
479
+ list.forEach(function (a, k) {
480
+ var ang = centre + (k - (list.length - 1) / 2) * step;
481
+ var x = G.cx + Math.cos(ang) * ex, y = G.cy + Math.sin(ang) * ey;
482
+ var p = a.project && nodeById["p:" + a.project];
483
+ if (p && list.length === 1) x += (p.ax - G.cx) * 0.35; // lean toward its lobe
484
+ a.fx = a.x = Math.max(40, Math.min(W - 40, x));
485
+ a.fy = a.y = Math.max(28, Math.min(H - 28, y));
486
+ });
487
+ });
488
+ }
489
+
490
+ function contain(alpha) {
491
+ var k = 0.9;
492
+ for (var i = 0; i < nodes.length; i++) {
493
+ var n = nodes[i];
494
+ if (n.type === "agent" || n.fx !== undefined && n.fx !== null) continue;
495
+ var best = 1e9, hb = null;
496
+ for (var j = 0; j < 2; j++) {
497
+ var h = G.hemis[j], dx = (n.x - h.x) / G.rx, dy = (n.y - h.y) / G.ry, d = Math.sqrt(dx * dx + dy * dy);
498
+ if (d < best) { best = d; hb = h; }
499
+ }
500
+ if (best > k) {
501
+ n.x = hb.x + (n.x - hb.x) * k / best;
502
+ n.y = hb.y + (n.y - hb.y) * k / best;
503
+ n.vx *= 0.5; n.vy *= 0.5;
504
+ }
505
+ }
506
+ }
507
+
508
+ function phyllotaxis(n, i, cx, cy, spacing) {
509
+ var r = spacing * Math.sqrt(i + 0.5), a = i * 2.39996;
510
+ return [cx + r * Math.cos(a), cy + r * Math.sin(a)];
511
+ }
512
+
513
+ function initialPositions() {
514
+ var counters = {};
515
+ nodes.forEach(function (n) {
516
+ if (n.type === "memory" && n.x === undefined) {
517
+ var c = counters[n.cluster] = (counters[n.cluster] || 0) + 1;
518
+ var p = phyllotaxis(n, c, n.ax, n.ay, 7 * G.s + 2);
519
+ n.x = p[0]; n.y = p[1];
520
+ } else if (n.type === "project" && n.x === undefined) {
521
+ n.x = n.ax; n.y = n.ay;
522
+ }
523
+ });
524
+ }
525
+
526
+ function buildSim(alpha) {
527
+ initialPositions(); // before sim.nodes(): d3 would otherwise seed new nodes near (0, 0)
528
+ if (!hasD3) { contain(); dirty = true; return; }
529
+ var d3 = window.d3;
530
+ if (!sim) {
531
+ sim = d3.forceSimulation().stop().alphaDecay(0.035).velocityDecay(0.38)
532
+ .force("charge", d3.forceManyBody().strength(function (n) { return n.type === "project" ? -70 * G.s : -5 * G.s; }).distanceMax(90))
533
+ .force("collide", d3.forceCollide().radius(function (n) { return n.r + 1.6; }).iterations(1))
534
+ .force("x", d3.forceX(function (n) { return n.ax; }).strength(function (n) { return n.type === "project" ? 0.35 : 0.06; }))
535
+ .force("y", d3.forceY(function (n) { return n.ay; }).strength(function (n) { return n.type === "project" ? 0.35 : 0.06; }))
536
+ .force("contain", contain);
537
+ }
538
+ sim.nodes(nodes.filter(function (n) { return n.type !== "agent"; }));
539
+ sim.force("link", d3.forceLink(memLinks).id(function (n) { return n.id; })
540
+ .distance(function (l) { return l.kind === "ml" ? 30 * G.s + 8 : (l.target.cluster === "global" ? 14 : 18) * G.s + 6; })
541
+ .strength(function (l) { return l.kind === "ml" ? 0.025 : 0.12; }));
542
+ sim.force("charge").strength(function (n) { return n.type === "project" ? -70 * G.s : -5 * G.s; });
543
+ sim.force("x").x(function (n) { return n.ax; });
544
+ sim.force("y").y(function (n) { return n.ay; });
545
+ sim.alpha(Math.max(sim.alpha(), alpha));
546
+ dirty = true;
547
+ }
548
+
549
+ var pendingGraph = null;
550
+
551
+ function setGraph(g) {
552
+ if (!G) { pendingGraph = g; return; }
553
+ var old = nodeById;
554
+ var next = [], byId = {};
555
+ var now = performance.now();
556
+ function keep(id, props) {
557
+ var n = old[id] || { id: id, phase: hash(id) * Math.PI * 2, born: firstGraph ? now + Math.random() * 1400 : now };
558
+ Object.assign(n, props);
559
+ next.push(n); byId[id] = n;
560
+ return n;
561
+ }
562
+ keep("__brain__", { type: "hub", r: 16 * G.s + 5, name: "Brain", color: "#efebe4" });
563
+ (g.projects || []).forEach(function (p) {
564
+ keep("p:" + p.id, { type: "project", project: p.id, name: p.name, path: p.path, memories: p.memories, activity: p.activity,
565
+ r: Math.min(16, 6 + Math.sqrt(p.memories || 0) * 1.3) * G.s + 3 });
566
+ });
567
+ (g.memories || []).forEach(function (m) {
568
+ var isNew = !old["m:" + m.id] && !firstGraph;
569
+ var n = keep("m:" + m.id, { type: "memory", mid: m.id, title: m.title, kind: m.kind, cluster: m.cluster, project: m.project,
570
+ r: 2.6 * G.s + 1, color: KIND_COLORS[m.kind] || "#9c9890" });
571
+ if (isNew || pendingBloom[m.id]) { n.bloom = now; delete pendingBloom[m.id]; }
572
+ });
573
+ (g.agents || []).forEach(function (a) {
574
+ keep("a:" + a.id, { type: "agent", agent: a.agent, session: a.session_id, project: a.project, name: a.project_name,
575
+ active: a.active, connected: a.connected, r: 6 * G.s + 2.5, color: agentColor(a.agent) });
576
+ });
577
+ nodes = next; nodeById = byId;
578
+ memLinks = []; agentLinks = []; projectLinks = [];
579
+ nodes.forEach(function (n) {
580
+ if (n.type === "memory") {
581
+ var parent = n.cluster === "global" ? "__brain__" : "p:" + n.cluster;
582
+ if (byId[parent]) memLinks.push({ source: parent, target: n.id, kind: "pm" });
583
+ } else if (n.type === "agent") {
584
+ agentLinks.push({ source: n, target: byId.__brain__ });
585
+ } else if (n.type === "project") {
586
+ projectLinks.push({ source: byId.__brain__, target: n });
587
+ }
588
+ });
589
+ (g.links || []).forEach(function (l) {
590
+ if (byId["m:" + l.from_id] && byId["m:" + l.to_id]) memLinks.push({ source: "m:" + l.from_id, target: "m:" + l.to_id, kind: "ml", type: l.type });
591
+ });
592
+ assignAnchors();
593
+ buildSim(firstGraph ? 1 : 0.25);
594
+ firstGraph = false;
595
+ kick();
596
+ }
597
+
598
+ // ---- impulses
599
+ function curvePoints(a, b) {
600
+ var mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
601
+ var cx = mx + (G.cx - mx) * 0.25, cy = my + (G.cy - my) * 0.25;
602
+ var pts = [];
603
+ for (var i = 0; i <= 12; i++) {
604
+ var t = i / 12, u = 1 - t;
605
+ pts.push([u * u * a.x + 2 * u * t * cx + t * t * b.x, u * u * a.y + 2 * u * t * cy + t * t * b.y]);
606
+ }
607
+ return pts;
608
+ }
609
+
610
+ var lastImpulse = {};
611
+
612
+ function findAgent(p) {
613
+ var exact = nodeById["a:" + p.agent + ":" + p.session_id];
614
+ if (exact) return exact;
615
+ var fam = String(p.agent || "").indexOf("codex") >= 0 ? "codex" : "claude";
616
+ var best = null;
617
+ nodes.forEach(function (n) {
618
+ if (n.type !== "agent") return;
619
+ var nf = n.agent.indexOf("codex") >= 0 ? "codex" : "claude";
620
+ if (nf === fam && (!best || n.project === p.project)) best = n;
621
+ });
622
+ return best;
623
+ }
624
+
625
+ function pulse(p) {
626
+ if (!G) return;
627
+ var hub = nodeById.__brain__, project = p.project ? nodeById["p:" + p.project] : null;
628
+ var agent = findAgent(p), now = performance.now();
629
+ if (p.type === "ado") {
630
+ if (hub) flashes[hub.id] = now;
631
+ dirty = true;
632
+ kick();
633
+ return;
634
+ }
635
+ if (p.type === "memory") {
636
+ if (agent) agent.lit = now;
637
+ if (!REDUCED && agent && hub) impulses.push({ path: curvePoints(agent, hub), start: now, dur: 900, color: agent.color, small: false, project: null, targets: [] });
638
+ setTimeout(function () {
639
+ if (hub) flashes[hub.id] = performance.now();
640
+ pendingBloom[p.id] = 1;
641
+ var existing = nodeById["m:" + p.id];
642
+ if (existing) existing.bloom = performance.now();
643
+ if (project) flashes[project.id] = performance.now();
644
+ kick();
645
+ }, agent && hub && !REDUCED ? 900 : 0);
646
+ kick();
647
+ return;
648
+ }
649
+ var key = (agent ? agent.id : "x") + p.type;
650
+ if (lastImpulse[key] && now - lastImpulse[key] < 350) return;
651
+ lastImpulse[key] = now;
652
+ if (agent) agent.lit = now;
653
+ if (!project) { kick(); return; }
654
+ if (REDUCED) { if (hub) flashes[hub.id] = now; flashes[project.id] = now; dirty = true; return; }
655
+ var color = agent ? agent.color : agentColor(p.agent);
656
+ // continue into one or two neurons of the lobe
657
+ var mems = nodes.filter(function (n) { return n.type === "memory" && n.cluster === p.project; });
658
+ var targets = [];
659
+ for (var i = 0; i < Math.min(2, mems.length); i++) targets.push(mems[Math.floor(Math.random() * mems.length)]);
660
+ var dur = p.type === "usage" ? 900 : 1100, firstEnd = now;
661
+ if (agent && hub) {
662
+ impulses.push({ path: curvePoints(agent, hub), start: now, dur: dur, color: color, small: p.type === "usage", project: hub, targets: [] });
663
+ firstEnd += dur;
664
+ }
665
+ if (hub) {
666
+ impulses.push({ path: curvePoints(hub, project), start: firstEnd, dur: dur, color: color, small: p.type === "usage", project: project, targets: [] });
667
+ firstEnd += dur;
668
+ }
669
+ targets.forEach(function (target) {
670
+ impulses.push({ path: curvePoints(project, target), start: firstEnd, dur: dur, color: color, small: p.type === "usage", project: project, targets: [target] });
671
+ });
672
+ if (impulses.length > 60) impulses.splice(0, impulses.length - 60);
673
+ kick();
674
+ }
675
+
676
+ function pointAlong(path, t) {
677
+ var f = t * (path.length - 1), i = Math.min(path.length - 2, Math.floor(f)), r = f - i;
678
+ return [path[i][0] + (path[i + 1][0] - path[i][0]) * r, path[i][1] + (path[i + 1][1] - path[i][1]) * r];
679
+ }
680
+
681
+ // ---- frame
682
+ function draw(now) {
683
+ var t = (now - start) / 1000;
684
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
685
+ ctx.clearRect(0, 0, W, H);
686
+ if (outline) ctx.drawImage(outline, 0, 0, W, H);
687
+
688
+ // synapses: project -> memories
689
+ ctx.lineWidth = 0.7;
690
+ ctx.strokeStyle = "rgba(239,235,228,0.06)";
691
+ ctx.beginPath();
692
+ for (var i = 0; i < memLinks.length; i++) {
693
+ var l = memLinks[i], s = typeof l.source === "object" ? l.source : nodeById[l.source], d = typeof l.target === "object" ? l.target : nodeById[l.target];
694
+ if (!s || !d || s.x === undefined) continue;
695
+ ctx.moveTo(s.x, s.y); ctx.lineTo(d.x, d.y);
696
+ }
697
+ ctx.stroke();
698
+
699
+ memLinks.forEach(function (l) {
700
+ if (l.kind !== "ml") return;
701
+ var s = typeof l.source === "object" ? l.source : nodeById[l.source], d = typeof l.target === "object" ? l.target : nodeById[l.target];
702
+ if (!s || !d || s.x === undefined || d.x === undefined) return;
703
+ var color = LINK_COLORS[l.type] || "#9c9890";
704
+ ctx.strokeStyle = color + "66"; ctx.lineWidth = 0.65;
705
+ ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(d.x, d.y); ctx.stroke();
706
+ if (!REDUCED && /^(blocks|depends_on|supersedes)$/.test(l.type)) {
707
+ var q = (t * 0.18 + hash(l.source + ">" + l.target + l.type)) % 1;
708
+ ctx.fillStyle = color; ctx.beginPath(); ctx.arc(s.x + (d.x - s.x) * q, s.y + (d.y - s.y) * q, 1.2, 0, Math.PI * 2); ctx.fill();
709
+ }
710
+ });
711
+
712
+ // synapses: agent -> brain, then brain -> project
713
+ agentLinks.forEach(function (l) {
714
+ var pts = curvePoints(l.source, l.target);
715
+ ctx.strokeStyle = l.source.color + (l.source.active ? "55" : "26");
716
+ ctx.lineWidth = l.source.active ? 1.3 : 0.9;
717
+ ctx.beginPath();
718
+ ctx.moveTo(pts[0][0], pts[0][1]);
719
+ for (var k = 1; k < pts.length; k++) ctx.lineTo(pts[k][0], pts[k][1]);
720
+ ctx.stroke();
721
+ });
722
+ projectLinks.forEach(function (l) {
723
+ var pts = curvePoints(l.source, l.target);
724
+ ctx.strokeStyle = "rgba(239,235,228,0.18)";
725
+ ctx.lineWidth = 1;
726
+ ctx.beginPath();
727
+ ctx.moveTo(pts[0][0], pts[0][1]);
728
+ for (var k = 1; k < pts.length; k++) ctx.lineTo(pts[k][0], pts[k][1]);
729
+ ctx.stroke();
730
+ });
731
+
732
+ // memories (neurons)
733
+ var anyAnim = false;
734
+ for (var m = 0; m < nodes.length; m++) {
735
+ var n = nodes[m];
736
+ if (n.type !== "memory" || n.x === undefined) continue;
737
+ var age = now - n.born;
738
+ if (age < 0) { anyAnim = true; continue; }
739
+ var appear = Math.min(1, age / 600);
740
+ if (appear < 1) anyAnim = true;
741
+ var tw = REDUCED ? 0.8 : 0.62 + 0.3 * Math.sin(t * (0.6 + (n.phase % 1) * 0.9) + n.phase);
742
+ var lit = n.lit && now - n.lit < 900 ? 1 - (now - n.lit) / 900 : 0;
743
+ if (lit) anyAnim = true;
744
+ var r = n.r * appear * (1 + lit * 0.8);
745
+ ctx.globalAlpha = Math.min(1, (tw + lit) * appear);
746
+ var sz = r * 5;
747
+ ctx.drawImage(sprite(n.color), n.x - sz / 2, n.y - sz / 2, sz, sz);
748
+ ctx.fillStyle = n.color;
749
+ ctx.beginPath(); ctx.arc(n.x, n.y, Math.max(0.6, r * 0.55), 0, Math.PI * 2); ctx.fill();
750
+ if (n.bloom) {
751
+ var b = (now - n.bloom) / 1800;
752
+ if (b < 1) {
753
+ anyAnim = true;
754
+ ctx.globalAlpha = (1 - b) * 0.9;
755
+ ctx.strokeStyle = n.color;
756
+ ctx.lineWidth = 2;
757
+ ctx.beginPath(); ctx.arc(n.x, n.y, n.r + b * 34 * G.s + 4, 0, Math.PI * 2); ctx.stroke();
758
+ var bs = (1 - b) * 40 * G.s + 8;
759
+ ctx.drawImage(sprite(n.color), n.x - bs / 2, n.y - bs / 2, bs, bs);
760
+ } else n.bloom = 0;
761
+ }
762
+ }
763
+ ctx.globalAlpha = 1;
764
+
765
+ // Brain hub
766
+ var hub = nodeById.__brain__;
767
+ if (hub && hub.x !== undefined) {
768
+ var hf = flashes[hub.id] ? (now - flashes[hub.id]) / 1400 : 1;
769
+ if (hf < 1) anyAnim = true; else delete flashes[hub.id];
770
+ var hl = hf < 1 ? 1 - hf : 0;
771
+ ctx.globalAlpha = 0.9 + hl * 0.1;
772
+ var hs = hub.r * (6.8 + hl * 4);
773
+ ctx.drawImage(sprite("#3a3631"), hub.x - hs / 2, hub.y - hs / 2, hs, hs);
774
+ ctx.globalAlpha = 1;
775
+ ctx.fillStyle = "#efebe4";
776
+ ctx.beginPath(); ctx.arc(hub.x, hub.y, hub.r * 0.48, 0, Math.PI * 2); ctx.fill();
777
+ ctx.font = "500 " + Math.max(11, fs - 1) + "px " + FONT;
778
+ ctx.textAlign = "center";
779
+ var hubLabel = "Brain", hubW = ctx.measureText(hubLabel).width;
780
+ ctx.fillStyle = "rgba(3,2,1,0.78)";
781
+ ctx.fillRect(hub.x - hubW / 2 - 5, hub.y + hub.r + 5, hubW + 10, fs + 4);
782
+ ctx.fillStyle = "#efebe4";
783
+ ctx.fillText(hubLabel, hub.x, hub.y + hub.r + 7);
784
+ }
785
+
786
+ // impulses
787
+ for (var q = impulses.length - 1; q >= 0; q--) {
788
+ var im = impulses[q], p = (now - im.start) / im.dur;
789
+ if (p < 0) { anyAnim = true; continue; }
790
+ if (p >= 1) {
791
+ if (im.project) flashes[im.project.id] = now;
792
+ im.targets.forEach(function (tn) { tn.lit = now; });
793
+ impulses.splice(q, 1);
794
+ continue;
795
+ }
796
+ anyAnim = true;
797
+ var e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
798
+ var sp = sprite(im.color), size = (im.small ? 16 : 26) * G.s + 8;
799
+ for (var tr = 0; tr < 5; tr++) {
800
+ var pp = pointAlong(im.path, Math.max(0, e - tr * 0.035));
801
+ ctx.globalAlpha = (1 - tr / 5) * 0.9;
802
+ var ts = size * (1 - tr * 0.13);
803
+ ctx.drawImage(sp, pp[0] - ts / 2, pp[1] - ts / 2, ts, ts);
804
+ }
805
+ }
806
+ ctx.globalAlpha = 1;
807
+
808
+ // projects (lobes)
809
+ var fs = Math.max(11, Math.min(17, 15 * G.s + 4));
810
+ ctx.textAlign = "center";
811
+ ctx.textBaseline = "top";
812
+ ctx.font = "500 " + fs + "px " + FONT;
813
+ nodes.forEach(function (n) {
814
+ if (n.type !== "project" || n.x === undefined) return;
815
+ var f = flashes[n.id] ? (now - flashes[n.id]) / 1400 : 1;
816
+ if (f < 1) anyAnim = true; else delete flashes[n.id];
817
+ var fl = f < 1 ? 1 - f : 0;
818
+ var breathe = REDUCED ? 0 : 0.08 * Math.sin(t * 0.8 + n.phase);
819
+ var gs = n.r * (4.2 + breathe + fl * 4);
820
+ ctx.globalAlpha = 0.55 + fl * 0.45;
821
+ ctx.drawImage(sprite(n.id === "p:other" ? "#24211c" : "#3a3631"), n.x - gs / 2, n.y - gs / 2, gs, gs);
822
+ if (fl) {
823
+ ctx.globalAlpha = fl;
824
+ var fs2 = gs * 1.3;
825
+ ctx.drawImage(sprite("#5c3a22"), n.x - fs2 / 2, n.y - fs2 / 2, fs2, fs2);
826
+ ctx.strokeStyle = "rgba(249,124,61," + fl + ")";
827
+ ctx.lineWidth = 1.5;
828
+ ctx.beginPath(); ctx.arc(n.x, n.y, n.r + (1 - fl) * 40 * G.s, 0, Math.PI * 2); ctx.stroke();
829
+ }
830
+ ctx.globalAlpha = 1;
831
+ ctx.fillStyle = "#cecac3";
832
+ ctx.beginPath(); ctx.arc(n.x, n.y, n.r * 0.5, 0, Math.PI * 2); ctx.fill();
833
+ ctx.fillStyle = "rgba(3,2,1,0.78)";
834
+ var label = n.name.length > 22 ? n.name.slice(0, 21) + "…" : n.name;
835
+ var tw2 = ctx.measureText(label).width;
836
+ ctx.fillRect(n.x - tw2 / 2 - 5, n.y + n.r + 3, tw2 + 10, fs + 4);
837
+ ctx.fillStyle = fl ? "#f97c3d" : "rgba(206,202,195,0.92)";
838
+ ctx.fillText(label, n.x, n.y + n.r + 5);
839
+ });
840
+
841
+ // agents (rim)
842
+ ctx.font = "500 " + Math.max(10, fs - 2) + "px " + FONT;
843
+ nodes.forEach(function (n) {
844
+ if (n.type !== "agent") return;
845
+ var lit = n.lit && now - n.lit < 1200 ? 1 - (now - n.lit) / 1200 : 0;
846
+ if (lit) anyAnim = true;
847
+ var pul = n.active && !REDUCED ? 0.5 + 0.5 * Math.sin(t * 3 + n.phase) : 0.3;
848
+ var as = n.r * (5 + pul * 1.5 + lit * 4);
849
+ ctx.globalAlpha = n.active || n.connected ? 0.9 : 0.45;
850
+ ctx.drawImage(sprite(n.color), n.x - as / 2, n.y - as / 2, as, as);
851
+ ctx.globalAlpha = 1;
852
+ ctx.fillStyle = n.active || lit ? "#efebe4" : n.color;
853
+ ctx.beginPath(); ctx.arc(n.x, n.y, n.r * 0.6, 0, Math.PI * 2); ctx.fill();
854
+ if (n.connected) {
855
+ ctx.strokeStyle = n.color + "99"; ctx.lineWidth = 1;
856
+ ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 3, 0, Math.PI * 2); ctx.stroke();
857
+ }
858
+ var above = n.y < G.cy;
859
+ ctx.textBaseline = above ? "bottom" : "top";
860
+ ctx.fillStyle = n.color;
861
+ ctx.fillText(agentShort(n.agent) + " · " + (n.name || ""), n.x, above ? n.y - n.r - 6 : n.y + n.r + 6);
862
+ });
863
+ ctx.textBaseline = "top";
864
+
865
+ if (hover) drawHover(hover);
866
+ return anyAnim;
867
+ }
868
+
869
+ function drawHover(n) {
870
+ ctx.strokeStyle = "rgba(239,235,228,0.8)";
871
+ ctx.lineWidth = 1.2;
872
+ ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 4, 0, Math.PI * 2); ctx.stroke();
873
+ }
874
+
875
+ function loop(now) {
876
+ if (!running || !active) { running = false; return; }
877
+ requestAnimationFrame(loop);
878
+ var simActive = sim && sim.alpha() > sim.alphaMin();
879
+ var busy = simActive || impulses.length > 0;
880
+ var minGap = busy ? 33 : 50; // ~30 fps while animating, ~20 fps idle twinkle
881
+ if (now - lastFrame < minGap) return;
882
+ lastFrame = now;
883
+ if (simActive) sim.tick();
884
+ var anim = draw(now);
885
+ if (REDUCED && !simActive && !anim && !impulses.length) { running = false; } // static until something changes
886
+ }
887
+
888
+ function kick() {
889
+ dirty = true;
890
+ if (document.hidden || !active) return;
891
+ if (!running) { running = true; requestAnimationFrame(loop); }
892
+ }
893
+
894
+ function resize() {
895
+ var r = wrap.getBoundingClientRect();
896
+ if (r.width < 10 || r.height < 10) return;
897
+ W = r.width; H = r.height;
898
+ DPR = Math.min(window.devicePixelRatio || 1, 1.5);
899
+ canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
900
+ var oldG = G;
901
+ G = geometry();
902
+ renderOutline();
903
+ if (oldG && nodes.length) {
904
+ var sx = G.s / oldG.s;
905
+ nodes.forEach(function (n) {
906
+ if (n.x !== undefined) { n.x = G.cx + (n.x - oldG.cx) * sx; n.y = G.cy + (n.y - oldG.cy) * sx; }
907
+ if (n.type === "memory") n.r = 2.6 * G.s + 1;
908
+ if (n.type === "agent") n.r = 6 * G.s + 2.5;
909
+ });
910
+ assignAnchors();
911
+ buildSim(0.15);
912
+ }
913
+ if (pendingGraph) { var pg = pendingGraph; pendingGraph = null; setGraph(pg); }
914
+ kick();
915
+ }
916
+
917
+ // hover + click
918
+ canvas.addEventListener("mousemove", function (e) {
919
+ var r = canvas.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top;
920
+ var best = null, bd = 14 * 14;
921
+ nodes.forEach(function (n) {
922
+ if (n.x === undefined || n.type === "hub") return;
923
+ var d = (n.x - x) * (n.x - x) + (n.y - y) * (n.y - y);
924
+ if (d < Math.max(bd, n.r * n.r)) { bd = d; best = n; }
925
+ });
926
+ hover = best;
927
+ if (!best) { tip.hidden = true; canvas.style.cursor = ""; kick(); return; }
928
+ canvas.style.cursor = best.type === "project" && best.path ? "pointer" : "";
929
+ var html = best.type === "memory" ? '<b style="color:' + best.color + '">' + esc(best.kind) + "</b> " + esc(best.title) +
930
+ '<div class="muted">' + esc(best.project || "global") + "</div>"
931
+ : best.type === "project" ? "<b>" + esc(best.name) + '</b><div class="muted">' + (best.memories || 0) + " memories · " + (best.activity || 0) + " events 7d</div>"
932
+ : '<b style="color:' + best.color + '">' + esc(best.agent) + "</b> · " + esc(best.name || "") +
933
+ '<div class="muted">' + (best.active ? "active" : "idle") + (best.connected ? " · mcp connected" : "") + "</div>";
934
+ tip.innerHTML = html;
935
+ tip.hidden = false;
936
+ tip.style.left = Math.min(W - 330, x + 14) + "px";
937
+ tip.style.top = Math.min(H - 60, y + 12) + "px";
938
+ kick();
939
+ });
940
+ canvas.addEventListener("mouseleave", function () { hover = null; tip.hidden = true; kick(); });
941
+ canvas.addEventListener("click", function () {
942
+ if (hover && hover.type === "project" && hover.path) location.href = "/projects/detail?path=" + encodeURIComponent(hover.path);
943
+ if (hover && hover.type === "memory") location.href = "/memory?q=" + encodeURIComponent(hover.title || "");
944
+ });
945
+
946
+ document.addEventListener("visibilitychange", function () {
947
+ if (document.hidden) running = false; else kick();
948
+ });
949
+ if (window.ResizeObserver) new ResizeObserver(function () { resize(); }).observe(wrap);
950
+ else window.addEventListener("resize", resize);
951
+
952
+ function setActive(on) {
953
+ active = on;
954
+ canvas.hidden = !on;
955
+ if (!on) { tip.hidden = true; running = false; } else { resize(); kick(); }
956
+ }
957
+
958
+ resize();
959
+ return { setGraph: setGraph, pulse: pulse, resize: resize, setActive: setActive };
960
+ })();
961
+
962
+ // ------------------------------------------------------------------ 2D / 3D switch
963
+
964
+ // Glow is owned by brain3d.js; show the saved state even before (or without) the 3D view starting.
965
+ (function () {
966
+ var glow = $("glow-toggle"), on = false;
967
+ if (!glow) return;
968
+ try { on = localStorage.getItem("devmem.bloom") === "1"; } catch (e) { /* default off */ }
969
+ glow.classList.toggle("on", on);
970
+ glow.setAttribute("aria-pressed", on ? "true" : "false");
971
+ glow.textContent = on ? "● Glow on" : "○ Glow off";
972
+ })();
973
+
974
+ var Brain = (function () {
975
+ var mode = "3d", impl3d = null, lastGraph = null, reason = "";
976
+ var btn = $("dim-toggle"), box3d = $("brain3d");
977
+ try { mode = localStorage.getItem("devmem.brainMode") === "2d" ? "2d" : "3d"; } catch (e) { /* default 3D */ }
978
+
979
+ function ensure3d() {
980
+ if (impl3d) return true;
981
+ if (!window.DevmemBrain3D || !window.ForceGraph3D) { reason = "3D library did not load"; return false; }
982
+ if (!window.DevmemBrain3D.available()) { reason = "WebGL unavailable"; return false; }
983
+ try {
984
+ box3d.hidden = false;
985
+ impl3d = window.DevmemBrain3D.create(box3d, { agentColor: agentColor, agentShort: agentShort, esc: esc, reduced: REDUCED });
986
+ if (lastGraph) impl3d.setGraph(lastGraph);
987
+ return true;
988
+ } catch (err) {
989
+ console.error("devmem 3D failed, using 2D", err);
990
+ reason = "3D failed to start";
991
+ box3d.hidden = true;
992
+ impl3d = null;
993
+ return false;
994
+ }
995
+ }
996
+
997
+ function apply(wanted, remember) {
998
+ var use3d = wanted === "3d" && ensure3d();
999
+ mode = use3d ? "3d" : "2d";
1000
+ box3d.hidden = !use3d;
1001
+ $("brain-wrap").classList.toggle("is3d", use3d);
1002
+ if (impl3d) impl3d.setActive(use3d);
1003
+ if (use3d && lastGraph) impl3d.setGraph(lastGraph); // merge by id: positions are kept
1004
+ Brain2D.setActive(!use3d);
1005
+ if (!use3d && lastGraph) Brain2D.setGraph(lastGraph);
1006
+ btn.innerHTML = '<span class="seg' + (use3d ? "" : " active") + '">2D</span><span class="seg' + (use3d ? " active" : "") + '">3D</span>';
1007
+ document.body.classList.toggle("view-2d", !use3d);
1008
+ btn.title = use3d ? "Showing the 3D web; click for the flat 2D brain" :
1009
+ (wanted === "3d" && reason ? "3D unavailable (" + reason + "); showing 2D" : "Showing the 2D brain; click for the 3D web");
1010
+ btn.classList.toggle("unavailable", wanted === "3d" && !use3d);
1011
+ if (remember) { try { localStorage.setItem("devmem.brainMode", wanted); } catch (e) { /* ignore */ } }
1012
+ }
1013
+
1014
+ btn.addEventListener("click", function () { apply(mode === "3d" ? "2d" : "3d", true); });
1015
+ apply(mode, false);
1016
+
1017
+ return {
1018
+ setGraph: function (g) {
1019
+ lastGraph = g;
1020
+ if (mode === "3d" && impl3d) impl3d.setGraph(g); else Brain2D.setGraph(g);
1021
+ },
1022
+ pulse: function (p) { if (mode === "3d" && impl3d) impl3d.pulse(p); else Brain2D.pulse(p); },
1023
+ resize: function () { if (mode === "3d" && impl3d) impl3d.resize(); else Brain2D.resize(); },
1024
+ mode: function () { return mode; },
1025
+ };
1026
+ })();
1027
+ window.devmemBrain = Brain;
1028
+
1029
+ // ------------------------------------------------------------------ state + stream
1030
+
1031
+ var lastState = null;
1032
+
1033
+ function applyState(s) {
1034
+ lastState = Object.assign({}, lastState || {}, s);
1035
+ if (s.graph) Brain.setGraph(s.graph);
1036
+ renderLimits(s.limits || []);
1037
+ renderAgents(s.agents || {});
1038
+ renderFeed(s.activity || []);
1039
+ renderCounts(s.counts || {});
1040
+ if (Object.prototype.hasOwnProperty.call(s, "reminders")) {
1041
+ var serverReminders = {};
1042
+ (s.reminders || []).forEach(function (item) { serverReminders[item.id] = true; });
1043
+ Object.keys(finishedReminders).forEach(function (id) {
1044
+ if (!serverReminders[id]) delete finishedReminders[id];
1045
+ });
1046
+ renderReminders(s.reminders);
1047
+ }
1048
+ }
1049
+
1050
+ var es = null, lastMessage = 0, backoff = 1000;
1051
+
1052
+ function setStatus(state, text) {
1053
+ var el = $("status");
1054
+ el.dataset.state = state;
1055
+ el.querySelector(".txt").textContent = text;
1056
+ }
1057
+
1058
+ function disconnect() { if (es) { es.onerror = null; es.close(); es = null; } }
1059
+ function connect() {
1060
+ disconnect();
1061
+ if (document.hidden) return;
1062
+ setStatus("connecting", "connecting");
1063
+ es = new EventSource("/api/stream");
1064
+ es.addEventListener("state", function (e) {
1065
+ lastMessage = Date.now(); backoff = 1000;
1066
+ setStatus("live", "live");
1067
+ try { applyState(JSON.parse(e.data)); } catch (err) { console.error(err); }
1068
+ });
1069
+ es.addEventListener("messages", function (e) {
1070
+ lastMessage = Date.now();
1071
+ try { renderMessages(JSON.parse(e.data)); } catch (err) { console.error(err); }
1072
+ });
1073
+ es.addEventListener("pulse", function (e) {
1074
+ lastMessage = Date.now();
1075
+ try {
1076
+ var pulses = JSON.parse(e.data);
1077
+ pulses.forEach(Brain.pulse);
1078
+ if (pulses.some(function (pulse) { return pulse.type === "ado"; })) refreshAdoCard();
1079
+ } catch (err) { console.error(err); }
1080
+ });
1081
+ es.onerror = function () {
1082
+ setStatus("offline", "offline · reconnecting");
1083
+ if (es.readyState === EventSource.CLOSED) {
1084
+ setTimeout(connect, backoff);
1085
+ backoff = Math.min(backoff * 2, 15000);
1086
+ }
1087
+ };
1088
+ }
1089
+
1090
+ refreshAdoCard();
1091
+ setInterval(refreshAdoCard, 30000);
1092
+
1093
+ // Watchdog: a half-open connection (laptop sleep, server restart) sends nothing; reconnect it.
1094
+ setInterval(function () {
1095
+ if (!es) return; // hidden: no stream on purpose
1096
+ if (lastMessage && Date.now() - lastMessage > 12000) {
1097
+ setStatus("offline", "offline · reconnecting");
1098
+ lastMessage = Date.now();
1099
+ connect();
1100
+ }
1101
+ }, 3000);
1102
+
1103
+ // fix(2026-10-28): browsers allow 6 connections per site. A stream left open by a background tab or by a page kept for Back used
1104
+ // one up each, until new pages hung loading forever. Only the visible page keeps its stream.
1105
+ window.addEventListener("pagehide", disconnect);
1106
+ window.addEventListener("pageshow", function (e) { if (e.persisted) connect(); });
1107
+ document.addEventListener("visibilitychange", function () { if (document.hidden) disconnect(); else if (!es) { lastMessage = Date.now(); connect(); } });
1108
+
1109
+ setInterval(function () {
1110
+ var d = new Date();
1111
+ $("clock").textContent = d.toLocaleDateString(undefined, { weekday: "short", day: "numeric", month: "short" }) + " " +
1112
+ d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
1113
+ tickGauges();
1114
+ tickRelTimes();
1115
+ }, 1000);
1116
+
1117
+ connect();
1118
+ })();