Compare commits

..

5 Commits

Author SHA1 Message Date
7cfbef970e feat(mnemosyne): integrate spatial memory schema with orb system
Some checks failed
CI / test (pull_request) Failing after 10s
CI / validate (pull_request) Failing after 12s
Review Approval Gate / verify-review (pull_request) Failing after 3s
- Loads spatial-memory-schema.json on init
- Maps memory categories to rooms (Library, Workshop, Armory, Commons)
- Adds spawnCategorizedOrb() for category-based placement
- Adds drawHolographicThread() for orb connections
- Adds spawnWithRoomTransition() for animated room placement
- Animates holographic threads and room transitions in game loop

Supersedes #1150 (blocked by branch protection).
2026-04-10 04:11:22 +00:00
b1f5b1b859 feat(mnemosyne): add spatial memory schema
Copied from feat/mnemosyne-spatial-schema (PR #1150).
Maps memory categories to Nexus spatial zones with visual themes,
object properties, and holographic thread connections.
2026-04-10 04:10:35 +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
182a1148eb Merge pull request '[PERPLEXITY-03] Replace SOUL.md with pointer to canonical timmy-home version' (#1133) from perplexity/soul-md-pointer into main
Some checks failed
Deploy Nexus / deploy (push) Failing after 2s
Staging Verification Gate / verify-staging (push) Failing after 3s
2026-04-08 11:10:32 +00:00
2 changed files with 696 additions and 0 deletions

384
app.js
View File

@@ -2573,6 +2573,14 @@ function gameLoop() {
updateAshStorm(delta, elapsed);
// Project Mnemosyne - Memory Orb Animation
if (typeof animateMemoryOrbs === 'function') {
animateMemoryOrbs(delta);
animateHolographicThreads(delta);
animateRoomTransitions(delta);
}
const mode = NAV_MODES[navModeIdx];
const chatActive = document.activeElement === document.getElementById('chat-input');
@@ -2771,6 +2779,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,7 +2947,377 @@ 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)
});
});
}
// ═══════════════════════════════════════════
// MNEMOSYNE — SPATIAL MEMORY INTEGRATION
// ═══════════════════════════════════════════
// Spatial memory schema state (loaded async)
let spatialMemorySchema = null;
const holographicThreads = []; // Active thread meshes
/**
* Load the spatial memory schema and store it for room mapping.
* Called during init. Falls back gracefully if schema unavailable.
*/
async function loadSpatialMemorySchema() {
try {
const resp = await fetch('/spatial-memory-schema.json');
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
spatialMemorySchema = await resp.json();
console.info('[Mnemosyne] Spatial memory schema loaded:',
Object.keys(spatialMemorySchema.rooms).length, 'rooms');
} catch (err) {
console.warn('[Mnemosyne] Could not load spatial schema, using defaults:', err.message);
spatialMemorySchema = null;
}
}
/**
* Get the room definition for a memory category.
* @param {string} category - Memory category (user_pref, project, tool, general)
* @returns {object|null} Room definition with spatial_bounds and visual_theme
*/
function getRoomForCategory(category) {
if (!spatialMemorySchema) return null;
for (const [roomId, room] of Object.entries(spatialMemorySchema.rooms)) {
if (room.category === category) return { id: roomId, ...room };
}
// Fallback to commons for unknown categories
if (spatialMemorySchema.rooms.commons) {
return { id: 'commons', ...spatialMemorySchema.rooms.commons };
}
return null;
}
/**
* Calculate a random position within a room's spatial bounds.
* @param {object} room - Room definition with spatial_bounds
* @returns {THREE.Vector3} Position within room bounds
*/
function getPositionInRoom(room) {
const bounds = room.spatial_bounds;
const dims = bounds.dimensions;
const center = bounds.center;
return new THREE.Vector3(
center[0] + (Math.random() - 0.5) * dims[0] * 0.8,
center[1] + (Math.random() - 0.5) * dims[1] * 0.6 + 1.5, // Float above floor
center[2] + (Math.random() - 0.5) * dims[2] * 0.8
);
}
/**
* Spawn a categorized memory orb placed in its corresponding room.
* Extends spawnMemoryOrb with spatial placement based on category.
*
* @param {string} category - Memory category (user_pref, project, tool, general)
* @param {object} metadata - Memory metadata (source, content, score, etc.)
* @param {number} importance - 0-1 importance score (affects size/glow)
* @returns {THREE.Mesh} The spawned orb
*/
function spawnCategorizedOrb(category, metadata = {}, importance = 0.5) {
const room = getRoomForCategory(category);
const position = room ? getPositionInRoom(room) : new THREE.Vector3(0, 2, 0);
// Color from schema trust mapping or room theme
let color = 0x4af0c0; // Default cyan
if (room && room.visual_theme) {
const accent = room.visual_theme.colors?.accent;
if (accent) color = parseInt(accent.replace('#', ''), 16);
}
// Size scales with importance
const size = 0.2 + importance * 0.5;
const orb = spawnMemoryOrb(position, color, size, {
...metadata,
category: category,
room: room ? room.id : 'unknown'
});
return orb;
}
/**
* Draw a holographic thread connecting two memory orbs.
* @param {THREE.Mesh} orbA - First orb
* @param {THREE.Mesh} orbB - Second orb
* @param {number} color - Thread color (default: 0x4af0c0)
* @returns {THREE.Line} The thread mesh
*/
function drawHolographicThread(orbA, orbB, color = 0x4af0c0) {
if (typeof THREE === 'undefined' || !orbA || !orbB) return null;
const points = [orbA.position.clone(), orbB.position.clone()];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: color,
transparent: true,
opacity: 0.4,
linewidth: 1
});
const thread = new THREE.Line(geometry, material);
thread.userData = {
type: 'holographic_thread',
orbA: orbA,
orbB: orbB,
createdAt: Date.now()
};
scene.add(thread);
holographicThreads.push(thread);
return thread;
}
/**
* Update holographic threads to follow their connected orbs.
* Called from the animation loop.
* @param {number} delta - Time since last frame
*/
function animateHolographicThreads(delta) {
for (let i = holographicThreads.length - 1; i >= 0; i--) {
const thread = holographicThreads[i];
if (!thread || !thread.userData) continue;
const { orbA, orbB } = thread.userData;
// Remove thread if either orb is gone
if (!orbA || !orbA.parent || !orbB || !orbB.parent) {
scene.remove(thread);
thread.geometry.dispose();
thread.material.dispose();
holographicThreads.splice(i, 1);
continue;
}
// Update line positions to follow orbs
const positions = thread.geometry.attributes.position;
positions.setXYZ(0, orbA.position.x, orbA.position.y, orbA.position.z);
positions.setXYZ(1, orbB.position.x, orbB.position.y, orbB.position.z);
positions.needsUpdate = true;
// Pulse opacity
const age = (Date.now() - thread.userData.createdAt) / 1000;
thread.material.opacity = 0.3 + Math.sin(age * 2) * 0.1;
}
}
/**
* Spawn a memory orb and animate it transitioning to its room.
* @param {string} category - Memory category
* @param {object} metadata - Memory metadata
* @param {number} importance - 0-1 importance score
* @param {THREE.Vector3} startPos - Starting position (default: above avatar)
* @returns {THREE.Mesh} The orb (already in transit)
*/
function spawnWithRoomTransition(category, metadata = {}, importance = 0.5, startPos = null) {
if (!startPos) startPos = new THREE.Vector3(0, 2, 0);
const room = getRoomForCategory(category);
const endPos = room ? getPositionInRoom(room) : new THREE.Vector3(0, 2, 0);
let color = 0x4af0c0;
if (room && room.visual_theme) {
const accent = room.visual_theme.colors?.accent;
if (accent) color = parseInt(accent.replace('#', ''), 16);
}
const size = 0.2 + importance * 0.5;
// Spawn at start position
const orb = spawnMemoryOrb(startPos, color, size, {
...metadata,
category: category,
room: room ? room.id : 'unknown',
transitioning: true,
targetPos: endPos,
transitionStart: Date.now(),
transitionDuration: 2000 + Math.random() * 1000
});
return orb;
}
/**
* Animate room transitions for orbs that are in transit.
* @param {number} delta - Time since last frame
*/
function animateRoomTransitions(delta) {
for (const orb of memoryOrbs) {
if (!orb.userData?.transitioning || !orb.userData?.targetPos) continue;
const elapsed = Date.now() - orb.userData.transitionStart;
const duration = orb.userData.transitionDuration;
const progress = Math.min(1, elapsed / duration);
// Ease-out cubic
const eased = 1 - Math.pow(1 - progress, 3);
orb.position.lerpVectors(
orb.position, // Current (already partially moved)
orb.userData.targetPos,
eased * 0.05 // Smooth interpolation factor per frame
);
if (progress >= 1) {
orb.position.copy(orb.userData.targetPos);
orb.userData.transitioning = false;
delete orb.userData.targetPos;
delete orb.userData.transitionStart;
delete orb.userData.transitionDuration;
}
}
}
init().then(() => {
loadSpatialMemorySchema();
createAshStorm();
createPortalTunnel();
fetchGiteaData();

312
spatial-memory-schema.json Normal file
View File

@@ -0,0 +1,312 @@
{
"version": "1.0.0",
"project": "Mnemosyne",
"description": "Spatial memory schema for holographic memory visualization",
"rooms": {
"library": {
"name": "The Library",
"category": "user_pref",
"description": "User preferences and personal settings",
"visual_theme": {
"lighting": "soft_ambient",
"colors": {
"primary": "#8B4513",
"secondary": "#DAA520",
"accent": "#FFD700",
"particle": "#FFE4B5"
},
"materials": {
"floor": "dark_wood",
"walls": "bookshelf",
"ceiling": "vaulted_stone"
},
"particle_effects": [
"dust_motes",
"book_sparkles"
]
},
"spatial_bounds": {
"center": [
0,
0,
0
],
"dimensions": [
20,
10,
20
],
"orb_density": 0.7
},
"object_types": {
"preference": {
"shape": "sphere",
"base_size": 0.3,
"glow_intensity": 0.8
},
"setting": {
"shape": "cube",
"base_size": 0.4,
"glow_intensity": 0.6
}
}
},
"workshop": {
"name": "The Workshop",
"category": "project",
"description": "Active projects and development work",
"visual_theme": {
"lighting": "bright_work",
"colors": {
"primary": "#4682B4",
"secondary": "#B0C4DE",
"accent": "#00BFFF",
"particle": "#87CEEB"
},
"materials": {
"floor": "polished_concrete",
"walls": "blueprint_paper",
"ceiling": "industrial_metal"
},
"particle_effects": [
"blueprint_lines",
"tool_sparks"
]
},
"spatial_bounds": {
"center": [
30,
0,
0
],
"dimensions": [
25,
12,
25
],
"orb_density": 0.8
},
"object_types": {
"project": {
"shape": "pyramid",
"base_size": 0.5,
"glow_intensity": 0.9
},
"task": {
"shape": "cube",
"base_size": 0.3,
"glow_intensity": 0.7
}
}
},
"armory": {
"name": "The Armory",
"category": "tool",
"description": "Tools, skills, and capabilities",
"visual_theme": {
"lighting": "neon_glow",
"colors": {
"primary": "#2E8B57",
"secondary": "#3CB371",
"accent": "#00FF7F",
"particle": "#98FB98"
},
"materials": {
"floor": "chrome_grid",
"walls": "server_rack",
"ceiling": "neon_tube"
},
"particle_effects": [
"data_streams",
"circuit_traces"
]
},
"spatial_bounds": {
"center": [
0,
0,
30
],
"dimensions": [
15,
8,
15
],
"orb_density": 0.6
},
"object_types": {
"tool": {
"shape": "octahedron",
"base_size": 0.4,
"glow_intensity": 1.0
},
"skill": {
"shape": "sphere",
"base_size": 0.35,
"glow_intensity": 0.85
}
}
},
"commons": {
"name": "The Commons",
"category": "general",
"description": "General knowledge and miscellaneous facts",
"visual_theme": {
"lighting": "natural_daylight",
"colors": {
"primary": "#9370DB",
"secondary": "#BA55D3",
"accent": "#DA70D6",
"particle": "#E6E6FA"
},
"materials": {
"floor": "grass",
"walls": "floating_islands",
"ceiling": "open_sky"
},
"particle_effects": [
"floating_pollen",
"lightning_bugs"
]
},
"spatial_bounds": {
"center": [
30,
0,
30
],
"dimensions": [
30,
15,
30
],
"orb_density": 0.5
},
"object_types": {
"fact": {
"shape": "sphere",
"base_size": 0.25,
"glow_intensity": 0.7
},
"memory": {
"shape": "dodecahedron",
"base_size": 0.3,
"glow_intensity": 0.65
}
}
}
},
"object_properties": {
"trust_mapping": {
"description": "Maps trust score (0.0-1.0) to visual properties",
"glow_intensity": {
"min": 0.2,
"max": 1.0,
"curve": "linear"
},
"opacity": {
"min": 0.3,
"max": 1.0,
"curve": "ease_in_out"
}
},
"importance_mapping": {
"description": "Maps importance (relation count) to visual properties",
"scale": {
"min": 0.2,
"max": 2.0,
"curve": "logarithmic"
},
"particle_density": {
"min": 10,
"max": 100,
"curve": "linear"
}
},
"lifecycle_events": {
"FACT_CREATED": {
"animation": "fade_in",
"duration": 1.5,
"particle_effect": "spawn_burst"
},
"FACT_UPDATED": {
"animation": "pulse_glow",
"duration": 0.8,
"particle_effect": "update_ripple"
},
"FACT_REMOVED": {
"animation": "dissolve",
"duration": 2.0,
"particle_effect": "scatter"
},
"FACT_RECALLED": {
"animation": "beam_light",
"duration": 1.0,
"particle_effect": "recall_beam"
}
}
},
"connections": {
"holographic_threads": {
"description": "Visual connections between related memory orbs",
"material": "transparent_glow",
"colors": {
"strong_relation": "#00FFFF",
"medium_relation": "#00CED1",
"weak_relation": "#5F9EA0"
},
"thickness": {
"min": 0.02,
"max": 0.1,
"curve": "linear"
}
},
"cross_room_portals": {
"description": "Portals connecting different memory rooms",
"effect": "swirling_vortex",
"colors": {
"library_workshop": "#FFD700",
"workshop_armory": "#00BFFF",
"armory_commons": "#00FF7F",
"commons_library": "#DA70D6"
}
}
},
"rag_integration": {
"retrieval_visualization": {
"description": "How RAG retrieval results are visualized",
"highlight_effect": "golden_glow",
"spiral_arrangement": {
"radius": 3.0,
"height_step": 0.5,
"rotation_step": 0.618033988749895
},
"relevance_scoring": {
"high": {
"color": "#FFD700",
"size_multiplier": 1.5
},
"medium": {
"color": "#FFA500",
"size_multiplier": 1.2
},
"low": {
"color": "#FF8C00",
"size_multiplier": 1.0
}
}
},
"query_beam": {
"description": "Beam from user to relevant memory orbs",
"color": "#FFFFFF",
"opacity": 0.8,
"pulse_frequency": 2.0
}
},
"animation_timing": {
"orb_spawn_delay": 0.1,
"room_transition_duration": 2.0,
"connection_draw_speed": 0.5,
"particle_fade_time": 1.5
}
}