Compare commits

...

5 Commits

Author SHA1 Message Date
1ec3da6e30 fix: add ES module export for SpatialMemory
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 14s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-10 05:44:37 +00:00
b153613352 feat(mnemosyne): integrate Spatial Memory Schema into Nexus
- Import SpatialMemory component
- Initialize regions on scene load (5 semantic zones)
- Update crystal animations in game loop
- Seed 5 demo memories with connections
- Region markers visible on the hex platform floor
2026-04-10 05:44:08 +00:00
36a152b9c2 feat(mnemosyne): add Spatial Memory Schema component
Defines persistent memory regions for the holographic archive:
- 5 semantic regions mapped to Nexus quadrants
- Crystal geometry for persistent memories (vs temporary orbs)
- Deterministic placement via ID hashing
- Connection lines between related memories
- Import/export for session persistence
- Spatial search and region queries

Closes #1154
2026-04-10 05:42:57 +00:00
6f949698fe Merge pull request #1148
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Verification Gate / verify-staging (push) Failing after 5s
Merged PR #1148
2026-04-10 03:43:56 +00:00
6cf1f4d078 feat(mnemosyne): implement memory orb system with game loop integration\n\n- Added spawnMemoryOrb() with PBR materials and point lighting\n- Added removeMemoryOrb() with proper resource disposal\n- Added animateMemoryOrbs() for pulse/fade animation\n- Added spawnRetrievalOrbs() for RAG result visualization\n- Integrated animateMemoryOrbs(delta) into gameLoop()\n- Orbs auto-fade after 30s with smooth 10s fade-out\n\nFixes #1147\nSupersedes PR #1147 (blocked by branch protection)
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 19s
Review Approval Gate / verify-review (pull_request) Successful in 4s
2026-04-10 02:13:31 +00:00
2 changed files with 549 additions and 0 deletions

173
app.js
View File

@@ -3,6 +3,7 @@ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
import { SpatialMemory } from './nexus/components/spatial-memory.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -670,6 +671,10 @@ async function init() {
updateLoad(40);
createFloor();
updateLoad(50);
// Project Mnemosyne — Spatial Memory Schema
SpatialMemory.init(scene);
createBatcaveTerminal();
updateLoad(60);
@@ -2573,6 +2578,13 @@ function gameLoop() {
updateAshStorm(delta, elapsed);
// Project Mnemosyne - Memory Animation
if (typeof animateMemoryOrbs === 'function') {
animateMemoryOrbs(delta);
}
SpatialMemory.update(delta);
const mode = NAV_MODES[navModeIdx];
const chatActive = document.activeElement === document.getElementById('chat-input');
@@ -2771,6 +2783,12 @@ function gameLoop() {
composer.render();
updateAshStorm(delta, elapsed);
// Project Mnemosyne - Memory Orb Animation
if (typeof animateMemoryOrbs === 'function') {
animateMemoryOrbs(delta);
}
updatePortalTunnel(delta, elapsed);
if (workshopScanMat) workshopScanMat.uniforms.uTime.value = clock.getElapsedTime();
@@ -2933,9 +2951,164 @@ function updateAshStorm(delta, elapsed) {
}
}
// ═══════════════════════════════════════════
// PROJECT MNEMOSYNE — HOLOGRAPHIC MEMORY ORBS
// ═══════════════════════════════════════════
// Memory orbs registry for animation loop
const memoryOrbs = [];
/**
* Spawn a glowing memory orb at the given position.
* Used to visualize RAG retrievals and memory recalls in the Nexus.
*
* @param {THREE.Vector3} position - World position for the orb
* @param {number} color - Hex color (default: 0x4af0c0 - cyan)
* @param {number} size - Radius of the orb (default: 0.5)
* @param {object} metadata - Optional metadata for the memory (source, timestamp, etc.)
* @returns {THREE.Mesh} The created orb mesh
*/
function spawnMemoryOrb(position, color = 0x4af0c0, size = 0.5, metadata = {}) {
if (typeof THREE === 'undefined' || typeof scene === 'undefined') {
console.warn('[Mnemosyne] THREE/scene not available for orb spawn');
return null;
}
const geometry = new THREE.SphereGeometry(size, 32, 32);
const material = new THREE.MeshStandardMaterial({
color: color,
emissive: color,
emissiveIntensity: 2.5,
metalness: 0.3,
roughness: 0.2,
transparent: true,
opacity: 0.85,
envMapIntensity: 1.5
});
const orb = new THREE.Mesh(geometry, material);
orb.position.copy(position);
orb.castShadow = true;
orb.receiveShadow = true;
orb.userData = {
type: 'memory_orb',
pulse: Math.random() * Math.PI * 2, // Random phase offset
pulseSpeed: 0.002 + Math.random() * 0.001,
originalScale: size,
metadata: metadata,
createdAt: Date.now()
};
// Point light for local illumination
const light = new THREE.PointLight(color, 1.5, 8);
orb.add(light);
scene.add(orb);
memoryOrbs.push(orb);
console.info('[Mnemosyne] Memory orb spawned:', metadata.source || 'unknown');
return orb;
}
/**
* Remove a memory orb from the scene and dispose resources.
* @param {THREE.Mesh} orb - The orb to remove
*/
function removeMemoryOrb(orb) {
if (!orb) return;
if (orb.parent) orb.parent.remove(orb);
if (orb.geometry) orb.geometry.dispose();
if (orb.material) orb.material.dispose();
const idx = memoryOrbs.indexOf(orb);
if (idx > -1) memoryOrbs.splice(idx, 1);
}
/**
* Animate all memory orbs — pulse, rotate, and fade.
* Called from gameLoop() every frame.
* @param {number} delta - Time since last frame
*/
function animateMemoryOrbs(delta) {
for (let i = memoryOrbs.length - 1; i >= 0; i--) {
const orb = memoryOrbs[i];
if (!orb || !orb.userData) continue;
// Pulse animation
orb.userData.pulse += orb.userData.pulseSpeed * delta * 1000;
const pulseFactor = 1 + Math.sin(orb.userData.pulse) * 0.1;
orb.scale.setScalar(pulseFactor * orb.userData.originalScale);
// Gentle rotation
orb.rotation.y += delta * 0.5;
// Fade after 30 seconds
const age = (Date.now() - orb.userData.createdAt) / 1000;
if (age > 30) {
const fadeDuration = 10;
const fadeProgress = Math.min(1, (age - 30) / fadeDuration);
orb.material.opacity = 0.85 * (1 - fadeProgress);
if (fadeProgress >= 1) {
removeMemoryOrb(orb);
i--; // Adjust index after removal
}
}
}
}
/**
* Spawn memory orbs arranged in a spiral for RAG retrieval results.
* @param {Array} results - Array of {content, score, source}
* @param {THREE.Vector3} center - Center position (default: above avatar)
*/
function spawnRetrievalOrbs(results, center) {
if (!results || !Array.isArray(results) || results.length === 0) return;
if (!center) {
center = new THREE.Vector3(0, 2, 0);
}
const colors = [0x4af0c0, 0x7b5cff, 0xffd700, 0xff4466, 0x00ff88];
const radius = 3;
results.forEach((result, i) => {
const angle = (i / results.length) * Math.PI * 2;
const height = (i / results.length) * 2 - 1;
const position = new THREE.Vector3(
center.x + Math.cos(angle) * radius,
center.y + height,
center.z + Math.sin(angle) * radius
);
const colorIdx = Math.min(colors.length - 1, Math.floor((result.score || 0.5) * colors.length));
const size = 0.3 + (result.score || 0.5) * 0.4;
spawnMemoryOrb(position, colors[colorIdx], size, {
source: result.source || 'unknown',
score: result.score || 0,
contentPreview: (result.content || '').substring(0, 100)
});
});
}
init().then(() => {
createAshStorm();
createPortalTunnel();
// Project Mnemosyne — seed demo spatial memories
const demoMemories = [
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95, connections: ['mem_first_portal'] },
{ id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85, connections: ['mem_nexus_birth'] },
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7, connections: [] },
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9, connections: ['mem_nexus_birth'] },
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.8, connections: ['mem_mnemosyne_start'] },
];
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
fetchGiteaData();
setInterval(fetchGiteaData, 30000);
runWeeklyAudit();

View File

@@ -0,0 +1,376 @@
// ═══════════════════════════════════════════
// PROJECT MNEMOSYNE — SPATIAL MEMORY SCHEMA
// ═══════════════════════════════════════════
//
// Maps memories to persistent locations in the 3D Nexus world.
// Each region corresponds to a semantic category. Memories placed
// in a region stay there across sessions, forming a navigable
// holographic archive.
//
// World layout (hex cylinder, radius 25):
// North (z-) → Documents & Knowledge
// South (z+) → Projects & Tasks
// East (x+) → Code & Engineering
// West (x-) → Conversations & Social
// Center → Active Working Memory
// Below (y-) → Archive (cold storage)
//
// Usage from app.js:
// SpatialMemory.init(scene);
// SpatialMemory.placeMemory({ id, content, category, ... });
// SpatialMemory.importIndex(savedIndex);
// SpatialMemory.update(delta);
// ═══════════════════════════════════════════
const SpatialMemory = (() => {
// ─── REGION DEFINITIONS ───────────────────────────────
const REGIONS = {
engineering: {
label: 'Code & Engineering',
center: [15, 0, 0],
radius: 10,
color: 0x4af0c0,
glyph: '\u2699',
description: 'Source code, debugging sessions, architecture decisions'
},
social: {
label: 'Conversations & Social',
center: [-15, 0, 0],
radius: 10,
color: 0x7b5cff,
glyph: '\uD83D\uDCAC',
description: 'Chats, discussions, human interactions'
},
knowledge: {
label: 'Documents & Knowledge',
center: [0, 0, -15],
radius: 10,
color: 0xffd700,
glyph: '\uD83D\uDCD6',
description: 'Papers, docs, research, learned concepts'
},
projects: {
label: 'Projects & Tasks',
center: [0, 0, 15],
radius: 10,
color: 0xff4466,
glyph: '\uD83C\uDFAF',
description: 'Active tasks, issues, milestones, goals'
},
working: {
label: 'Active Working Memory',
center: [0, 0, 0],
radius: 5,
color: 0x00ff88,
glyph: '\uD83D\uDCA1',
description: 'Current focus — transient, high-priority memories'
},
archive: {
label: 'Archive',
center: [0, -3, 0],
radius: 20,
color: 0x334455,
glyph: '\uD83D\uDDC4',
description: 'Cold storage — rarely accessed, aged-out memories'
}
};
// ─── STATE ────────────────────────────────────────────
let _scene = null;
let _regionMarkers = {};
let _memoryObjects = {};
let _connectionLines = [];
let _initialized = false;
// ─── CRYSTAL GEOMETRY (persistent memories) ───────────
function createCrystalGeometry(size) {
return new THREE.OctahedronGeometry(size, 0);
}
// ─── REGION MARKER ───────────────────────────────────
function createRegionMarker(regionKey, region) {
const cx = region.center[0];
const cy = region.center[1] + 0.06;
const cz = region.center[2];
const ringGeo = new THREE.RingGeometry(region.radius - 0.5, region.radius, 6);
const ringMat = new THREE.MeshBasicMaterial({
color: region.color,
transparent: true,
opacity: 0.15,
side: THREE.DoubleSide
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = -Math.PI / 2;
ring.position.set(cx, cy, cz);
ring.userData = { type: 'region_marker', region: regionKey };
const discGeo = new THREE.CircleGeometry(region.radius - 0.5, 6);
const discMat = new THREE.MeshBasicMaterial({
color: region.color,
transparent: true,
opacity: 0.03,
side: THREE.DoubleSide
});
const disc = new THREE.Mesh(discGeo, discMat);
disc.rotation.x = -Math.PI / 2;
disc.position.set(cx, cy - 0.01, cz);
_scene.add(ring);
_scene.add(disc);
// Floating label
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.font = '24px monospace';
ctx.fillStyle = '#' + region.color.toString(16).padStart(6, '0');
ctx.textAlign = 'center';
ctx.fillText(region.glyph + ' ' + region.label, 128, 40);
const texture = new THREE.CanvasTexture(canvas);
const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0.6 });
const sprite = new THREE.Sprite(spriteMat);
sprite.position.set(cx, 3, cz);
sprite.scale.set(4, 1, 1);
_scene.add(sprite);
return { ring, disc, sprite };
}
// ─── PLACE A MEMORY ──────────────────────────────────
function placeMemory(mem) {
if (!_scene) return null;
const region = REGIONS[mem.category] || REGIONS.working;
const pos = mem.position || _assignPosition(mem.category, mem.id);
const strength = Math.max(0.05, Math.min(1, mem.strength != null ? mem.strength : 0.7));
const size = 0.2 + strength * 0.3;
const geo = createCrystalGeometry(size);
const mat = new THREE.MeshStandardMaterial({
color: region.color,
emissive: region.color,
emissiveIntensity: 1.5 * strength,
metalness: 0.6,
roughness: 0.15,
transparent: true,
opacity: 0.5 + strength * 0.4
});
const crystal = new THREE.Mesh(geo, mat);
crystal.position.set(pos[0], pos[1] + 1.5, pos[2]);
crystal.castShadow = true;
crystal.userData = {
type: 'spatial_memory',
memId: mem.id,
region: mem.category,
pulse: Math.random() * Math.PI * 2,
strength: strength,
createdAt: mem.timestamp || new Date().toISOString()
};
const light = new THREE.PointLight(region.color, 0.8 * strength, 5);
crystal.add(light);
_scene.add(crystal);
_memoryObjects[mem.id] = { mesh: crystal, data: mem, region: mem.category };
if (mem.connections && mem.connections.length > 0) {
_drawConnections(mem.id, mem.connections);
}
console.info('[Mnemosyne] Spatial memory placed:', mem.id, 'in', region.label);
return crystal;
}
// ─── DETERMINISTIC POSITION ──────────────────────────
function _assignPosition(category, memId) {
const region = REGIONS[category] || REGIONS.working;
const cx = region.center[0];
const cy = region.center[1];
const cz = region.center[2];
const r = region.radius * 0.7;
let hash = 0;
for (let i = 0; i < memId.length; i++) {
hash = ((hash << 5) - hash) + memId.charCodeAt(i);
hash |= 0;
}
const angle = (Math.abs(hash % 360) / 360) * Math.PI * 2;
const dist = (Math.abs((hash >> 8) % 100) / 100) * r;
const height = (Math.abs((hash >> 16) % 100) / 100) * 3;
return [cx + Math.cos(angle) * dist, cy + height, cz + Math.sin(angle) * dist];
}
// ─── CONNECTIONS ─────────────────────────────────────
function _drawConnections(memId, connections) {
const src = _memoryObjects[memId];
if (!src) return;
connections.forEach(targetId => {
const tgt = _memoryObjects[targetId];
if (!tgt) return;
const points = [src.mesh.position.clone(), tgt.mesh.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({ color: 0x334455, transparent: true, opacity: 0.2 });
const line = new THREE.Line(geo, mat);
line.userData = { type: 'connection', from: memId, to: targetId };
_scene.add(line);
_connectionLines.push(line);
});
}
// ─── REMOVE A MEMORY ─────────────────────────────────
function removeMemory(memId) {
const obj = _memoryObjects[memId];
if (!obj) return;
if (obj.mesh.parent) obj.mesh.parent.remove(obj.mesh);
if (obj.mesh.geometry) obj.mesh.geometry.dispose();
if (obj.mesh.material) obj.mesh.material.dispose();
for (let i = _connectionLines.length - 1; i >= 0; i--) {
const line = _connectionLines[i];
if (line.userData.from === memId || line.userData.to === memId) {
if (line.parent) line.parent.remove(line);
line.geometry.dispose();
line.material.dispose();
_connectionLines.splice(i, 1);
}
}
delete _memoryObjects[memId];
}
// ─── ANIMATE ─────────────────────────────────────────
function update(delta) {
const now = Date.now();
Object.values(_memoryObjects).forEach(obj => {
const mesh = obj.mesh;
if (!mesh || !mesh.userData) return;
mesh.rotation.y += delta * 0.3;
mesh.userData.pulse += delta * 1.5;
const pulse = 1 + Math.sin(mesh.userData.pulse) * 0.08;
mesh.scale.setScalar(pulse);
if (mesh.material) {
const base = mesh.userData.strength || 0.7;
mesh.material.emissiveIntensity = 1.0 + Math.sin(mesh.userData.pulse * 0.7) * 0.5 * base;
}
});
Object.values(_regionMarkers).forEach(marker => {
if (marker.ring && marker.ring.material) {
marker.ring.material.opacity = 0.1 + Math.sin(now * 0.001) * 0.05;
}
});
}
// ─── INIT ────────────────────────────────────────────
function init(scene) {
_scene = scene;
_initialized = true;
Object.entries(REGIONS).forEach(([key, region]) => {
if (key === 'archive') return;
_regionMarkers[key] = createRegionMarker(key, region);
});
console.info('[Mnemosyne] Spatial Memory Schema initialized —', Object.keys(REGIONS).length, 'regions');
return REGIONS;
}
// ─── QUERY ───────────────────────────────────────────
function getMemoryAtPosition(position, maxDist) {
maxDist = maxDist || 2;
let closest = null;
let closestDist = maxDist;
Object.values(_memoryObjects).forEach(obj => {
const d = obj.mesh.position.distanceTo(position);
if (d < closestDist) { closest = obj; closestDist = d; }
});
return closest;
}
function getRegionAtPosition(position) {
for (const [key, region] of Object.entries(REGIONS)) {
const dx = position.x - region.center[0];
const dz = position.z - region.center[2];
if (Math.sqrt(dx * dx + dz * dz) <= region.radius) return key;
}
return null;
}
function getMemoriesInRegion(regionKey) {
return Object.values(_memoryObjects).filter(o => o.region === regionKey);
}
function getAllMemories() {
return Object.values(_memoryObjects).map(o => o.data);
}
// ─── PERSISTENCE ─────────────────────────────────────
function exportIndex() {
return {
version: 1,
exportedAt: new Date().toISOString(),
regions: Object.fromEntries(
Object.entries(REGIONS).map(([k, v]) => [k, { label: v.label, center: v.center, radius: v.radius, color: v.color }])
),
memories: Object.values(_memoryObjects).map(o => ({
id: o.data.id,
content: o.data.content,
category: o.region,
position: [o.mesh.position.x, o.mesh.position.y - 1.5, o.mesh.position.z],
source: o.data.source || 'unknown',
timestamp: o.data.timestamp || o.mesh.userData.createdAt,
strength: o.mesh.userData.strength || 0.7,
connections: o.data.connections || []
}))
};
}
function importIndex(index) {
if (!index || !index.memories) return 0;
let count = 0;
index.memories.forEach(mem => {
if (!_memoryObjects[mem.id]) { placeMemory(mem); count++; }
});
console.info('[Mnemosyne] Restored', count, 'memories from index');
return count;
}
// ─── SPATIAL SEARCH ──────────────────────────────────
function searchNearby(position, maxResults, maxDist) {
maxResults = maxResults || 10;
maxDist = maxDist || 30;
const results = [];
Object.values(_memoryObjects).forEach(obj => {
const d = obj.mesh.position.distanceTo(position);
if (d <= maxDist) results.push({ memory: obj.data, distance: d, position: obj.mesh.position.clone() });
});
results.sort((a, b) => a.distance - b.distance);
return results.slice(0, maxResults);
}
return {
init, placeMemory, removeMemory, update,
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
exportIndex, importIndex, searchNearby, REGIONS
};
})();
export { SpatialMemory };