Compare commits
7 Commits
mimo/code/
...
feat/mnemo
| Author | SHA1 | Date | |
|---|---|---|---|
| 93f3722a36 | |||
| 3e3e451820 | |||
| 14b75792fe | |||
| 66c0433fc3 | |||
| 332789199f | |||
| 2e0853510d | |||
| 31fdd01931 |
116
app.js
116
app.js
@@ -44,6 +44,7 @@ let particles, dustParticles;
|
|||||||
let debugOverlay;
|
let debugOverlay;
|
||||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||||
let chatOpen = true;
|
let chatOpen = true;
|
||||||
|
let memoryFeedEntries = []; // Mnemosyne: recent memory events for feed panel
|
||||||
let loadProgress = 0;
|
let loadProgress = 0;
|
||||||
let performanceTier = 'high';
|
let performanceTier = 'high';
|
||||||
|
|
||||||
@@ -2041,6 +2042,14 @@ function connectHermes() {
|
|||||||
addChatMessage('system', 'Hermes link established.');
|
addChatMessage('system', 'Hermes link established.');
|
||||||
updateWsHudStatus(true);
|
updateWsHudStatus(true);
|
||||||
refreshWorkshopPanel();
|
refreshWorkshopPanel();
|
||||||
|
|
||||||
|
// Mnemosyne: request memory sync from Hermes
|
||||||
|
try {
|
||||||
|
hermesWs.send(JSON.stringify({ type: 'memory', action: 'sync_request' }));
|
||||||
|
console.info('[Mnemosyne] Sent sync_request to Hermes');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Mnemosyne] Failed to send sync_request:', e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize MemPalace
|
// Initialize MemPalace
|
||||||
@@ -2091,6 +2100,8 @@ function handleHermesMessage(data) {
|
|||||||
recentToolOutputs.push({ type: 'result', agent: data.agent || 'SYSTEM', content });
|
recentToolOutputs.push({ type: 'result', agent: data.agent || 'SYSTEM', content });
|
||||||
addToolMessage(data.agent || 'SYSTEM', 'result', content);
|
addToolMessage(data.agent || 'SYSTEM', 'result', content);
|
||||||
refreshWorkshopPanel();
|
refreshWorkshopPanel();
|
||||||
|
} else if (data.type === 'memory') {
|
||||||
|
handleMemoryMessage(data);
|
||||||
} else if (data.type === 'history') {
|
} else if (data.type === 'history') {
|
||||||
const container = document.getElementById('chat-messages');
|
const container = document.getElementById('chat-messages');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
@@ -2102,6 +2113,111 @@ function handleHermesMessage(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// MNEMOSYNE — LIVE MEMORY BRIDGE
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming memory messages from Hermes WS.
|
||||||
|
* Actions: place, remove, update, sync_response
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all entries from the memory feed.
|
||||||
|
*/
|
||||||
|
function clearMemoryFeed() {
|
||||||
|
memoryFeedEntries = [];
|
||||||
|
renderMemoryFeed();
|
||||||
|
console.info('[Mnemosyne] Memory feed cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMemoryMessage(data) {
|
||||||
|
const action = data.action;
|
||||||
|
const memory = data.memory;
|
||||||
|
const memories = data.memories;
|
||||||
|
|
||||||
|
if (action === 'place' && memory) {
|
||||||
|
const placed = SpatialMemory.placeMemory(memory);
|
||||||
|
if (placed) {
|
||||||
|
addMemoryFeedEntry('place', memory);
|
||||||
|
console.info('[Mnemosyne] Memory placed via WS:', memory.id);
|
||||||
|
}
|
||||||
|
} else if (action === 'remove' && memory) {
|
||||||
|
SpatialMemory.removeMemory(memory.id);
|
||||||
|
addMemoryFeedEntry('remove', memory);
|
||||||
|
console.info('[Mnemosyne] Memory removed via WS:', memory.id);
|
||||||
|
} else if (action === 'update' && memory) {
|
||||||
|
SpatialMemory.updateMemory(memory.id, memory);
|
||||||
|
addMemoryFeedEntry('update', memory);
|
||||||
|
console.info('[Mnemosyne] Memory updated via WS:', memory.id);
|
||||||
|
} else if (action === 'sync_response' && Array.isArray(memories)) {
|
||||||
|
const count = SpatialMemory.importMemories(memories);
|
||||||
|
addMemoryFeedEntry('sync', { content: count + ' memories synced', id: 'sync' });
|
||||||
|
console.info('[Mnemosyne] Synced', count, 'memories from Hermes');
|
||||||
|
} else {
|
||||||
|
console.warn('[Mnemosyne] Unknown memory action:', action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an entry to the memory activity feed panel.
|
||||||
|
*/
|
||||||
|
function addMemoryFeedEntry(action, memory) {
|
||||||
|
const entry = {
|
||||||
|
action,
|
||||||
|
content: memory.content || memory.id || '(unknown)',
|
||||||
|
category: memory.category || 'working',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
memoryFeedEntries.unshift(entry);
|
||||||
|
if (memoryFeedEntries.length > 5) memoryFeedEntries.pop();
|
||||||
|
|
||||||
|
renderMemoryFeed();
|
||||||
|
|
||||||
|
// Auto-dismiss entries older than 5 minutes
|
||||||
|
setTimeout(() => {
|
||||||
|
const idx = memoryFeedEntries.indexOf(entry);
|
||||||
|
if (idx > -1) {
|
||||||
|
memoryFeedEntries.splice(idx, 1);
|
||||||
|
renderMemoryFeed();
|
||||||
|
}
|
||||||
|
}, 300000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the memory feed panel.
|
||||||
|
*/
|
||||||
|
function renderMemoryFeed() {
|
||||||
|
const container = document.getElementById('memory-feed-list');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
memoryFeedEntries.forEach(entry => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'memory-feed-entry memory-feed-' + entry.action;
|
||||||
|
|
||||||
|
const regionDef = SpatialMemory.REGIONS[entry.category] || SpatialMemory.REGIONS.working;
|
||||||
|
const dotColor = '#' + regionDef.color.toString(16).padStart(6, '0');
|
||||||
|
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||||||
|
const truncated = entry.content.length > 40 ? entry.content.slice(0, 40) + '\u2026' : entry.content;
|
||||||
|
const actionIcon = { place: '\u2795', remove: '\u2796', update: '\u270F', sync: '\u21C4' }[entry.action] || '\u2022';
|
||||||
|
|
||||||
|
el.innerHTML = '<span class="memory-feed-dot" style="background:' + dotColor + '"></span>' +
|
||||||
|
'<span class="memory-feed-action">' + actionIcon + '</span>' +
|
||||||
|
'<span class="memory-feed-content">' + truncated + '</span>' +
|
||||||
|
'<span class="memory-feed-time">' + time + '</span>';
|
||||||
|
|
||||||
|
container.appendChild(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show feed if there are entries
|
||||||
|
const panel = document.getElementById('memory-feed');
|
||||||
|
if (panel) panel.style.display = memoryFeedEntries.length > 0 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateWsHudStatus(connected) {
|
function updateWsHudStatus(connected) {
|
||||||
// Update MemPalace status alongside regular WS status
|
// Update MemPalace status alongside regular WS status
|
||||||
updateMemPalaceStatus();
|
updateMemPalaceStatus();
|
||||||
|
|||||||
10
index.html
10
index.html
@@ -439,5 +439,15 @@ index.html
|
|||||||
setInterval(poll, INTERVAL);
|
setInterval(poll, INTERVAL);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Memory Activity Feed (Mnemosyne) -->
|
||||||
|
<div id="memory-feed" class="memory-feed" style="display:none;">
|
||||||
|
<div class="memory-feed-header">
|
||||||
|
<span class="memory-feed-title">✨ Memory Feed</span>
|
||||||
|
<div class="memory-feed-actions"><button class="memory-feed-clear" onclick="clearMemoryFeed()">Clear</button><button class="memory-feed-toggle" onclick="document.getElementById('memory-feed').style.display='none'">✕</button></div>
|
||||||
|
</div>
|
||||||
|
<div id="memory-feed-list" class="memory-feed-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -471,8 +471,83 @@ const SpatialMemory = (() => {
|
|||||||
return results.slice(0, maxResults);
|
return results.slice(0, maxResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── BULK IMPORT (WebSocket sync) ───────────────────
|
||||||
|
/**
|
||||||
|
* Import an array of memories in batch — for WebSocket sync.
|
||||||
|
* Skips duplicates (same id). Returns count of newly placed.
|
||||||
|
* @param {Array} memories - Array of memory objects { id, content, category, ... }
|
||||||
|
* @returns {number} Count of newly placed memories
|
||||||
|
*/
|
||||||
|
function importMemories(memories) {
|
||||||
|
if (!Array.isArray(memories) || memories.length === 0) return 0;
|
||||||
|
let count = 0;
|
||||||
|
memories.forEach(mem => {
|
||||||
|
if (mem.id && !_memoryObjects[mem.id]) {
|
||||||
|
placeMemory(mem);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (count > 0) {
|
||||||
|
_dirty = true;
|
||||||
|
saveToStorage();
|
||||||
|
console.info('[Mnemosyne] Bulk imported', count, 'new memories (total:', Object.keys(_memoryObjects).length, ')');
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── UPDATE MEMORY ──────────────────────────────────
|
||||||
|
/**
|
||||||
|
* Update an existing memory's visual properties (strength, connections).
|
||||||
|
* Does not move the crystal — only updates metadata and re-renders.
|
||||||
|
* @param {string} memId - Memory ID to update
|
||||||
|
* @param {object} updates - Fields to update: { strength, connections, content }
|
||||||
|
* @returns {boolean} True if updated
|
||||||
|
*/
|
||||||
|
function updateMemory(memId, updates) {
|
||||||
|
const obj = _memoryObjects[memId];
|
||||||
|
if (!obj) return false;
|
||||||
|
|
||||||
|
if (updates.strength != null) {
|
||||||
|
const strength = Math.max(0.05, Math.min(1, updates.strength));
|
||||||
|
obj.mesh.userData.strength = strength;
|
||||||
|
obj.mesh.material.emissiveIntensity = 1.5 * strength;
|
||||||
|
obj.mesh.material.opacity = 0.5 + strength * 0.4;
|
||||||
|
}
|
||||||
|
if (updates.content != null) {
|
||||||
|
obj.data.content = updates.content;
|
||||||
|
}
|
||||||
|
if (updates.connections != null) {
|
||||||
|
obj.data.connections = updates.connections;
|
||||||
|
// Rebuild connection lines
|
||||||
|
_rebuildConnections(memId);
|
||||||
|
}
|
||||||
|
_dirty = true;
|
||||||
|
saveToStorage();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _rebuildConnections(memId) {
|
||||||
|
// Remove existing lines for this memory
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recreate lines for current connections
|
||||||
|
const obj = _memoryObjects[memId];
|
||||||
|
if (!obj || !obj.data.connections) return;
|
||||||
|
obj.data.connections.forEach(targetId => {
|
||||||
|
const target = _memoryObjects[targetId];
|
||||||
|
if (target) _createConnectionLine(obj, target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init, placeMemory, removeMemory, update,
|
init, placeMemory, removeMemory, update, importMemories, updateMemory,
|
||||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||||
exportIndex, importIndex, searchNearby, REGIONS,
|
exportIndex, importIndex, searchNearby, REGIONS,
|
||||||
saveToStorage, loadFromStorage, clearStorage
|
saveToStorage, loadFromStorage, clearStorage
|
||||||
|
|||||||
121
style.css
121
style.css
@@ -1223,3 +1223,124 @@ canvas#nexus-canvas {
|
|||||||
.l402-msg { color: #fff; }
|
.l402-msg { color: #fff; }
|
||||||
|
|
||||||
.pse-status { color: #4af0c0; font-weight: 600; }
|
.pse-status { color: #4af0c0; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
MNEMOSYNE — Memory Activity Feed
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.memory-feed {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 320px;
|
||||||
|
background: rgba(10, 15, 40, 0.92);
|
||||||
|
border: 1px solid rgba(74, 240, 192, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
z-index: 900;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
animation: memoryFeedIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes memoryFeedIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid rgba(74, 240, 192, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-title {
|
||||||
|
color: #4af0c0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.memory-feed-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-clear {
|
||||||
|
background: rgba(74, 240, 192, 0.1);
|
||||||
|
border: 1px solid rgba(74, 240, 192, 0.3);
|
||||||
|
color: #4af0c0;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.memory-feed-clear:hover {
|
||||||
|
background: rgba(74, 240, 192, 0.2);
|
||||||
|
border-color: #4af0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.memory-feed-toggle:hover { color: #fff; }
|
||||||
|
|
||||||
|
.memory-feed-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.memory-feed-entry:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-feed-place { border-left: 2px solid #4af0c0; }
|
||||||
|
.memory-feed-remove { border-left: 2px solid #ff4466; }
|
||||||
|
.memory-feed-update { border-left: 2px solid #ffd700; }
|
||||||
|
.memory-feed-sync { border-left: 2px solid #7b5cff; }
|
||||||
|
|||||||
Reference in New Issue
Block a user