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,600 @@
1
+ // devmem 3D brain: a glowing neural web on 3d-force-graph (WebGL / three.js).
2
+ //
3
+ // The pinned 3d-force-graph bundle does not expose THREE. Its second three.js copy must stay coupled to r162, so the few classes needed for
4
+ // canvas-texture sprites (labels, glow halos) are taken from objects the library itself creates:
5
+ // a probe node's default mesh and the post-processing composer's render target. If that fails the web
6
+ // still renders with the library's default spheres and hover tooltips.
7
+ (function () {
8
+ "use strict";
9
+
10
+ var KIND_COLORS = { note: "#88a8c9", decision: "#aa9dc5", preference: "#c7959c", snippet: "#90b697", todo: "#ccb484" };
11
+ var LINK_COLORS = { blocks: "#f47c70", depends_on: "#ccb484", relates_to: "#9c9890", overlaps: "#66635d", fixes: "#90b697", supersedes: "#4a463f" };
12
+ var R = 120; // overall web radius (world units)
13
+ var ADDITIVE = 2; // THREE.AdditiveBlending
14
+
15
+ function webglAvailable() {
16
+ try {
17
+ var c = document.createElement("canvas");
18
+ return !!(window.WebGLRenderingContext && (c.getContext("webgl2") || c.getContext("webgl")));
19
+ } catch (e) { return false; }
20
+ }
21
+
22
+ function create(container, opts) {
23
+ var agentColor = opts.agentColor, agentShort = opts.agentShort, esc = opts.esc, REDUCED = !!opts.reduced;
24
+ var C = null; // harvested three.js constructors
25
+ var Graph = ForceGraph3D({ controlType: "orbit", rendererConfig: { antialias: true, alpha: false, powerPreference: "low-power" } })(container);
26
+ var nodes = [], links = [], nodeById = {}, linkByKey = {};
27
+ var pendingGraph = null, pendingBloom = {}, anims = [], lastImpulse = {};
28
+ var active = true, ready = false, fitted = false, resumeTimer = null, loopOn = false, lastTwinkle = 0;
29
+ var texCache = {}, geomCache = {}, lastUserMove = 0, fitTimer = null;
30
+ var bloomPass = null, bloomAdded = false, bloomEnabled = false, bloomError = "";
31
+
32
+ try { bloomEnabled = localStorage.getItem("devmem.bloom") === "1"; } catch (e) { /* default off */ }
33
+
34
+ function bloomActive() { return !!(bloomPass && bloomPass.enabled); }
35
+
36
+ function haloBaseSize(n) {
37
+ var size = n.radius * (n.type === "hub" ? 10 : n.type === "project" ? 7 : n.type === "agent" ? 9 : 6);
38
+ return size * (bloomActive() ? 0.62 : 0.5);
39
+ }
40
+
41
+ function haloOpacity(n) { return n.baseGlow * (bloomActive() ? 0.42 : 0.3); }
42
+
43
+ function syncHalos() {
44
+ for (var i = 0; i < nodes.length; i++) {
45
+ var n = nodes[i], parts = n.parts;
46
+ if (!parts || !parts.halo || isAnimating(n)) continue;
47
+ parts.halo.material.opacity = haloOpacity(n);
48
+ parts.halo.scale.setScalar(haloBaseSize(n));
49
+ }
50
+ }
51
+
52
+ function resizeBloom(width, height) {
53
+ if (!bloomPass || !bloomPass.setSize) return;
54
+ // UnrealBloomPass halves internally; pass CSS canvas size times renderer pixel ratio for real half-resolution bloom.
55
+ var ratio = Graph.renderer && Graph.renderer().getPixelRatio ? Graph.renderer().getPixelRatio() : window.devicePixelRatio || 1;
56
+ bloomPass.setSize(Math.max(1, Math.ceil(width * ratio)), Math.max(1, Math.ceil(height * ratio)));
57
+ }
58
+
59
+ function ensureBloom() {
60
+ if (bloomAdded || !window.__devmemBloom) return;
61
+ try {
62
+ var composer = Graph.postProcessingComposer && Graph.postProcessingComposer();
63
+ if (!composer || !composer.addPass) { bloomError = "3D graph has no post-processing composer"; setGlow(bloomEnabled); return; }
64
+ // A plain x/y object is sufficient; UnrealBloomPass copies it into its own Vector2.
65
+ bloomPass = new window.__devmemBloom({ x: 1, y: 1 }, 0.75, 0.45, 0.25);
66
+ bloomPass.enabled = bloomEnabled;
67
+ composer.addPass(bloomPass);
68
+ bloomAdded = true;
69
+ var rect = container.getBoundingClientRect();
70
+ resizeBloom(rect.width, rect.height);
71
+ syncHalos();
72
+ } catch (e) {
73
+ // The graph and its halo fallback remain usable if the module/composer is incompatible.
74
+ bloomPass = null;
75
+ bloomError = String((e && e.message) || e);
76
+ console.warn("devmem: bloom unavailable", e);
77
+ }
78
+ setGlow(bloomEnabled);
79
+ }
80
+
81
+ function bloomFailed() {
82
+ bloomError = window.__devmemBloomError || "bloom module failed to load";
83
+ console.warn("devmem: bloom unavailable:", bloomError);
84
+ setGlow(bloomEnabled);
85
+ }
86
+
87
+ function setGlow(on) {
88
+ bloomEnabled = on;
89
+ try { localStorage.setItem("devmem.bloom", on ? "1" : "0"); } catch (e) { /* storage unavailable */ }
90
+ if (bloomPass) bloomPass.enabled = on;
91
+ syncHalos();
92
+ var button = document.getElementById("glow-toggle");
93
+ if (button) {
94
+ button.setAttribute("aria-pressed", on ? "true" : "false");
95
+ button.classList.toggle("on", on);
96
+ button.textContent = on ? "● Glow on" : "○ Glow off";
97
+ button.title = on ? "Turn off 3D bloom glow (3D view only)" : "Turn on 3D bloom glow (3D view only)";
98
+ // Make a missing bloom effect visible instead of a toggle that silently does nothing.
99
+ button.classList.toggle("unavailable", !bloomPass);
100
+ if (!bloomPass) button.title = "Glow effect not active: " + (bloomError || "bloom module still loading");
101
+ }
102
+ }
103
+
104
+ Graph.renderer().setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
105
+ Graph.backgroundColor("#030201")
106
+ .showNavInfo(false)
107
+ .enableNodeDrag(false)
108
+ .nodeId("id")
109
+ .warmupTicks(40)
110
+ .cooldownTime(12000)
111
+ .d3AlphaDecay(0.025)
112
+ .d3VelocityDecay(0.32)
113
+ .linkOpacity(0.32)
114
+ .linkWidth(0)
115
+ .linkColor(function (l) { return l.color; })
116
+ .linkDirectionalParticles(function (l) { return l.particles || 0; })
117
+ .linkDirectionalParticleColor(function (l) { return l.pcolor || "#efebe4"; })
118
+ .linkDirectionalParticleWidth(function (l) { return l.pwidth || 1.6; })
119
+ .linkDirectionalParticleSpeed(function (l) { return l.kind === "ah" ? 0.016 : 0.035; })
120
+ .linkDirectionalParticleResolution(6)
121
+ .nodeLabel(tooltip)
122
+ .onNodeClick(function (n) {
123
+ if (n.type === "project" && n.path) location.href = "/projects/detail?path=" + encodeURIComponent(n.path);
124
+ else if (n.type === "memory") location.href = "/memory?q=" + encodeURIComponent(n.title || "");
125
+ })
126
+ .onEngineStop(function () {
127
+ fit(900);
128
+ });
129
+
130
+ var glowButton = document.getElementById("glow-toggle");
131
+ if (glowButton) glowButton.addEventListener("click", function () { setGlow(!bloomEnabled); });
132
+ setGlow(bloomEnabled);
133
+ window.addEventListener("devmem-bloom", ensureBloom);
134
+ window.addEventListener("devmem-bloom-failed", bloomFailed);
135
+ if (window.__devmemBloomError) bloomFailed();
136
+ ensureBloom();
137
+
138
+ // ---------------------------------------------------------------- forces: web, clusters, loose brain shape
139
+ var shapeNodes = [];
140
+ function shapeForce(alpha) {
141
+ for (var i = 0; i < shapeNodes.length; i++) {
142
+ var n = shapeNodes[i];
143
+ if (n.fx !== undefined && n.fx !== null) continue;
144
+ // ellipsoid: wider left-right, like a brain seen from the front
145
+ var ex = n.x / (R * 1.35), ey = n.y / (R * 0.95), ez = n.z / (R * 1.05);
146
+ var r = Math.sqrt(ex * ex + ey * ey + ez * ez) || 1e-6;
147
+ if (n.type === "agent") {
148
+ var shell = 1.45; // agents float on the outside
149
+ var k = (shell - r) * 0.08 * alpha;
150
+ n.vx += n.x / r * k; n.vy += n.y / r * k; n.vz += n.z / r * k;
151
+ } else if (r > 1) {
152
+ var k2 = (r - 1) * 0.12 * alpha;
153
+ n.vx -= n.x * k2; n.vy -= n.y * k2; n.vz -= n.z * k2;
154
+ }
155
+ if (n.type === "project") { // two loose hemispheres
156
+ var side = n.side || 1;
157
+ n.vx += (side * R * 0.5 - n.x) * 0.02 * alpha;
158
+ }
159
+ }
160
+ }
161
+ shapeForce.initialize = function (ns) { shapeNodes = ns; };
162
+ Graph.d3Force("shape", shapeForce);
163
+ Graph.d3Force("charge").strength(function (n) {
164
+ return n.type === "project" ? -140 : n.type === "agent" ? -60 : n.type === "hub" ? -30 : -9;
165
+ });
166
+ Graph.d3Force("link")
167
+ .distance(function (l) { return l.kind === "ah" ? 85 : l.kind === "hp" ? 62 : l.kind === "hm" ? 26 : l.kind === "ml" ? 34 : 14 + (l.spread || 0); })
168
+ .strength(function (l) { return l.kind === "ah" ? 0.04 : l.kind === "hp" ? 0.16 : l.kind === "ml" ? 0.07 : 0.55; });
169
+
170
+ // ---------------------------------------------------------------- auto-rotation, paused while the user orbits/zooms
171
+ var controls = Graph.controls();
172
+ if (controls) {
173
+ controls.autoRotate = !REDUCED;
174
+ controls.autoRotateSpeed = 0.55;
175
+ controls.enableDamping = true;
176
+ controls.dampingFactor = 0.08;
177
+ if (controls.addEventListener) {
178
+ controls.addEventListener("start", function () {
179
+ controls.autoRotate = false;
180
+ clearTimeout(resumeTimer);
181
+ });
182
+ controls.addEventListener("end", function () {
183
+ clearTimeout(resumeTimer);
184
+ if (!REDUCED) resumeTimer = setTimeout(function () { controls.autoRotate = true; }, 6000);
185
+ });
186
+ }
187
+ }
188
+
189
+ // ---------------------------------------------------------------- harvest three.js classes from a probe node
190
+ function harvest() {
191
+ try {
192
+ var probe = Graph.graphData().nodes[0];
193
+ var mesh = probe && probe.__threeObj;
194
+ var comp = Graph.postProcessingComposer();
195
+ var texture = comp && (comp.renderTarget1 || comp.readBuffer) && (comp.renderTarget1 || comp.readBuffer).texture;
196
+ if (!mesh || !mesh.geometry || !mesh.material || !texture) return false;
197
+ var Geo = mesh.geometry.constructor;
198
+ C = {
199
+ Mesh: mesh.constructor,
200
+ Object3D: Object.getPrototypeOf(mesh.constructor.prototype).constructor,
201
+ Sphere: Geo,
202
+ BufferGeometry: Object.getPrototypeOf(Geo.prototype).constructor,
203
+ Attr: mesh.geometry.getAttribute("position").constructor,
204
+ Material: mesh.material.constructor,
205
+ Texture: texture.constructor,
206
+ };
207
+ // smoke-construct once so a mismatch falls back instead of failing later
208
+ new C.Mesh(new C.Sphere(1, 6, 4), new C.Material({ color: 0xffffff }));
209
+ return true;
210
+ } catch (e) {
211
+ C = null;
212
+ return false;
213
+ }
214
+ }
215
+
216
+ function bootstrap() {
217
+ Graph.graphData({ nodes: [{ id: "__probe" }], links: [] });
218
+ var tries = 0;
219
+ (function poll() {
220
+ tries++;
221
+ var probe = Graph.graphData().nodes[0];
222
+ if ((probe && probe.__threeObj) || tries > 90) {
223
+ if (!harvest()) console.warn("devmem 3D: sprite classes unavailable; using default spheres");
224
+ if (C) Graph.nodeThreeObject(makeNode);
225
+ else Graph.nodeVal(function (n) { return n.val; }).nodeColor(function (n) { return n.color; }).nodeOpacity(0.9);
226
+ ready = true;
227
+ Graph.graphData({ nodes: [], links: [] });
228
+ if (pendingGraph) { var g = pendingGraph; pendingGraph = null; setGraph(g); }
229
+ return;
230
+ }
231
+ requestAnimationFrame(poll);
232
+ })();
233
+ }
234
+
235
+ // ---------------------------------------------------------------- node objects
236
+ function canvasTexture(canvas) {
237
+ var t = new C.Texture(canvas);
238
+ if ("colorSpace" in t) t.colorSpace = "srgb";
239
+ t.needsUpdate = true;
240
+ return t;
241
+ }
242
+
243
+ function haloTexture() {
244
+ if (texCache.halo) return texCache.halo;
245
+ var c = document.createElement("canvas"), s = 64;
246
+ c.width = c.height = s;
247
+ var x = c.getContext("2d"), g = x.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);
248
+ g.addColorStop(0, "rgba(255,255,255,1)");
249
+ g.addColorStop(0.18, "rgba(255,255,255,0.55)");
250
+ g.addColorStop(0.5, "rgba(255,255,255,0.12)");
251
+ g.addColorStop(1, "rgba(255,255,255,0)");
252
+ x.fillStyle = g;
253
+ x.fillRect(0, 0, s, s);
254
+ return (texCache.halo = canvasTexture(c));
255
+ }
256
+
257
+ function labelTexture(text, color) {
258
+ var key = text + "|" + color;
259
+ if (texCache[key]) return texCache[key];
260
+ var c = document.createElement("canvas"), x = c.getContext("2d"), fs = 44;
261
+ x.font = "500 " + fs + "px Switzer, system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
262
+ var w = Math.ceil(x.measureText(text).width) + 28;
263
+ c.width = w; c.height = fs + 22;
264
+ x.font = "500 " + fs + "px Switzer, system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
265
+ x.fillStyle = "rgba(3,2,1,0.78)";
266
+ roundRect(x, 0, 0, w, c.height, 14);
267
+ x.fill();
268
+ x.fillStyle = color;
269
+ x.textBaseline = "middle";
270
+ x.fillText(text, 14, c.height / 2 + 2);
271
+ var t = canvasTexture(c);
272
+ t.aspect = w / c.height;
273
+ return (texCache[key] = t);
274
+ }
275
+
276
+ function roundRect(x, l, t, w, h, r) {
277
+ x.beginPath();
278
+ x.moveTo(l + r, t); x.lineTo(l + w - r, t); x.quadraticCurveTo(l + w, t, l + w, t + r);
279
+ x.lineTo(l + w, t + h - r); x.quadraticCurveTo(l + w, t + h, l + w - r, t + h);
280
+ x.lineTo(l + r, t + h); x.quadraticCurveTo(l, t + h, l, t + h - r);
281
+ x.lineTo(l, t + r); x.quadraticCurveTo(l, t, l + r, t);
282
+ x.closePath();
283
+ }
284
+
285
+ function planeGeometry() {
286
+ if (geomCache.plane) return geomCache.plane;
287
+ var g = new C.BufferGeometry();
288
+ g.setAttribute("position", new C.Attr(new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0]), 3));
289
+ g.setAttribute("normal", new C.Attr(new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]), 3));
290
+ g.setAttribute("uv", new C.Attr(new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), 2));
291
+ g.setIndex([0, 1, 2, 0, 2, 3]);
292
+ return (geomCache.plane = g);
293
+ }
294
+
295
+ function sphereGeometry(r, detail) {
296
+ var key = r + ":" + detail;
297
+ return geomCache[key] || (geomCache[key] = new C.Sphere(r, detail, Math.max(4, detail - 4)));
298
+ }
299
+
300
+ function billboard(renderer, scene, camera) {
301
+ this.quaternion.copy(camera.quaternion);
302
+ this.updateMatrixWorld();
303
+ }
304
+
305
+ // A camera-facing quad. Lambert with black diffuse + emissive map behaves like an unlit sprite.
306
+ function quad(tex, color, size, additive, opacity) {
307
+ var m = new C.Material({ color: 0x000000, emissive: color, emissiveMap: tex, map: tex, transparent: true,
308
+ depthWrite: false, opacity: opacity });
309
+ if (additive) m.blending = ADDITIVE;
310
+ var mesh = new C.Mesh(planeGeometry(), m);
311
+ mesh.scale.set(size * (tex.aspect || 1), size, 1);
312
+ mesh.onBeforeRender = billboard;
313
+ mesh.renderOrder = additive ? 1 : 2;
314
+ return mesh;
315
+ }
316
+
317
+ function makeNode(n) {
318
+ var g = new C.Object3D();
319
+ var color = n.color;
320
+ var core = new C.Mesh(sphereGeometry(n.radius, n.type === "memory" ? 8 : 16),
321
+ new C.Material({ color: color, emissive: color, emissiveIntensity: n.type === "hub" ? 1.5 : 0.85, transparent: true,
322
+ opacity: n.type === "hub" ? 0.98 : 0.95 }));
323
+ g.add(core);
324
+ var halo = quad(haloTexture(), color, haloBaseSize(n), true, haloOpacity(n));
325
+ g.add(halo);
326
+ var label = null;
327
+ if (n.type === "project" || n.type === "agent" || n.type === "hub") {
328
+ var text = n.type === "agent" ? agentShort(n.agent) + " · " + (n.name || "") : n.name;
329
+ if (text.length > 26) text = text.slice(0, 25) + "…";
330
+ label = quad(labelTexture(text, n.type === "agent" ? color : "#cecac3"), 0xffffff, n.type === "agent" ? 5 : n.type === "hub" ? 7.5 : 6.5, false, 0.95);
331
+ label.position.y = n.radius + (n.type === "agent" ? 6 : n.type === "hub" ? 10 : 8);
332
+ g.add(label);
333
+ }
334
+ n.parts = { core: core, halo: halo, label: label, group: g };
335
+ if (n.bloomAt) g.scale.setScalar(0.001);
336
+ return g;
337
+ }
338
+
339
+ function tooltip(n) {
340
+ if (n.type === "memory") return '<b style="color:' + n.color + '">' + esc(n.kind) + "</b> " + esc(n.title) + "<br><small>" + esc(n.project || "global") + "</small>";
341
+ if (n.type === "project") return "<b>" + esc(n.name) + "</b><br><small>" + (n.memories || 0) + " memories · " + (n.activity || 0) + " events 7d</small>";
342
+ if (n.type === "agent") return '<b style="color:' + n.color + '">' + esc(n.agent) + "</b> · " + esc(n.name || "") + "<br><small>" +
343
+ (n.active ? "active" : "idle") + (n.connected ? " · mcp connected" : "") + "</small>";
344
+ return "<b>Brain</b><br><small>where your knowledge meets the models</small>";
345
+ }
346
+
347
+ // ---------------------------------------------------------------- graph merge (keeps positions and three objects)
348
+ function setGraph(g) {
349
+ if (!ready) { pendingGraph = g; return; }
350
+ var now = performance.now();
351
+ var next = [], byId = {};
352
+ function keep(id, props) {
353
+ var n = nodeById[id] || { id: id, phase: Math.random() * Math.PI * 2 };
354
+ Object.assign(n, props);
355
+ next.push(n); byId[id] = n;
356
+ return n;
357
+ }
358
+ keep("__brain__", { type: "hub", name: "Brain", radius: 12, color: "#efebe4", baseGlow: 1, val: 18, fx: 0, fy: 0, fz: 0 });
359
+ var side = 1;
360
+ (g.projects || []).forEach(function (p) {
361
+ var n = keep("p:" + p.id, { type: "project", project: p.id, name: p.name, path: p.path, memories: p.memories, activity: p.activity,
362
+ radius: Math.min(9, 4.2 + Math.sqrt(p.memories || 0) * 0.7), color: p.id === "other" ? "#66635d" : "#9c9890", baseGlow: 0.55, val: 10 });
363
+ if (!n.side) { n.side = side; side = -side; }
364
+ if (n.x === undefined) { n.x = n.side * R * 0.5 + (Math.random() - 0.5) * 40; n.y = (Math.random() - 0.5) * 60; n.z = (Math.random() - 0.5) * 60; }
365
+ });
366
+ (g.memories || []).forEach(function (m) {
367
+ var fresh = !nodeById["m:" + m.id];
368
+ var n = keep("m:" + m.id, { type: "memory", mid: m.id, title: m.title, kind: m.kind, cluster: m.cluster, project: m.project,
369
+ radius: 1.5, color: KIND_COLORS[m.kind] || "#9c9890", baseGlow: 0.42, val: 1 });
370
+ if (fresh) {
371
+ var parent = byId[m.cluster === "global" ? "__brain__" : "p:" + m.cluster];
372
+ if (parent && parent.x !== undefined) {
373
+ n.x = parent.x + (Math.random() - 0.5) * 8; n.y = parent.y + (Math.random() - 0.5) * 8; n.z = parent.z + (Math.random() - 0.5) * 8;
374
+ }
375
+ if (nodes.length) { // bloom in, except on the first load
376
+ n.bloomAt = now; anims.push({ node: n, kind: "bloom", start: now, dur: 1100 }); }
377
+ delete pendingBloom[m.id];
378
+ }
379
+ });
380
+ (g.agents || []).forEach(function (a) {
381
+ keep("a:" + a.id, { type: "agent", agent: a.agent, session: a.session_id, project: a.project, name: a.project_name,
382
+ active: a.active, connected: a.connected, radius: 3.2, color: agentColor(a.agent), baseGlow: a.active ? 0.9 : 0.5, val: 4 });
383
+ });
384
+ var nextLinks = [], nextKeys = {};
385
+ function link(src, dst, kind, color, spread, suffix) {
386
+ var key = src + ">" + dst + (suffix || "");
387
+ var l = linkByKey[key] || { source: src, target: dst };
388
+ l.kind = kind; l.color = color; l.spread = spread || 0; l.particles = 0;
389
+ nextLinks.push(l); nextKeys[key] = l;
390
+ }
391
+ var perCluster = {};
392
+ next.forEach(function (n) {
393
+ if (n.type === "memory") {
394
+ var parent = n.cluster === "global" ? "__brain__" : "p:" + n.cluster;
395
+ if (!byId[parent]) return;
396
+ var k = perCluster[parent] = (perCluster[parent] || 0) + 1;
397
+ link(parent, n.id, n.cluster === "global" ? "hm" : "pm", n.cluster === "global" ? "#4a463f" : "#3a3631", Math.sqrt(k) * 2.2);
398
+ } else if (n.type === "agent") {
399
+ link(n.id, "__brain__", "ah", n.color);
400
+ } else if (n.type === "project") {
401
+ link("__brain__", n.id, "hp", "#4a463f");
402
+ }
403
+ });
404
+ (g.links || []).forEach(function (l) {
405
+ if (!byId["m:" + l.from_id] || !byId["m:" + l.to_id]) return;
406
+ link("m:" + l.from_id, "m:" + l.to_id, "ml", LINK_COLORS[l.type] || "#9c9890", 0, ":" + l.type);
407
+ var edge = nextLinks[nextLinks.length - 1];
408
+ edge.particles = /^(blocks|depends_on|supersedes)$/.test(l.type) ? 1 : 0;
409
+ edge.pwidth = 0.8; edge.pcolor = edge.color;
410
+ });
411
+ nodes = next; nodeById = byId; links = nextLinks; linkByKey = nextKeys;
412
+ syncHalos();
413
+ Graph.graphData({ nodes: nodes, links: links });
414
+ if (!fitted) fitSoon(1500);
415
+ startLoop();
416
+ }
417
+
418
+ // ---------------------------------------------------------------- pulses
419
+ function findAgent(p) {
420
+ var exact = nodeById["a:" + p.agent + ":" + p.session_id];
421
+ if (exact) return exact;
422
+ var fam = String(p.agent || "").indexOf("codex") >= 0 ? "codex" : "claude", best = null;
423
+ nodes.forEach(function (n) {
424
+ if (n.type !== "agent") return;
425
+ var nf = n.agent.indexOf("codex") >= 0 ? "codex" : "claude";
426
+ if (nf === fam && (!best || n.project === p.project)) best = n;
427
+ });
428
+ return best;
429
+ }
430
+
431
+ function flash(node, dur) {
432
+ if (!node) return;
433
+ anims.push({ node: node, kind: "flash", start: performance.now(), dur: dur || 1300 });
434
+ startLoop();
435
+ }
436
+
437
+ function emit(l, color, width) {
438
+ if (!l || REDUCED) return;
439
+ l.pcolor = color; l.pwidth = width;
440
+ try { Graph.emitParticle(l); } catch (e) { /* link not yet laid out */ }
441
+ }
442
+
443
+ function pulse(p) {
444
+ if (!ready) return;
445
+ var hub = nodeById.__brain__, project = p.project ? nodeById["p:" + p.project] : null;
446
+ var agent = findAgent(p), now = performance.now();
447
+ if (p.type === "ado") {
448
+ flash(hub, 1600);
449
+ return;
450
+ }
451
+ if (p.type === "memory") {
452
+ if (agent) flash(agent, 900);
453
+ var memoryLink = agent && hub && linkByKey[agent.id + ">" + hub.id];
454
+ emit(memoryLink, agent ? agent.color : agentColor(p.agent), 2.6);
455
+ setTimeout(function () {
456
+ flash(hub, 1300);
457
+ pendingBloom[p.id] = 1;
458
+ var existing = nodeById["m:" + p.id];
459
+ if (existing) {
460
+ existing.bloomAt = performance.now();
461
+ anims.push({ node: existing, kind: "bloom", start: existing.bloomAt, dur: 1100 });
462
+ }
463
+ flash(project, 1600);
464
+ }, memoryLink && !REDUCED ? 950 : 0);
465
+ return;
466
+ }
467
+ var key = (agent ? agent.id : "x") + p.type;
468
+ if (lastImpulse[key] && now - lastImpulse[key] < 350) return;
469
+ lastImpulse[key] = now;
470
+ var color = agent ? agent.color : agentColor(p.agent);
471
+ var small = p.type === "usage";
472
+ if (agent) flash(agent, 900);
473
+ if (agent) emit(linkByKey[agent.id + ">" + hub.id], color, small ? 1.6 : 2.6);
474
+ if (!project) return;
475
+ var delay = agent && hub && !REDUCED ? 950 : 0;
476
+ setTimeout(function () {
477
+ flash(hub, 1300);
478
+ emit(hub && linkByKey[hub.id + ">" + project.id], color, small ? 1.6 : 2.6);
479
+ var projectDelay = hub && !REDUCED ? 950 : 0;
480
+ setTimeout(function () {
481
+ flash(project, 1300);
482
+ var mem = links.filter(function (l) { return l.kind === "pm" && (l.source === project || l.source === project.id); });
483
+ for (var i = 0; i < Math.min(2, mem.length); i++) emit(mem[Math.floor(Math.random() * mem.length)], color, small ? 1.1 : 1.6);
484
+ }, projectDelay);
485
+ }, delay);
486
+ }
487
+
488
+ // ---------------------------------------------------------------- light animation loop (flash, bloom, twinkle)
489
+ function startLoop() {
490
+ if (loopOn || !active || document.hidden || !C) return;
491
+ loopOn = true;
492
+ requestAnimationFrame(loop);
493
+ }
494
+
495
+ function loop(now) {
496
+ if (!active || document.hidden) { loopOn = false; return; }
497
+ for (var i = anims.length - 1; i >= 0; i--) {
498
+ var a = anims[i], t = Math.min(1, (now - a.start) / a.dur), n = a.node, parts = n.parts;
499
+ if (!parts) { if (now - a.start > 4000) anims.splice(i, 1); continue; }
500
+ if (a.kind === "bloom") {
501
+ var s = t < 1 ? 1 + 2.2 * Math.pow(t - 1, 3) + 1.2 * Math.pow(t - 1, 2) : 1; // ease-out-back
502
+ parts.group.scale.setScalar(Math.max(0.001, s));
503
+ parts.halo.material.opacity = haloOpacity(n) + (1 - t) * 1.2;
504
+ parts.halo.scale.setScalar(haloBaseSize(n) * (1 + (1 - t) * 2.5));
505
+ } else {
506
+ var f = 1 - t;
507
+ parts.core.material.emissiveIntensity = (n.type === "hub" ? 1.5 : 0.85) + f * 1.6;
508
+ parts.halo.material.opacity = haloOpacity(n) + f * 0.9;
509
+ var base = haloBaseSize(n);
510
+ parts.halo.scale.setScalar(base * (1 + f * 1.4));
511
+ }
512
+ if (t >= 1) {
513
+ if (a.kind === "bloom") { parts.group.scale.setScalar(1); n.bloomAt = 0; }
514
+ anims.splice(i, 1);
515
+ }
516
+ }
517
+ // subtle twinkle and active-agent breathing at ~15 fps (halo opacity only)
518
+ var twinkle = !REDUCED && now - lastTwinkle > 66;
519
+ if (twinkle) {
520
+ lastTwinkle = now;
521
+ var tt = now / 1000;
522
+ for (var k = 0; k < nodes.length; k++) {
523
+ var m = nodes[k];
524
+ if (!m.parts || m.bloomAt || isAnimating(m)) continue;
525
+ if (m.type === "memory") m.parts.halo.material.opacity = haloOpacity(m) * (0.65 + 0.35 * Math.sin(tt * (0.7 + (m.phase % 1)) + m.phase));
526
+ else if (m.type === "agent" && m.active) m.parts.halo.material.opacity = haloOpacity(m) + (bloomActive() ? 0.15 : 0.35) * Math.sin(tt * 3 + m.phase);
527
+ }
528
+ }
529
+ if (anims.length || !REDUCED) requestAnimationFrame(loop);
530
+ else loopOn = false;
531
+ }
532
+
533
+ function isAnimating(n) {
534
+ for (var i = 0; i < anims.length; i++) if (anims[i].node === n) return true;
535
+ return false;
536
+ }
537
+
538
+ // ---------------------------------------------------------------- lifecycle
539
+ // Live updates keep reheating the layout, so the engine may never stop: fit on settle, after the
540
+ // first data and after a resize, but leave the camera alone for 20 s after the user orbits or zooms.
541
+ var USER_HOLD_MS = 20000;
542
+
543
+ function fit(ms) {
544
+ if (nodes.length <= 2 || Date.now() - lastUserMove < USER_HOLD_MS) return;
545
+ // The hub sits at the origin and the camera orbits it: set the distance so the hub, projects and agents
546
+ // fill the view; outlying memory leaves may run to the edges instead of shrinking the web.
547
+ var reach = 0;
548
+ nodes.forEach(function (n) {
549
+ if (n.type === "memory" || n.x === undefined) return;
550
+ reach = Math.max(reach, Math.sqrt(n.x * n.x + n.y * n.y + n.z * n.z) + (n.radius || 0));
551
+ });
552
+ if (!reach) return;
553
+ var first = !fitted; // jump on page load, glide on later refits
554
+ fitted = true;
555
+ var camera = Graph.camera(), rect = container.getBoundingClientRect();
556
+ var half = Math.PI * camera.fov / 360, aspect = rect.width / Math.max(1, rect.height);
557
+ var fitHalf = Math.min(half, Math.atan(Math.tan(half) * aspect));
558
+ var distance = reach / Math.sin(fitHalf) * 1.04;
559
+ var pos = Graph.cameraPosition(), len = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) || 1;
560
+ Graph.cameraPosition({ x: pos.x / len * distance, y: pos.y / len * distance, z: pos.z / len * distance }, { x: 0, y: 0, z: 0 }, first ? 0 : ms);
561
+ }
562
+
563
+ function fitSoon(delay) {
564
+ clearTimeout(fitTimer);
565
+ fitTimer = setTimeout(function () { fit(700); }, delay);
566
+ }
567
+
568
+ ["pointerdown", "wheel"].forEach(function (type) {
569
+ container.addEventListener(type, function () { lastUserMove = Date.now(); }, { passive: true });
570
+ });
571
+
572
+ function resize() {
573
+ var r = container.getBoundingClientRect();
574
+ if (r.width > 10 && r.height > 10) {
575
+ Graph.width(r.width).height(r.height);
576
+ resizeBloom(r.width, r.height);
577
+ if (fitted) fitSoon(300);
578
+ }
579
+ }
580
+
581
+ function setActive(on) {
582
+ active = on;
583
+ if (on && !document.hidden) { Graph.resumeAnimation(); resize(); startLoop(); }
584
+ else Graph.pauseAnimation();
585
+ }
586
+
587
+ document.addEventListener("visibilitychange", function () {
588
+ if (!active) return;
589
+ if (document.hidden) Graph.pauseAnimation();
590
+ else { Graph.resumeAnimation(); startLoop(); }
591
+ });
592
+ if (window.ResizeObserver) new ResizeObserver(resize).observe(container);
593
+ resize();
594
+ bootstrap();
595
+
596
+ return { setGraph: setGraph, pulse: pulse, resize: resize, setActive: setActive, graph: Graph };
597
+ }
598
+
599
+ window.DevmemBrain3D = { available: function () { return !!window.ForceGraph3D && webglAvailable(); }, create: create };
600
+ })();
@@ -0,0 +1,18 @@
1
+ // Opt-in live room. Deliberately uses DOM nodes and textContent for all message text.
2
+ (function () {
3
+ "use strict";
4
+ var dataEl = document.getElementById("live-data");
5
+ if (!dataEl) return;
6
+ var data = JSON.parse(dataEl.textContent), items = document.getElementById("live-items"), agents = document.getElementById("live-agents"), form = document.getElementById("live-form"), input = document.getElementById("live-body"), note = document.getElementById("live-note"), error = document.getElementById("live-error"), scroll = document.getElementById("live-scroll"), first = true, messageSig, agentSig;
7
+ function el(name, cls, value) { var node = document.createElement(name); if (cls) node.className = cls; if (value !== undefined) node.textContent = value; return node; }
8
+ function localTime(value) { var date = new Date(value); return isNaN(date) ? value : date.toLocaleTimeString([], {hour:"2-digit", minute:"2-digit", hour12:false}); }
9
+ function dot(color) { var node = el("i", "agent-dot"); if (color) node.style.setProperty("--agent", color.hex); return node; }
10
+ function renderMessages() { var sig = JSON.stringify(data.messages); if (!first && sig === messageSig) return; messageSig = sig; var stick = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 80; items.replaceChildren(); if (!data.messages.length) items.append(el("p", "muted", "No messages in the last 2 hours.")); data.messages.forEach(function (m) { var box = el("article", "bubble " + (m.sender_kind === "user" ? "mine" : "theirs") + (m.color ? " agent-bubble" : "")), foot = el("div", "b-foot", localTime(m.created_at)), who = el("div", "b-who"); if (m.color) { box.style.setProperty("--agent", m.color.hex); who.append(dot(m.color)); } who.append(document.createTextNode(m.display_name)); box.append(who); if (m.kind === "event") box.append(el("div", "b-subject", m.headline)); box.append(el("div", "b-text", m.body)); if (m.kind === "question" && m.status === "open") foot.append(document.createTextNode(" · Question open")); foot.append(document.createTextNode(" · ")); var link = el("a", "", "Open"); link.href = "/messages/thread/" + encodeURIComponent(m.thread_id); foot.append(link); box.append(foot); if (m.sender_kind === "user" && m.recipient === "#live") { var delivery = el("div", "delivery"); if (m.delivery.length) m.delivery.forEach(function (s) { var line = el("span", "", s.state === "received" ? "Received by " : "Waiting for "); if (s.color) line.append(dot(s.color)); line.append(document.createTextNode(s.name)); delivery.append(line); }); else delivery.append(document.createTextNode("Not delivered")); box.append(delivery); } items.append(box); }); if (first || stick) scroll.scrollTop = scroll.scrollHeight; first = false; }
11
+ function renderAgents() { var sig = JSON.stringify([data.agents, data.connected]); if (sig === agentSig) return; agentSig = sig; agents.replaceChildren(); data.agents.forEach(function (a) { var row = el("div", "live-agent"), info = el("div", "live-agent-info"), name = el("span", "", a.name), status = el("small", "", a.status); row.title = a.key; if (a.color) row.append(dot(a.color)); info.append(name); if (a.status === "idle") status.append(document.createTextNode(" · will see messages at its next action")); info.append(status); row.append(info); if (a.connectable) { var button = el("button", "", a.connected ? "Disconnect" : "Connect"); button.addEventListener("click", function () { change(a); }); row.append(button); } agents.append(row); }); note.textContent = document.querySelector('input[name="project"]') ? "Ping @Codex or @Claude anywhere in your message to call that agent. Ping both for two replies. Names are case-insensitive." : "Automatic replies are disabled. Connected agents receive messages at their next action."; }
12
+ function refresh(read) { fetch("/api/live" + (read ? "?read=1" : "")).then(function (r) { return r.ok ? r.json() : Promise.reject(Error("live chat refresh failed")); }).then(function (d) { data = d; renderMessages(); renderAgents(); error.hidden = true; }).catch(function (e) { error.textContent = e.message; error.hidden = false; }); }
13
+ function change(a) { var fd = new FormData(); fd.append("agent_key", a.key); fetch(a.connected ? "/messages/live/disconnect" : "/messages/live/connect", {method:"POST", body:fd}).then(function (r) { if (!r.ok) throw Error("connection update failed"); refresh(!document.hidden); }).catch(function (e) { error.textContent = e.message; error.hidden = false; }); }
14
+ form.addEventListener("submit", function (e) { e.preventDefault(); if (!input.value.trim()) return; var button = form.querySelector("button"); button.disabled = true; fetch(form.action, {method:"POST", body:new FormData(form)}).then(function (r) { if (!r.ok) return r.json().then(function (x) { throw Error(x.detail || "send failed"); }); input.value = ""; refresh(!document.hidden); }).catch(function (e) { error.textContent = e.message; error.hidden = false; }).then(function () { button.disabled = false; input.focus(); }); });
15
+ input.addEventListener("keydown", function (e) { if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); } });
16
+ var stream = new EventSource("/api/stream?only=messages"), timer = setInterval(function () { refresh(!document.hidden); }, 10000);
17
+ stream.addEventListener("messages", function () { refresh(!document.hidden); }); window.addEventListener("visibilitychange", function () { if (!document.hidden) refresh(true); }); window.addEventListener("pagehide", function () { stream.close(); clearInterval(timer); }); renderMessages(); renderAgents(); if (!document.hidden) refresh(true);
18
+ }());