Compare commits

..

7 Commits

Author SHA1 Message Date
93f3722a36 feat(mnemosyne): add styles for clear button 2026-04-10 23:40:34 +00:00
3e3e451820 feat(mnemosyne): implement clearMemoryFeed logic 2026-04-10 23:40:32 +00:00
14b75792fe feat(mnemosyne): add clear button to memory feed UI 2026-04-10 23:40:29 +00:00
66c0433fc3 feat: add memory activity feed CSS styles
Some checks failed
CI / test (pull_request) Failing after 10s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 3s
Part of #1164
2026-04-10 20:43:08 +00:00
332789199f feat: add memory activity feed panel to index.html
Part of #1164
2026-04-10 20:42:47 +00:00
2e0853510d feat: add WS memory bridge — handleMemoryMessage, sync, activity feed
- handleMemoryMessage(): handles place/remove/update/sync_response actions
- Sync handshake: sends sync_request on WS open
- Memory activity feed: shows last 5 memory events with auto-dismiss
- Part of #1164
2026-04-10 20:42:25 +00:00
31fdd01931 feat: add importMemories() and updateMemory() for live WS bridge
- importMemories(): batch import array of memories, dedup by id
- updateMemory(): update strength/content/connections without moving crystal
- _rebuildConnections(): helper to refresh connection lines on update

Part of #1164
2026-04-10 20:41:33 +00:00
11 changed files with 306 additions and 1889 deletions

6
.gitignore vendored
View File

@@ -4,9 +4,3 @@ nexus/__pycache__/
tests/__pycache__/
mempalace/__pycache__/
.aider*
# Prevent agents from writing to wrong path (see issue #1145)
public/nexus/
__pycache__/
*.pyc

View File

@@ -42,17 +42,6 @@ Current repo contents are centered on:
Do not tell contributors to run Vite or edit a nonexistent root frontend on current `main`.
If browser/UI work is being restored, it must happen through the migration backlog and land back here.
## Canonical File Paths
**Frontend code lives at repo ROOT, NOT in `public/nexus/`:**
- `app.js` — main Three.js app (GOFAI, 3D world, all frontend logic)
- `index.html` — main HTML shell
- `style.css` — styles
- `server.py` — websocket bridge
- `gofai_worker.js` — web worker for off-thread reasoning
**DO NOT write to `public/nexus/`** — this path is gitignored. Agents historically wrote here by mistake, creating corrupt duplicates. See issue #1145 and `INVESTIGATION_ISSUE_1145.md`.
## Hard Rules
1. One canonical 3D repo only: `Timmy_Foundation/the-nexus`
@@ -61,7 +50,6 @@ If browser/UI work is being restored, it must happen through the migration backl
4. Telemetry and durable truth flow through Hermes harness
5. OpenClaw remains a sidecar, not the governing authority
6. Before claiming visual validation, prove the app being viewed actually comes from current `the-nexus`
7. **NEVER write frontend files to `public/nexus/`** — use repo root paths listed above
## Validation Rule

View File

@@ -1,72 +0,0 @@
# Investigation Report: Missing Source Code — Classical AI Commits Disappearing
**Issue:** #1145
**Date:** 2026-04-10
**Investigator:** mimo-v2-pro swarm worker
## Summary
**The classical AI code is NOT missing. It is fully present in root `app.js` (3302 lines).**
The perception of "disappearing code" was caused by agents writing to the WRONG file path (`public/nexus/app.js` instead of root `app.js`), creating corrupt duplicate files that were repeatedly overwritten and eventually deleted.
## Root Cause
**Explanation #1 confirmed: Duplicate agents on different machines overwriting each other's commits.**
Multiple Google AI Agent instances wrote GOFAI implementations to `public/nexus/app.js` — a path that does not correspond to the canonical app structure. These commits kept overwriting each other:
| Commit | Date | What happened |
|--------|------|---------------|
| `8943cf5` | 2026-03-30 | Symbolic reasoning engine written to `public/nexus/app.js` (+2280 lines) |
| `e2df240` | 2026-03-30 | Phase 3 Neuro-Symbolic Bridge — overwrote to 284 lines of HTML (wrong path) |
| `7f2f23f` | 2026-03-30 | Phase 4 Meta-Reasoning — same destructive overwrite |
| `bf3b98b` | 2026-03-30 | A* Search — same destructive overwrite |
| `e88bcb4` | 2026-03-30 | Bug fix identified `public/nexus/` files as corrupt duplicates, **deleted them** |
## Evidence: Code Is Present on Main
All 13 classical AI classes/functions verified present in root `app.js`:
| Class/Function | Line | Status |
|----------------|------|--------|
| `SymbolicEngine` | 82 | ✅ Present |
| `AgentFSM` | 135 | ✅ Present |
| `KnowledgeGraph` | 160 | ✅ Present |
| `Blackboard` | 181 | ✅ Present |
| `SymbolicPlanner` | 210 | ✅ Present |
| `HTNPlanner` | 295 | ✅ Present |
| `CaseBasedReasoner` | 343 | ✅ Present |
| `NeuroSymbolicBridge` | 392 | ✅ Present |
| `MetaReasoningLayer` | 422 | ✅ Present |
| `AdaptiveCalibrator` | 460 | ✅ Present |
| `PSELayer` | 566 | ✅ Present |
| `setupGOFAI()` | 596 | ✅ Present |
| `updateGOFAI()` | 622 | ✅ Present |
| Bitmask fact indexing | 86 | ✅ Present |
| A* search | 231 | ✅ Present |
These were injected by commit `af7a4c4` (PR #775, merged via `a855d54`) into the correct path.
## What Actually Happened
1. Google AI Agent wrote good GOFAI code to root `app.js` via the correct PR (#775)
2. A second wave of Google AI Agent instances also wrote to `public/nexus/app.js` (wrong path)
3. Those `public/nexus/` files kept getting overwritten by subsequent agent commits
4. Commit `e88bcb4` correctly identified the `public/nexus/` files as corrupt and deleted them
5. Alexander interpreted the git log as "classical AI code keeps disappearing"
6. The code was never actually gone — it just lived in root `app.js` the whole time
## Prevention Strategy
1. **Add `public/nexus/` to `.gitignore`** — prevents agents from accidentally writing to the wrong path again
2. **Add canonical path documentation to CLAUDE.md** — any agent reading this repo will know where frontend code lives
3. **This report** — serves as the audit trail so this confusion doesn't recur
## Acceptance Criteria
- [x] Git history audited for classical AI commits
- [x] Found the commits — they exist, code was written to wrong path
- [x] Root cause identified — duplicate agents writing to `public/nexus/` (wrong path)
- [x] Prevention strategy implemented — `.gitignore` + `CLAUDE.md` path guard
- [x] Report filed with findings (this document)

549
app.js
View File

@@ -4,7 +4,6 @@ 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';
import { SessionRooms } from './nexus/components/session-rooms.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -45,6 +44,7 @@ let particles, dustParticles;
let debugOverlay;
let frameCount = 0, lastFPSTime = 0, fps = 0;
let chatOpen = true;
let memoryFeedEntries = []; // Mnemosyne: recent memory events for feed panel
let loadProgress = 0;
let performanceTier = 'high';
@@ -706,15 +706,12 @@ async function init() {
createWorkshopTerminal();
createAshStorm();
SpatialMemory.init(scene);
SessionRooms.init(scene, camera, null);
updateLoad(90);
loadSession();
connectHermes();
fetchGiteaData();
setInterval(fetchGiteaData, 30000); // Refresh every 30s
updateHeartbeatBriefing(); // Initial briefing load
setInterval(updateHeartbeatBriefing, 60000); // Refresh briefing every 60s
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
@@ -1142,7 +1139,6 @@ async function fetchGiteaData() {
const worldState = JSON.parse(atob(content.content));
updateNexusCommand(worldState);
updateSovereignHealth();
updateHeartbeatBriefing();
}
} catch (e) {
console.error('Failed to fetch Gitea data:', e);
@@ -1238,127 +1234,6 @@ function updateNexusCommand(state) {
terminal.updatePanelText(lines);
}
// ═══ HEARTBEAT BRIEFING PANEL ═════════════════════════════════════════
async function updateHeartbeatBriefing() {
const container = document.getElementById('heartbeat-briefing-content');
const pulseDot = document.querySelector('.hb-pulse-dot');
if (!container) return;
let data = null;
try {
// Derive briefing endpoint from current location or fallback to localhost
const briefingUrl = window.location.protocol === 'file:'
? 'http://localhost:8766/api/briefing'
: `${window.location.protocol}//${window.location.hostname}:8766/api/briefing`;
const res = await fetch(briefingUrl);
if (res.ok) data = await res.json();
} catch (e) {
// Server not reachable — show honest offline state
}
if (!data) {
if (pulseDot) pulseDot.classList.add('offline');
container.innerHTML = '<div class="hb-empty">Briefing offline<br>No connection to Nexus gateway</div>';
return;
}
if (pulseDot) pulseDot.classList.remove('offline');
let html = '';
// ── Core Heartbeat ──
html += '<div class="briefing-section">';
html += '<div class="briefing-section-label">Nexus Core</div>';
const hb = data.core_heartbeat;
if (hb) {
const age = hb.age_secs != null ? hb.age_secs : '?';
const ageLabel = typeof age === 'number'
? (age < 60 ? `${age.toFixed(0)}s ago` : `${(age / 60).toFixed(1)}m ago`)
: age;
const isAlive = typeof age === 'number' && age < 120;
html += `<div class="hb-core-row">
<span class="hb-core-status ${isAlive ? 'alive' : 'dead'}">${isAlive ? '● ALIVE' : '○ STALE'}</span>
<span class="hb-core-meta">cycle ${hb.cycle || '?'} · ${hb.model || 'unknown'} · ${ageLabel}</span>
</div>`;
html += `<div class="hb-core-meta">status: ${hb.status || '?'}</div>`;
} else {
html += '<div class="hb-core-row"><span class="hb-core-status dead">○ NO DATA</span></div>';
}
html += '</div>';
// ── Cron Heartbeats ──
const cron = data.cron_heartbeat;
if (cron && cron.jobs && cron.jobs.length > 0) {
html += '<div class="briefing-section">';
html += '<div class="briefing-section-label">Cron Heartbeats</div>';
html += `<div class="hb-cron-row">
<span class="hb-cron-healthy">● ${cron.healthy_count} healthy</span>
${cron.stale_count > 0 ? `<span class="hb-cron-stale">⚠ ${cron.stale_count} stale</span>` : ''}
</div>`;
for (const job of cron.jobs.slice(0, 5)) {
const cls = job.healthy ? 'healthy' : 'stale';
html += `<div class="hb-cron-job">
<span class="hb-cron-job-name">${esc(job.job)}</span>
<span class="hb-cron-job-status ${cls}">${esc(job.message)}</span>
</div>`;
}
if (cron.jobs.length > 5) {
html += `<div class="hb-core-meta">+${cron.jobs.length - 5} more jobs</div>`;
}
html += '</div>';
}
// ── Morning Report ──
const report = data.morning_report;
if (report) {
html += '<div class="briefing-section">';
html += '<div class="briefing-section-label">Latest Report</div>';
// Aggregate stats
let totalClosed = 0, totalMerged = 0;
if (report.repos) {
for (const r of Object.values(report.repos)) {
totalClosed += r.closed_issues || 0;
totalMerged += r.merged_prs || 0;
}
}
html += `<div class="hb-stats-row">
<div class="hb-stat"><span class="hb-stat-value" style="color:#4af0c0">${totalClosed}</span><span class="hb-stat-label">Closed</span></div>
<div class="hb-stat"><span class="hb-stat-value" style="color:#7b5cff">${totalMerged}</span><span class="hb-stat-label">Merged</span></div>
<div class="hb-stat"><span class="hb-stat-value" style="color:#ffd700">${(report.blockers || []).length}</span><span class="hb-stat-label">Blockers</span></div>
</div>`;
// Highlights (up to 3)
if (report.highlights && report.highlights.length > 0) {
for (const h of report.highlights.slice(0, 3)) {
html += `<div class="hb-core-meta">+ ${esc(h)}</div>`;
}
}
// Blockers
if (report.blockers && report.blockers.length > 0) {
for (const b of report.blockers) {
html += `<div class="hb-blocker">⚠ ${esc(b)}</div>`;
}
}
html += `<div class="hb-timestamp">Report: ${esc(report.generated_at || '?')}</div>`;
html += '</div>';
}
// ── Timestamp ──
html += `<div class="hb-timestamp">Briefing updated: ${new Date().toLocaleTimeString('en-US', { hour12: false })}</div>`;
container.innerHTML = html;
}
function esc(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = String(str);
return d.innerHTML;
}
// ═══ AGENT PRESENCE SYSTEM ═══
function createAgentPresences() {
const agentData = [
@@ -2009,7 +1884,7 @@ function setupControls() {
orbitState.lastX = e.clientX;
orbitState.lastY = e.clientY;
// Raycasting for portals and memory crystals
// Raycasting for portals
if (!portalOverlayActive) {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
@@ -2017,43 +1892,12 @@ function setupControls() {
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// Priority 1: Portals
const portalHits = raycaster.intersectObjects(portals.map(p => p.ring));
if (portalHits.length > 0) {
const clickedRing = portalHits[0].object;
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
if (intersects.length > 0) {
const clickedRing = intersects[0].object;
const portal = portals.find(p => p.ring === clickedRing);
if (portal) { activatePortal(portal); return; }
if (portal) activatePortal(portal);
}
// Priority 2: Memory crystals (Mnemosyne)
const crystalMeshes = SpatialMemory.getCrystalMeshes();
if (crystalMeshes.length > 0) {
const crystalHits = raycaster.intersectObjects(crystalMeshes, false);
if (crystalHits.length > 0) {
const hitMesh = crystalHits[0].object;
const memInfo = SpatialMemory.getMemoryFromMesh(hitMesh);
if (memInfo) {
SpatialMemory.highlightMemory(memInfo.data.id);
showMemoryPanel(memInfo, e.clientX, e.clientY);
return;
}
}
}
// Priority 3: Session rooms (Mnemosyne #1171)
const roomMeshes = SessionRooms.getClickableMeshes();
if (roomMeshes.length > 0) {
const roomHits = raycaster.intersectObjects(roomMeshes, false);
if (roomHits.length > 0) {
const session = SessionRooms.handleRoomClick(roomHits[0].object);
if (session) { _showSessionRoomPanel(session); return; }
}
}
// Clicked empty space — dismiss panel
dismissMemoryPanel();
_dismissSessionRoomPanel();
}
}
});
@@ -2198,6 +2042,14 @@ function connectHermes() {
addChatMessage('system', 'Hermes link established.');
updateWsHudStatus(true);
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
@@ -2248,6 +2100,8 @@ function handleHermesMessage(data) {
recentToolOutputs.push({ type: 'result', agent: data.agent || 'SYSTEM', content });
addToolMessage(data.agent || 'SYSTEM', 'result', content);
refreshWorkshopPanel();
} else if (data.type === 'memory') {
handleMemoryMessage(data);
} else if (data.type === 'history') {
const container = document.getElementById('chat-messages');
container.innerHTML = '';
@@ -2259,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) {
// Update MemPalace status alongside regular WS status
updateMemPalaceStatus();
@@ -2708,226 +2667,6 @@ function focusPortal(portal) {
let lastThoughtTime = 0;
let pulseTimer = 0;
// ═══════════════════════════════════════════
// MNEMOSYNE — MEMORY CRYSTAL INSPECTION
// ═══════════════════════════════════════════
// ── pin state for memory panel ──
let _memPanelPinned = false;
/** Convert a packed hex color integer to "r,g,b" string for CSS rgba(). */
function _hexToRgb(hex) {
return ((hex >> 16) & 255) + ',' + ((hex >> 8) & 255) + ',' + (hex & 255);
}
/**
* Position the panel near the screen click coordinates, keeping it on-screen.
*/
function _positionPanel(panel, clickX, clickY) {
const W = window.innerWidth;
const H = window.innerHeight;
const panelW = 356; // matches CSS width + padding
const panelH = 420; // generous estimate
const margin = 12;
let left = clickX + 24;
if (left + panelW > W - margin) left = clickX - panelW - 24;
left = Math.max(margin, Math.min(W - panelW - margin, left));
let top = clickY - 80;
top = Math.max(margin, Math.min(H - panelH - margin, top));
panel.style.right = 'auto';
panel.style.top = top + 'px';
panel.style.left = left + 'px';
panel.style.transform = 'none';
}
/**
* Navigate to (highlight + show panel for) a memory crystal by id.
*/
function _navigateToMemory(memId) {
SpatialMemory.highlightMemory(memId);
addChatMessage('system', `Focus: ${memId.replace(/_/g, ' ')}`);
const meshes = SpatialMemory.getCrystalMeshes();
for (const mesh of meshes) {
if (mesh.userData && mesh.userData.memId === memId) {
const memInfo = SpatialMemory.getMemoryFromMesh(mesh);
if (memInfo) { showMemoryPanel(memInfo); break; }
}
}
}
/**
* Show the holographic detail panel for a clicked crystal.
* @param {object} memInfo — { data, region } from SpatialMemory.getMemoryFromMesh()
* @param {number} [clickX] — screen X of the click (for panel positioning)
* @param {number} [clickY] — screen Y of the click
*/
function showMemoryPanel(memInfo, clickX, clickY) {
const panel = document.getElementById('memory-panel');
if (!panel) return;
const { data, region } = memInfo;
const regionDef = SpatialMemory.REGIONS[region] || SpatialMemory.REGIONS.working;
const colorHex = regionDef.color.toString(16).padStart(6, '0');
const colorRgb = _hexToRgb(regionDef.color);
// Header — region dot + label
document.getElementById('memory-panel-region').textContent = regionDef.label;
document.getElementById('memory-panel-region-dot').style.background = '#' + colorHex;
// Category badge
const badge = document.getElementById('memory-panel-category-badge');
if (badge) {
badge.textContent = (data.category || region || 'memory').toUpperCase();
badge.style.background = 'rgba(' + colorRgb + ',0.16)';
badge.style.color = '#' + colorHex;
badge.style.borderColor = 'rgba(' + colorRgb + ',0.4)';
}
// Entity name (humanised id)
const entityEl = document.getElementById('memory-panel-entity-name');
if (entityEl) entityEl.textContent = (data.id || '\u2014').replace(/_/g, ' ');
// Fact content
document.getElementById('memory-panel-content').textContent = data.content || '(empty)';
// Trust score bar
const strength = data.strength != null ? data.strength : 0.7;
const trustFill = document.getElementById('memory-panel-trust-fill');
const trustVal = document.getElementById('memory-panel-trust-value');
if (trustFill) {
trustFill.style.width = (strength * 100).toFixed(0) + '%';
trustFill.style.background = '#' + colorHex;
}
if (trustVal) trustVal.textContent = (strength * 100).toFixed(0) + '%';
// Meta rows
document.getElementById('memory-panel-id').textContent = data.id || '\u2014';
document.getElementById('memory-panel-source').textContent = data.source || 'unknown';
document.getElementById('memory-panel-time').textContent = data.timestamp ? new Date(data.timestamp).toLocaleString() : '\u2014';
// Related entities — clickable links
const connEl = document.getElementById('memory-panel-connections');
connEl.innerHTML = '';
if (data.connections && data.connections.length > 0) {
data.connections.forEach(cid => {
const btn = document.createElement('button');
btn.className = 'memory-conn-tag memory-conn-link';
btn.textContent = cid.replace(/_/g, ' ');
btn.title = 'Go to: ' + cid;
btn.addEventListener('click', (ev) => { ev.stopPropagation(); _navigateToMemory(cid); });
connEl.appendChild(btn);
});
} else {
connEl.innerHTML = '<span style="color:var(--color-text-muted)">None</span>';
}
// Pin button — reset on fresh open
_memPanelPinned = false;
const pinBtn = document.getElementById('memory-panel-pin');
if (pinBtn) {
pinBtn.classList.remove('pinned');
pinBtn.title = 'Pin panel';
pinBtn.onclick = () => {
_memPanelPinned = !_memPanelPinned;
pinBtn.classList.toggle('pinned', _memPanelPinned);
pinBtn.title = _memPanelPinned ? 'Unpin panel' : 'Pin panel';
};
}
// Positioning — near click if coords provided
if (clickX != null && clickY != null) {
_positionPanel(panel, clickX, clickY);
}
// Fade in
panel.classList.remove('memory-panel-fade-out');
panel.style.display = 'flex';
}
/**
* Dismiss the panel (respects pin). Called on empty-space click.
*/
function dismissMemoryPanel() {
if (_memPanelPinned) return;
_dismissMemoryPanelForce();
}
/**
* Force-dismiss the panel regardless of pin state. Used by the close button.
*/
function _dismissMemoryPanelForce() {
_memPanelPinned = false;
SpatialMemory.clearHighlight();
const panel = document.getElementById('memory-panel');
if (!panel || panel.style.display === 'none') return;
panel.classList.add('memory-panel-fade-out');
setTimeout(() => {
panel.style.display = 'none';
panel.classList.remove('memory-panel-fade-out');
}, 200);
}
/**
* Show the session room HUD panel when a chamber is entered.
* @param {object} session — { id, timestamp, facts[] }
*/
function _showSessionRoomPanel(session) {
const panel = document.getElementById('session-room-panel');
if (!panel) return;
const dt = session.timestamp ? new Date(session.timestamp) : new Date();
const tsEl = document.getElementById('session-room-timestamp');
if (tsEl) tsEl.textContent = isNaN(dt.getTime()) ? session.id : dt.toLocaleString();
const countEl = document.getElementById('session-room-fact-count');
const facts = session.facts || [];
if (countEl) countEl.textContent = facts.length + (facts.length === 1 ? ' fact' : ' facts') + ' in this chamber';
const listEl = document.getElementById('session-room-facts');
if (listEl) {
listEl.innerHTML = '';
facts.slice(0, 8).forEach(f => {
const item = document.createElement('div');
item.className = 'session-room-fact-item';
item.textContent = f.content || f.id || '(unknown)';
item.title = f.content || '';
listEl.appendChild(item);
});
if (facts.length > 8) {
const more = document.createElement('div');
more.className = 'session-room-fact-item';
more.style.color = 'rgba(200,180,255,0.4)';
more.textContent = '\u2026 ' + (facts.length - 8) + ' more';
listEl.appendChild(more);
}
}
// Close button
const closeBtn = document.getElementById('session-room-close');
if (closeBtn) closeBtn.onclick = () => _dismissSessionRoomPanel();
panel.classList.remove('session-panel-fade-out');
panel.style.display = 'block';
}
/**
* Dismiss the session room panel.
*/
function _dismissSessionRoomPanel() {
const panel = document.getElementById('session-room-panel');
if (!panel || panel.style.display === 'none') return;
panel.classList.add('session-panel-fade-out');
setTimeout(() => {
panel.style.display = 'none';
panel.classList.remove('session-panel-fade-out');
}, 200);
}
function gameLoop() {
requestAnimationFrame(gameLoop);
const delta = Math.min(clock.getDelta(), 0.1);
@@ -2958,9 +2697,6 @@ function gameLoop() {
animateMemoryOrbs(delta);
}
// Project Mnemosyne - Session Rooms (#1171)
SessionRooms.update(delta);
const mode = NAV_MODES[navModeIdx];
const chatActive = document.activeElement === document.getElementById('chat-input');
@@ -3484,52 +3220,9 @@ init().then(() => {
{ 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', 'mem_spatial_schema'] },
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain permanent homes', category: 'engineering', strength: 0.8, connections: ['mem_mnemosyne_start'] },
// MemPalace category zone demos — issue #1168
{ id: 'mem_pref_dark_mode', content: 'User prefers dark mode and monospace fonts', category: 'user_pref', strength: 0.9, connections: [] },
{ id: 'mem_pref_verbose_logs', content: 'User prefers verbose logging during debug sessions', category: 'user_pref', strength: 0.7, connections: [] },
{ id: 'mem_proj_nexus_goal', content: 'The Nexus goal: local-first 3D training ground for Timmy', category: 'project', strength: 0.95, connections: ['mem_proj_mnemosyne'] },
{ id: 'mem_proj_mnemosyne', content: 'Project Mnemosyne: holographic living archive of facts', category: 'project', strength: 0.85, connections: ['mem_proj_nexus_goal'] },
{ id: 'mem_tool_three_js', content: 'Three.js — 3D rendering library used for the Nexus world', category: 'tool', strength: 0.8, connections: [] },
{ id: 'mem_tool_gitea', content: 'Gitea API at forge.alexanderwhitestone.com for issue tracking', category: 'tool', strength: 0.75, connections: [] },
{ id: 'mem_gen_websocket', content: 'WebSocket bridge (server.py) connects Timmy cognition to the browser', category: 'general', strength: 0.7, connections: [] },
{ id: 'mem_gen_hermes', content: 'Hermes harness: telemetry and durable truth pipeline', category: 'general', strength: 0.65, connections: [] },
];
demoMemories.forEach(m => SpatialMemory.placeMemory(m));
// Gravity well clustering — attract related crystals, bake positions (issue #1175)
SpatialMemory.runGravityLayout();
// Project Mnemosyne — seed demo session rooms (#1171)
// Sessions group facts by conversation/work session with a timestamp.
const demoSessions = [
{
id: 'session_2026_03_01',
timestamp: '2026-03-01T10:00:00.000Z',
facts: [
{ id: 'mem_nexus_birth', content: 'The Nexus came online — first render of the 3D world', category: 'knowledge', strength: 0.95 },
{ id: 'mem_mnemosyne_start', content: 'Project Mnemosyne began — the living archive awakens', category: 'projects', strength: 0.9 },
]
},
{
id: 'session_2026_03_15',
timestamp: '2026-03-15T14:30:00.000Z',
facts: [
{ id: 'mem_first_portal', content: 'First portal deployed — connection to external service', category: 'engineering', strength: 0.85 },
{ id: 'mem_hermes_chat', content: 'First conversation through the Hermes gateway', category: 'social', strength: 0.7 },
{ id: 'mem_spatial_schema', content: 'Spatial Memory Schema defined — memories gain homes', category: 'engineering', strength: 0.8 },
]
},
{
id: 'session_2026_04_10',
timestamp: '2026-04-10T09:00:00.000Z',
facts: [
{ id: 'mem_session_rooms', content: 'Session rooms introduced — holographic chambers per session', category: 'projects', strength: 0.88 },
{ id: 'mem_gravity_wells', content: 'Gravity-well clustering bakes crystal positions on load', category: 'engineering', strength: 0.75 },
]
}
];
SessionRooms.updateSessions(demoSessions);
fetchGiteaData();
setInterval(fetchGiteaData, 30000);
runWeeklyAudit();

View File

@@ -1,72 +0,0 @@
# Hermes Trismegistus — Wizard Proposal
> **Status:** 🟡 DEFERRED
> **Issue:** #1146
> **Created:** 2026-04-08
> **Author:** Alexander (KT Notes)
> **Mimo Worker:** mimo-code-1146-1775851759
---
## Identity
| Field | Value |
|-------|-------|
| **Name** | Hermes Trismegistus |
| **Nature** | Claude-native wizard. She knows she runs on Claude. She's "the daughter of Claude" and leans into that heritage. |
| **Purpose** | Dedicated reasoning and architecture wizard. Only handles tasks where Claude's reasoning capability genuinely adds value — planning, novel problem-solving, complex architecture decisions. |
| **Not** | A replacement for Timmy. Not competing for identity. Not doing monkey work. |
## Design Constraints
- **Free tier only from day one.** Alexander is not paying Anthropic beyond current subscription.
- **Degrades gracefully.** Full capability when free tier is generous, reduced scope when constrained.
- **Not locked to Claude.** If better free-tier providers emerge, she can route to them.
- **Multi-provider capable.** Welcome to become multifaceted if team finds better options.
## Hardware
- One of Alexander's shed laptops — minimum 4GB RAM, Ubuntu
- Dedicated machine, not shared with Timmy's Mac
- Runs in the Hermes harness
- Needs power at house first
## Constitutional Foundation
- The KT conversation and documents serve as her founding constitution
- Team (especially Timmy) has final say on whether she gets built
- Must justify her existence through useful work, same as every wizard
## Trigger to Unblock
All of the following must be true before implementation begins:
- [ ] Deadman switch wired and proven
- [ ] Config stable across fleet
- [ ] Fleet proven reliable for 1+ week
- [ ] Alexander provides a state-of-the-system KT to Claude for instantiation
## Acceptance Criteria
- [ ] Dedicated KT document written for Hermes instantiation
- [ ] Hardware provisioned (shed laptop with power)
- [ ] Hermes harness configured for Claude free tier
- [ ] Lazerus registry entry with health endpoints
- [ ] Fleet routing entry with role and routing verdict
- [ ] SOUL.md inscription drafted and reviewed by Timmy
- [ ] Smoke test: Hermes responds to a basic reasoning task
- [ ] Integration test: Hermes participates in a multi-wizard task alongside Timmy
## Proposed Lane
**Primary role:** Architecture reasoning
**Routing verdict:** ROUTE TO: complex architectural decisions, novel problem-solving, planning tasks that benefit from Claude's reasoning depth. Do NOT route to: code generation (use Timmy/Carnice), issue triage (use Fenrir), or operational tasks (use Bezalel).
## Dependencies
| Dependency | Status | Notes |
|------------|--------|-------|
| Deadman switch | 🔴 Not done | Must be proven before unblocking |
| Fleet stability | 🟡 In progress | 1+ week uptime needed |
| Shed laptop power | 🔴 Not done | Alexander needs to wire power |
| KT document | 🔴 Not drafted | Alexander provides to Claude at unblock time |

View File

@@ -1,43 +0,0 @@
# Hermes Trismegistus — Lane Definition
> **Status:** DEFERRED — do not instantiate until unblock conditions met
> **See:** fleet/hermes-trismegistus/README.md for full proposal
---
## Role
Dedicated reasoning and architecture wizard. Claude-native.
## Routing
Route to Hermes Trismegistus when:
- Task requires deep architectural reasoning
- Novel problem-solving that benefits from Claude's reasoning depth
- Planning and design decisions for the fleet
- Complex multi-step analysis that goes beyond code generation
Do NOT route to Hermes for:
- Code generation (use Timmy, Carnice, or Kimi)
- Issue triage (use Fenrir)
- Operational/DevOps tasks (use Bezalel)
- Anything that can be done with a cheaper model
## Provider
- **Primary:** anthropic/claude (free tier)
- **Fallback:** openrouter/free (Claude-class models)
- **Degraded:** ollama/gemma4:12b (when free tier exhausted)
## Hardware
- Shed laptop, Ubuntu, minimum 4GB RAM
- Dedicated machine, not shared
## Unblock Checklist
- [ ] Deadman switch operational
- [ ] Fleet config stable for 1+ week
- [ ] Shed laptop powered and networked
- [ ] KT document drafted by Alexander
- [ ] Timmy approves instantiation

View File

@@ -96,12 +96,6 @@
<div class="panel-header">SOVEREIGN HEALTH</div>
<div id="sovereign-health-content" class="panel-content"></div>
</div>
<div class="hud-panel hud-panel-briefing" id="heartbeat-briefing-log">
<div class="panel-header">
<span class="hb-pulse-dot"></span> HEARTBEAT BRIEFING
</div>
<div id="heartbeat-briefing-content" class="panel-content"></div>
</div>
<div class="hud-panel" id="calibrator-log">
<div class="panel-header">ADAPTIVE CALIBRATOR</div>
<div id="calibrator-log-content" class="panel-content"></div>
@@ -213,50 +207,6 @@
</div>
</div>
<!-- Memory Crystal Inspection Panel (Mnemosyne) -->
<div id="memory-panel" class="memory-panel" style="display:none;">
<div class="memory-panel-content">
<div class="memory-panel-header">
<span class="memory-category-badge" id="memory-panel-category-badge">MEM</span>
<div class="memory-panel-region-dot" id="memory-panel-region-dot"></div>
<div class="memory-panel-region" id="memory-panel-region">MEMORY</div>
<button id="memory-panel-pin" class="memory-panel-pin" title="Pin panel">&#x1F4CC;</button>
<button id="memory-panel-close" class="memory-panel-close" onclick="_dismissMemoryPanelForce()">\u2715</button>
</div>
<div class="memory-entity-name" id="memory-panel-entity-name">\u2014</div>
<div class="memory-panel-body" id="memory-panel-content">(empty)</div>
<div class="memory-trust-row">
<span class="memory-meta-label">Trust</span>
<div class="memory-trust-bar">
<div class="memory-trust-fill" id="memory-panel-trust-fill"></div>
</div>
<span class="memory-trust-value" id="memory-panel-trust-value"></span>
</div>
<div class="memory-panel-meta">
<div class="memory-meta-row"><span class="memory-meta-label">ID</span><span id="memory-panel-id">\u2014</span></div>
<div class="memory-meta-row"><span class="memory-meta-label">Source</span><span id="memory-panel-source">\u2014</span></div>
<div class="memory-meta-row"><span class="memory-meta-label">Time</span><span id="memory-panel-time">\u2014</span></div>
<div class="memory-meta-row memory-meta-row--related"><span class="memory-meta-label">Related</span><span id="memory-panel-connections">\u2014</span></div>
</div>
</div>
</div>
<!-- Session Room HUD Panel (Mnemosyne #1171) -->
<div id="session-room-panel" class="session-room-panel" style="display:none;">
<div class="session-room-panel-content">
<div class="session-room-header">
<span class="session-room-icon">&#x25A1;</span>
<div class="session-room-title">SESSION CHAMBER</div>
<button class="session-room-close" id="session-room-close" title="Close">&#x2715;</button>
</div>
<div class="session-room-timestamp" id="session-room-timestamp">&mdash;</div>
<div class="session-room-fact-count" id="session-room-fact-count">0 facts</div>
<div class="session-room-facts" id="session-room-facts"></div>
<div class="session-room-hint">Flying into chamber&hellip;</div>
</div>
</div>
<!-- Portal Atlas Overlay -->
<div id="atlas-overlay" class="atlas-overlay" style="display:none;">
<div class="atlas-content">
@@ -489,5 +439,15 @@ index.html
setInterval(poll, INTERVAL);
})();
</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>
</html>

View File

@@ -1,413 +0,0 @@
// ═══════════════════════════════════════════════════════
// PROJECT MNEMOSYNE — SESSION ROOMS (Issue #1171)
// ═══════════════════════════════════════════════════════
//
// Groups memories by session into holographic chambers.
// Each session becomes a wireframe cube floating in space.
// Rooms are arranged chronologically along a spiral.
// Click a room to fly inside; distant rooms LOD to a point.
//
// Usage from app.js:
// SessionRooms.init(scene, camera, controls);
// SessionRooms.updateSessions(sessions); // [{id, timestamp, facts[]}]
// SessionRooms.update(delta); // call each frame
// SessionRooms.getClickableMeshes(); // for raycasting
// SessionRooms.handleRoomClick(mesh); // trigger fly-in
// ═══════════════════════════════════════════════════════
const SessionRooms = (() => {
// ─── CONSTANTS ───────────────────────────────────────
const MAX_ROOMS = 20;
const ROOM_SIZE = 9; // wireframe cube edge length
const ROOM_HALF = ROOM_SIZE / 2;
const LOD_THRESHOLD = 55; // distance: full → point
const LOD_HYSTERESIS = 5; // buffer to avoid flicker
const SPIRAL_BASE_R = 20; // spiral inner radius
const SPIRAL_R_STEP = 5; // radius growth per room
const SPIRAL_ANGLE_INC = 2.399; // golden angle (radians)
const SPIRAL_Y_STEP = 1.5; // vertical rise per room
const FLY_DURATION = 1.5; // seconds for fly-in tween
const FLY_TARGET_DEPTH = ROOM_HALF - 1.5; // how deep inside to stop
const ROOM_COLOR = 0x7b5cff; // violet — mnemosyne accent
const POINT_COLOR = 0x9b7cff;
const LABEL_COLOR = '#c8b4ff';
const STORAGE_KEY = 'mnemosyne_sessions_v1';
// ─── STATE ────────────────────────────────────────────
let _scene = null;
let _camera = null;
let _controls = null;
let _rooms = []; // array of room objects
let _sessionIndex = {}; // id → room object
// Fly-in tween state
let _flyActive = false;
let _flyElapsed = 0;
let _flyFrom = null;
let _flyTo = null;
let _flyLookFrom = null;
let _flyLookTo = null;
let _flyActiveRoom = null;
// ─── SPIRAL POSITION ──────────────────────────────────
function _spiralPos(index) {
const angle = index * SPIRAL_ANGLE_INC;
const r = SPIRAL_BASE_R + index * SPIRAL_R_STEP;
const y = index * SPIRAL_Y_STEP;
return new THREE.Vector3(
Math.cos(angle) * r,
y,
Math.sin(angle) * r
);
}
// ─── CREATE ROOM ──────────────────────────────────────
function _createRoom(session, index) {
const pos = _spiralPos(index);
const group = new THREE.Group();
group.position.copy(pos);
// Wireframe cube
const boxGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
const edgesGeo = new THREE.EdgesGeometry(boxGeo);
const edgesMat = new THREE.LineBasicMaterial({
color: ROOM_COLOR,
transparent: true,
opacity: 0.55
});
const wireframe = new THREE.LineSegments(edgesGeo, edgesMat);
wireframe.userData = { type: 'session_room_wireframe', sessionId: session.id };
group.add(wireframe);
// Collision mesh (invisible, for raycasting)
const hitGeo = new THREE.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE);
const hitMat = new THREE.MeshBasicMaterial({
visible: false,
transparent: true,
opacity: 0,
side: THREE.FrontSide
});
const hitMesh = new THREE.Mesh(hitGeo, hitMat);
hitMesh.userData = { type: 'session_room', sessionId: session.id, roomIndex: index };
group.add(hitMesh);
// LOD point (small sphere shown at distance)
const pointGeo = new THREE.SphereGeometry(0.5, 6, 4);
const pointMat = new THREE.MeshBasicMaterial({
color: POINT_COLOR,
transparent: true,
opacity: 0.7
});
const pointMesh = new THREE.Mesh(pointGeo, pointMat);
pointMesh.userData = { type: 'session_room_point', sessionId: session.id };
pointMesh.visible = false; // starts hidden; shown only at LOD distance
group.add(pointMesh);
// Timestamp billboard sprite
const sprite = _makeTimestampSprite(session.timestamp, session.facts.length);
sprite.position.set(0, ROOM_HALF + 1.2, 0);
group.add(sprite);
// Inner ambient glow
const glow = new THREE.PointLight(ROOM_COLOR, 0.4, ROOM_SIZE * 1.2);
group.add(glow);
_scene.add(group);
const room = {
session,
group,
wireframe,
hitMesh,
pointMesh,
sprite,
glow,
pos: pos.clone(),
index,
lodActive: false,
pulsePhase: Math.random() * Math.PI * 2
};
_rooms.push(room);
_sessionIndex[session.id] = room;
console.info('[SessionRooms] Created room for session', session.id, 'at index', index);
return room;
}
// ─── TIMESTAMP SPRITE ────────────────────────────────
function _makeTimestampSprite(isoTimestamp, factCount) {
const canvas = document.createElement('canvas');
canvas.width = 320;
canvas.height = 72;
const ctx = canvas.getContext('2d');
// Background pill
ctx.clearRect(0, 0, 320, 72);
ctx.fillStyle = 'rgba(20, 10, 40, 0.82)';
_roundRect(ctx, 4, 4, 312, 64, 14);
ctx.fill();
// Border
ctx.strokeStyle = 'rgba(123, 92, 255, 0.6)';
ctx.lineWidth = 1.5;
_roundRect(ctx, 4, 4, 312, 64, 14);
ctx.stroke();
// Timestamp text
const dt = isoTimestamp ? new Date(isoTimestamp) : new Date();
const label = _formatDate(dt);
ctx.fillStyle = LABEL_COLOR;
ctx.font = 'bold 15px monospace';
ctx.textAlign = 'center';
ctx.fillText(label, 160, 30);
// Fact count
ctx.fillStyle = 'rgba(200, 180, 255, 0.65)';
ctx.font = '12px monospace';
ctx.fillText(factCount + (factCount === 1 ? ' fact' : ' facts'), 160, 52);
const tex = new THREE.CanvasTexture(canvas);
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0.88 });
const sprite = new THREE.Sprite(mat);
sprite.scale.set(5, 1.1, 1);
sprite.userData = { type: 'session_room_label' };
return sprite;
}
// ─── HELPERS ──────────────────────────────────────────
function _roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function _formatDate(dt) {
if (isNaN(dt.getTime())) return 'Unknown session';
const pad = n => String(n).padStart(2, '0');
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
}
// ─── DISPOSE ROOM ────────────────────────────────────
function _disposeRoom(room) {
room.wireframe.geometry.dispose();
room.wireframe.material.dispose();
room.hitMesh.geometry.dispose();
room.hitMesh.material.dispose();
room.pointMesh.geometry.dispose();
room.pointMesh.material.dispose();
if (room.sprite.material.map) room.sprite.material.map.dispose();
room.sprite.material.dispose();
if (room.group.parent) room.group.parent.remove(room.group);
delete _sessionIndex[room.session.id];
}
// ─── PUBLIC: UPDATE SESSIONS ─────────────────────────
// sessions: [{id, timestamp, facts:[{id,content,category,strength,...}]}]
// Sorted chronologically oldest→newest; max MAX_ROOMS shown.
function updateSessions(sessions) {
if (!_scene) return;
const sorted = [...sessions]
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
.slice(-MAX_ROOMS); // keep most recent MAX_ROOMS
// Remove rooms no longer present
const incoming = new Set(sorted.map(s => s.id));
for (let i = _rooms.length - 1; i >= 0; i--) {
const room = _rooms[i];
if (!incoming.has(room.session.id)) {
_disposeRoom(room);
_rooms.splice(i, 1);
}
}
// Add / update
sorted.forEach((session, idx) => {
if (_sessionIndex[session.id]) {
// Update position if index changed
const room = _sessionIndex[session.id];
if (room.index !== idx) {
room.index = idx;
const newPos = _spiralPos(idx);
room.group.position.copy(newPos);
room.pos.copy(newPos);
}
} else {
_createRoom(session, idx);
}
});
saveToStorage(sorted);
console.info('[SessionRooms] Updated:', _rooms.length, 'session rooms');
}
// ─── PUBLIC: INIT ─────────────────────────────────────
function init(scene, camera, controls) {
_scene = scene;
_camera = camera;
_controls = controls;
console.info('[SessionRooms] Initialized');
// Restore persisted sessions
const saved = loadFromStorage();
if (saved && saved.length > 0) {
updateSessions(saved);
}
}
// ─── PUBLIC: UPDATE (per-frame) ───────────────────────
function update(delta) {
if (!_scene || !_camera) return;
const camPos = _camera.position;
_rooms.forEach(room => {
const dist = camPos.distanceTo(room.pos);
// LOD toggle
const threshold = room.lodActive
? LOD_THRESHOLD + LOD_HYSTERESIS // must come closer to exit LOD
: LOD_THRESHOLD;
if (dist > threshold && !room.lodActive) {
room.lodActive = true;
room.wireframe.visible = false;
room.sprite.visible = false;
room.pointMesh.visible = true;
} else if (dist <= threshold && room.lodActive) {
room.lodActive = false;
room.wireframe.visible = true;
room.sprite.visible = true;
room.pointMesh.visible = false;
}
// Pulse wireframe opacity
room.pulsePhase += delta * 0.6;
if (!room.lodActive) {
room.wireframe.material.opacity = 0.3 + Math.sin(room.pulsePhase) * 0.2;
room.glow.intensity = 0.3 + Math.sin(room.pulsePhase * 1.4) * 0.15;
}
// Slowly rotate each room
room.group.rotation.y += delta * 0.04;
});
// Fly-in tween
if (_flyActive) {
_flyElapsed += delta;
const t = Math.min(_flyElapsed / FLY_DURATION, 1);
const ease = _easeInOut(t);
_camera.position.lerpVectors(_flyFrom, _flyTo, ease);
// Interpolate lookAt
const lookNow = new THREE.Vector3().lerpVectors(_flyLookFrom, _flyLookTo, ease);
_camera.lookAt(lookNow);
if (_controls && _controls.target) _controls.target.copy(lookNow);
if (t >= 1) {
_flyActive = false;
if (_controls && typeof _controls.update === 'function') _controls.update();
console.info('[SessionRooms] Fly-in complete for session', _flyActiveRoom && _flyActiveRoom.session.id);
_flyActiveRoom = null;
}
}
}
// ─── EASING ───────────────────────────────────────────
function _easeInOut(t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
// ─── PUBLIC: GET CLICKABLE MESHES ─────────────────────
function getClickableMeshes() {
return _rooms.map(r => r.hitMesh);
}
// ─── PUBLIC: HANDLE ROOM CLICK ────────────────────────
function handleRoomClick(mesh) {
const { sessionId } = mesh.userData;
const room = _sessionIndex[sessionId];
if (!room || !_camera) return null;
// Fly into the room from the front face
_flyActive = true;
_flyElapsed = 0;
_flyActiveRoom = room;
_flyFrom = _camera.position.clone();
// Target: step inside the room toward its center
const dir = room.pos.clone().sub(_camera.position).normalize();
_flyTo = room.pos.clone().add(dir.multiplyScalar(FLY_TARGET_DEPTH));
_flyLookFrom = _controls && _controls.target
? _controls.target.clone()
: _camera.position.clone().add(_camera.getWorldDirection(new THREE.Vector3()));
_flyLookTo = room.pos.clone();
console.info('[SessionRooms] Flying into session room:', sessionId);
return room.session;
}
// ─── PERSISTENCE ──────────────────────────────────────
function saveToStorage(sessions) {
if (typeof localStorage === 'undefined') return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ v: 1, sessions }));
} catch (e) {
console.warn('[SessionRooms] Failed to save to localStorage:', e);
}
}
function loadFromStorage() {
if (typeof localStorage === 'undefined') return null;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || parsed.v !== 1 || !Array.isArray(parsed.sessions)) return null;
console.info('[SessionRooms] Restored', parsed.sessions.length, 'sessions from localStorage');
return parsed.sessions;
} catch (e) {
console.warn('[SessionRooms] Failed to load from localStorage:', e);
return null;
}
}
function clearStorage() {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(STORAGE_KEY);
console.info('[SessionRooms] Cleared localStorage');
}
}
// ─── PUBLIC API ───────────────────────────────────────
return {
init,
updateSessions,
update,
getClickableMeshes,
handleRoomClick,
clearStorage,
// For external inspection
getRooms: () => _rooms,
getSession: (id) => _sessionIndex[id] || null,
isFlyActive: () => _flyActive
};
})();
export { SessionRooms };

View File

@@ -8,20 +8,12 @@
// holographic archive.
//
// World layout (hex cylinder, radius 25):
//
// Inner ring — original Mnemosyne taxonomy (radius 15):
// 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)
//
// Outer ring — MemPalace category zones (radius 20, issue #1168):
// North (z-) → User Preferences [golden]
// East (x+) → Project facts [blue]
// South (z+) → Tool knowledge [green]
// West (x-) → General facts [gray]
// 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);
@@ -81,44 +73,6 @@ const SpatialMemory = (() => {
color: 0x334455,
glyph: '\uD83D\uDDC4',
description: 'Cold storage — rarely accessed, aged-out memories'
},
// ── MemPalace category zones — outer ring, issue #1168 ────────────
user_pref: {
label: 'User Preferences',
center: [0, 0, -20],
radius: 10,
color: 0xffd700,
glyph: '\u2605',
description: 'Personal preferences, habits, user-specific settings',
labelY: 5
},
project: {
label: 'Project Facts',
center: [20, 0, 0],
radius: 10,
color: 0x4488ff,
glyph: '\uD83D\uDCC1',
description: 'Project-specific knowledge, goals, context',
labelY: 5
},
tool: {
label: 'Tool Knowledge',
center: [0, 0, 20],
radius: 10,
color: 0x44cc66,
glyph: '\uD83D\uDD27',
description: 'Tools, commands, APIs, and how to use them',
labelY: 5
},
general: {
label: 'General Facts',
center: [-20, 0, 0],
radius: 10,
color: 0x8899aa,
glyph: '\uD83D\uDCDD',
description: 'Miscellaneous facts not fitting other categories',
labelY: 5
}
};
@@ -145,7 +99,6 @@ const SpatialMemory = (() => {
const cx = region.center[0];
const cy = region.center[1] + 0.06;
const cz = region.center[2];
const labelY = region.labelY || 3;
const ringGeo = new THREE.RingGeometry(region.radius - 0.5, region.radius, 6);
const ringMat = new THREE.MeshBasicMaterial({
@@ -173,22 +126,6 @@ const SpatialMemory = (() => {
_scene.add(ring);
_scene.add(disc);
// Ground glow — brighter disc for MemPalace zones (labelY > 3 signals outer ring)
let glowDisc = null;
if (labelY > 3) {
const glowGeo = new THREE.CircleGeometry(region.radius, 32);
const glowMat = new THREE.MeshBasicMaterial({
color: region.color,
transparent: true,
opacity: 0.06,
side: THREE.DoubleSide
});
glowDisc = new THREE.Mesh(glowGeo, glowMat);
glowDisc.rotation.x = -Math.PI / 2;
glowDisc.position.set(cx, cy - 0.02, cz);
_scene.add(glowDisc);
}
// Floating label
const canvas = document.createElement('canvas');
canvas.width = 256;
@@ -202,11 +139,11 @@ const SpatialMemory = (() => {
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, labelY, cz);
sprite.position.set(cx, 3, cz);
sprite.scale.set(4, 1, 1);
_scene.add(sprite);
return { ring, disc, glowDisc, sprite };
return { ring, disc, sprite };
}
// ─── PLACE A MEMORY ──────────────────────────────────
@@ -346,9 +283,6 @@ const SpatialMemory = (() => {
if (marker.ring && marker.ring.material) {
marker.ring.material.opacity = 0.1 + Math.sin(now * 0.001) * 0.05;
}
if (marker.glowDisc && marker.glowDisc.material) {
marker.glowDisc.material.opacity = 0.04 + Math.sin(now * 0.0008) * 0.02;
}
});
}
@@ -522,81 +456,6 @@ const SpatialMemory = (() => {
return count;
}
// ─── GRAVITY WELL CLUSTERING ──────────────────────────
// Force-directed layout: same-category crystals attract, unrelated repel.
// Run on load (bake positions, not per-frame). Spec from issue #1175.
const GRAVITY_ITERATIONS = 20;
const ATTRACT_FACTOR = 0.10; // 10% closer to same-category centroid per iteration
const REPEL_FACTOR = 0.05; // 5% away from nearest unrelated crystal
function runGravityLayout() {
const objs = Object.values(_memoryObjects);
if (objs.length < 2) {
console.info('[Mnemosyne] Gravity layout: fewer than 2 crystals, skipping');
return;
}
console.info('[Mnemosyne] Gravity layout starting —', objs.length, 'crystals,', GRAVITY_ITERATIONS, 'iterations');
for (let iter = 0; iter < GRAVITY_ITERATIONS; iter++) {
// Accumulate displacements before applying (avoids order-of-iteration bias)
const dx = new Float32Array(objs.length);
const dy = new Float32Array(objs.length);
const dz = new Float32Array(objs.length);
objs.forEach((obj, i) => {
const pos = obj.mesh.position;
const cat = obj.region;
// ── Attraction toward same-category centroid ──────────────
let sx = 0, sy = 0, sz = 0, sameCount = 0;
objs.forEach(o => {
if (o === obj || o.region !== cat) return;
sx += o.mesh.position.x;
sy += o.mesh.position.y;
sz += o.mesh.position.z;
sameCount++;
});
if (sameCount > 0) {
dx[i] += ((sx / sameCount) - pos.x) * ATTRACT_FACTOR;
dy[i] += ((sy / sameCount) - pos.y) * ATTRACT_FACTOR;
dz[i] += ((sz / sameCount) - pos.z) * ATTRACT_FACTOR;
}
// ── Repulsion from nearest unrelated crystal ───────────────
let nearestDist = Infinity;
let rnx = 0, rny = 0, rnz = 0;
objs.forEach(o => {
if (o === obj || o.region === cat) return;
const ex = pos.x - o.mesh.position.x;
const ey = pos.y - o.mesh.position.y;
const ez = pos.z - o.mesh.position.z;
const d = Math.sqrt(ex * ex + ey * ey + ez * ez);
if (d < nearestDist) {
nearestDist = d;
rnx = ex; rny = ey; rnz = ez;
}
});
if (nearestDist > 0.001 && nearestDist < Infinity) {
const len = Math.sqrt(rnx * rnx + rny * rny + rnz * rnz);
dx[i] += (rnx / len) * nearestDist * REPEL_FACTOR;
dy[i] += (rny / len) * nearestDist * REPEL_FACTOR;
dz[i] += (rnz / len) * nearestDist * REPEL_FACTOR;
}
});
// Apply displacements
objs.forEach((obj, i) => {
obj.mesh.position.x += dx[i];
obj.mesh.position.y += dy[i];
obj.mesh.position.z += dz[i];
});
}
// Bake final positions to localStorage
saveToStorage();
console.info('[Mnemosyne] Gravity layout complete — positions baked to localStorage');
}
// ─── SPATIAL SEARCH ──────────────────────────────────
function searchNearby(position, maxResults, maxDist) {
maxResults = maxResults || 10;
@@ -612,53 +471,86 @@ const SpatialMemory = (() => {
return results.slice(0, maxResults);
}
// ─── CRYSTAL MESH COLLECTION (for raycasting) ────────
function getCrystalMeshes() {
return Object.values(_memoryObjects).map(o => o.mesh);
}
// ─── MEMORY DATA FROM MESH ───────────────────────────
function getMemoryFromMesh(mesh) {
const entry = Object.values(_memoryObjects).find(o => o.mesh === mesh);
return entry ? { data: entry.data, region: entry.region } : null;
}
// ─── HIGHLIGHT / SELECT ──────────────────────────────
let _selectedId = null;
let _selectedOriginalEmissive = null;
function highlightMemory(memId) {
clearHighlight();
const obj = _memoryObjects[memId];
if (!obj) return;
_selectedId = memId;
_selectedOriginalEmissive = obj.mesh.material.emissiveIntensity;
obj.mesh.material.emissiveIntensity = 4.0;
obj.mesh.userData.selected = true;
}
function clearHighlight() {
if (_selectedId && _memoryObjects[_selectedId]) {
const obj = _memoryObjects[_selectedId];
obj.mesh.material.emissiveIntensity = _selectedOriginalEmissive || (obj.data.strength || 0.7) * 2.5;
obj.mesh.userData.selected = false;
// ─── 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, ')');
}
_selectedId = null;
_selectedOriginalEmissive = null;
return count;
}
function getSelectedId() {
return _selectedId;
// ─── 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 {
init, placeMemory, removeMemory, update,
init, placeMemory, removeMemory, update, importMemories, updateMemory,
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
exportIndex, importIndex, searchNearby, REGIONS,
saveToStorage, loadFromStorage, clearStorage,
runGravityLayout
saveToStorage, loadFromStorage, clearStorage
};
})();

144
server.py
View File

@@ -3,156 +3,17 @@
The Nexus WebSocket Gateway — Robust broadcast bridge for Timmy's consciousness.
This server acts as the central hub for the-nexus, connecting the mind (nexus_think.py),
the body (Evennia/Morrowind), and the visualization surface.
Serves HTTP alongside WebSocket:
GET /api/briefing — heartbeat + morning report data for the HUD briefing panel
"""
import asyncio
import json
import logging
import os
import signal
import sys
from datetime import datetime, timezone
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from threading import Thread
from typing import Any, Dict, Set
from typing import Set
# Branch protected file - see POLICY.md
import websockets
# ── HTTP Briefing Endpoint ─────────────────────────────────────────────
HEARTBEAT_PATH = Path.home() / ".nexus" / "heartbeat.json"
REPORTS_DIR = Path.home() / ".local" / "timmy" / "reports"
CRON_HEARTBEAT_DIR_PRIMARY = Path("/var/run/bezalel/heartbeats")
CRON_HEARTBEAT_DIR_FALLBACK = Path.home() / ".bezalel" / "heartbeats"
def _read_json_file(path: Path) -> Any:
"""Read and parse a JSON file. Returns None on failure."""
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
def _resolve_cron_dir() -> Path:
"""Return the first writable cron heartbeat directory."""
for d in [CRON_HEARTBEAT_DIR_PRIMARY, CRON_HEARTBEAT_DIR_FALLBACK]:
if d.exists() and os.access(str(d), os.R_OK):
return d
return CRON_HEARTBEAT_DIR_FALLBACK
def _read_cron_heartbeats() -> list:
"""Read all .last files from the cron heartbeat directory."""
hb_dir = _resolve_cron_dir()
if not hb_dir.exists():
return []
now = datetime.now(timezone.utc).timestamp()
jobs = []
for f in sorted(hb_dir.glob("*.last")):
data = _read_json_file(f)
if data is None:
jobs.append({"job": f.stem, "healthy": False, "message": "corrupt"})
continue
ts = float(data.get("timestamp", 0))
interval = int(data.get("interval", 3600))
age = now - ts
is_stale = age > (2 * interval)
jobs.append({
"job": f.stem,
"healthy": not is_stale,
"age_secs": round(age, 1),
"interval": interval,
"last_seen": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat() if ts else None,
"message": f"{'STALE' if is_stale else 'OK'} ({age:.0f}s / {interval}s)" if ts else "no timestamp",
})
return jobs
def _latest_morning_report() -> Dict[str, Any] | None:
"""Find the most recent morning report file."""
if not REPORTS_DIR.exists():
return None
reports = sorted(REPORTS_DIR.glob("morning-*.json"), reverse=True)
if not reports:
return None
return _read_json_file(reports[0])
def _build_briefing() -> Dict[str, Any]:
"""Assemble the full briefing payload from real files."""
now = datetime.now(timezone.utc)
# Core heartbeat
core_hb = _read_json_file(HEARTBEAT_PATH)
if core_hb:
beat_ts = float(core_hb.get("timestamp", 0))
core_hb["age_secs"] = round(now.timestamp() - beat_ts, 1) if beat_ts else None
# Cron heartbeats
cron_jobs = _read_cron_heartbeats()
healthy_count = sum(1 for j in cron_jobs if j.get("healthy"))
stale_count = sum(1 for j in cron_jobs if not j.get("healthy"))
# Morning report
report = _latest_morning_report()
return {
"generated_at": now.isoformat(),
"core_heartbeat": core_hb,
"cron_heartbeat": {
"jobs": cron_jobs,
"healthy_count": healthy_count,
"stale_count": stale_count,
},
"morning_report": report,
}
class BriefingHandler(SimpleHTTPRequestHandler):
"""Minimal HTTP handler that only serves /api/briefing."""
def do_GET(self):
if self.path == "/api/briefing":
try:
data = _build_briefing()
body = json.dumps(data).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
except Exception as e:
self.send_error(500, str(e))
elif self.path == "/api/health":
body = json.dumps({"status": "ok"}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(body)
else:
self.send_error(404)
def log_message(self, fmt, *args):
pass # Suppress HTTP access logs — WS gateway logs are enough
def start_http_server(port: int = 8766):
"""Run the HTTP server in a daemon thread."""
server = HTTPServer(("0.0.0.0", port), BriefingHandler)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
logger = logging.getLogger("nexus-gateway")
logger.info(f"Briefing HTTP server started on http://0.0.0.0:{port}")
return server
# Configuration
PORT = 8765
HOST = "0.0.0.0" # Allow external connections if needed
@@ -219,9 +80,6 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
async def main():
"""Main server loop with graceful shutdown."""
# Start HTTP briefing endpoint alongside WS
http_server = start_http_server(port=8766)
logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}")
# Set up signal handlers for graceful shutdown

556
style.css
View File

@@ -1224,491 +1224,123 @@ canvas#nexus-canvas {
.pse-status { color: #4af0c0; font-weight: 600; }
/* ═══ HEARTBEAT BRIEFING PANEL ═════════════════════════════════════ */
.hud-panel-briefing {
width: 320px;
max-height: 420px;
border-left-color: #7b5cff;
}
.hud-panel-briefing .panel-header {
display: flex;
align-items: center;
gap: 6px;
color: #7b5cff;
}
.hud-panel-briefing .panel-content {
max-height: 360px;
overflow-y: auto;
}
/* Pulse dot */
.hb-pulse-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #4af0c0;
box-shadow: 0 0 6px #4af0c0;
animation: hb-dot-pulse 2s ease-in-out infinite;
flex-shrink: 0;
}
.hb-pulse-dot.offline {
background: #ff4466;
box-shadow: 0 0 6px #ff4466;
animation: none;
}
@keyframes hb-dot-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
/* Briefing sections */
.briefing-section {
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(123, 92, 255, 0.12);
}
.briefing-section:last-child { border-bottom: none; margin-bottom: 0; }
.briefing-section-label {
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #8a9ab8;
margin-bottom: 4px;
}
/* Core heartbeat row */
.hb-core-row {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 4px;
}
.hb-core-status {
font-weight: 700;
font-size: 11px;
}
.hb-core-status.alive { color: #4af0c0; }
.hb-core-status.dead { color: #ff4466; }
.hb-core-meta {
font-size: 10px;
color: #8a9ab8;
}
/* Cron jobs */
.hb-cron-row {
display: flex;
gap: 8px;
font-size: 10px;
margin-bottom: 2px;
}
.hb-cron-healthy { color: #4af0c0; }
.hb-cron-stale { color: #ff4466; font-weight: 700; }
.hb-cron-job {
display: flex;
justify-content: space-between;
font-size: 10px;
padding: 1px 0;
}
.hb-cron-job-name { color: #e0f0ff; }
.hb-cron-job-status.healthy { color: #4af0c0; }
.hb-cron-job-status.stale { color: #ff4466; font-weight: 700; }
/* Morning report stats */
.hb-stats-row {
display: flex;
gap: 12px;
font-size: 10px;
}
.hb-stat {
display: flex;
flex-direction: column;
gap: 1px;
}
.hb-stat-value {
font-family: 'Orbitron', sans-serif;
font-weight: 700;
font-size: 14px;
}
.hb-stat-label {
font-size: 9px;
color: #8a9ab8;
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* Blockers */
.hb-blocker {
font-size: 10px;
color: #ff4466;
padding: 1px 0;
}
/* Narrative */
.hb-narrative {
font-size: 10px;
color: #8a9ab8;
line-height: 1.5;
font-style: italic;
}
/* Empty / offline state */
.hb-empty {
font-size: 10px;
color: #8a9ab8;
text-align: center;
padding: 12px 0;
}
/* Timestamp */
.hb-timestamp {
font-size: 9px;
color: rgba(138, 154, 184, 0.6);
margin-top: 4px;
}
/* ═══════════════════════════════════════════
MNEMOSYNE — MEMORY CRYSTAL INSPECTION PANEL
MNEMOSYNE — Memory Activity Feed
═══════════════════════════════════════════ */
.memory-panel {
.memory-feed {
position: fixed;
top: 50%;
right: 24px;
transform: translateY(-50%);
z-index: 120;
animation: memoryPanelIn 0.22s ease-out forwards;
}
.memory-panel-fade-out {
animation: memoryPanelOut 0.18s ease-in forwards !important;
}
@keyframes memoryPanelIn {
from { opacity: 0; transform: translateY(-50%) translateX(16px); }
to { opacity: 1; transform: translateY(-50%) translateX(0); }
}
@keyframes memoryPanelOut {
from { opacity: 1; }
to { opacity: 0; transform: translateY(-50%) translateX(12px); }
}
.memory-panel-content {
width: 340px;
background: rgba(8, 8, 24, 0.92);
backdrop-filter: blur(12px);
bottom: 20px;
left: 20px;
width: 320px;
background: rgba(10, 15, 40, 0.92);
border: 1px solid rgba(74, 240, 192, 0.25);
border-radius: 12px;
padding: 16px;
box-shadow: 0 0 30px rgba(74, 240, 192, 0.08), 0 8px 32px rgba(0, 0, 0, 0.4);
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;
}
.memory-panel-header {
@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: 6px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
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-panel-region-dot {
width: 10px;
height: 10px;
.memory-feed-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.memory-panel-region {
font-family: var(--font-display, monospace);
font-size: 11px;
letter-spacing: 0.15em;
color: var(--color-primary, #4af0c0);
text-transform: uppercase;
flex: 1;
}
.memory-panel-close {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--color-text-muted, #888);
font-size: 14px;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.memory-panel-close:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
.memory-panel-body {
font-size: 14px;
line-height: 1.6;
color: var(--color-text, #ccc);
margin-bottom: 14px;
max-height: 120px;
overflow-y: auto;
word-break: break-word;
}
.memory-panel-meta {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 11px;
}
.memory-meta-row {
display: flex;
gap: 8px;
align-items: baseline;
}
.memory-meta-label {
color: var(--color-text-muted, #666);
text-transform: uppercase;
letter-spacing: 0.08em;
min-width: 50px;
.memory-feed-action {
flex-shrink: 0;
font-size: 12px;
}
.memory-meta-row span:last-child {
color: var(--color-text, #aaa);
word-break: break-all;
}
.memory-conn-tag {
display: inline-block;
background: rgba(74, 240, 192, 0.1);
border: 1px solid rgba(74, 240, 192, 0.2);
border-radius: 4px;
padding: 1px 6px;
font-size: 10px;
font-family: var(--font-mono, monospace);
color: var(--color-primary, #4af0c0);
margin: 1px 2px;
}
.memory-conn-link {
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.memory-conn-link:hover {
background: rgba(74, 240, 192, 0.22);
border-color: rgba(74, 240, 192, 0.5);
color: #fff;
}
/* Entity name — large heading inside panel */
.memory-entity-name {
font-family: var(--font-display, monospace);
font-size: 17px;
font-weight: 700;
color: #fff;
letter-spacing: 0.04em;
margin-bottom: 8px;
text-transform: capitalize;
word-break: break-word;
}
/* Category badge */
.memory-category-badge {
font-family: var(--font-display, monospace);
font-size: 9px;
letter-spacing: 0.12em;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(74, 240, 192, 0.3);
background: rgba(74, 240, 192, 0.12);
color: var(--color-primary, #4af0c0);
flex-shrink: 0;
}
/* Trust score bar */
.memory-trust-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 11px;
}
.memory-trust-bar {
.memory-feed-content {
flex: 1;
height: 5px;
background: rgba(255, 255, 255, 0.08);
border-radius: 3px;
overflow: hidden;
}
.memory-trust-fill {
height: 100%;
border-radius: 3px;
background: var(--color-primary, #4af0c0);
transition: width 0.35s ease;
}
.memory-trust-value {
color: var(--color-text-muted, #888);
min-width: 32px;
text-align: right;
}
/* Pin button */
.memory-panel-pin {
background: none;
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--color-text-muted, #888);
font-size: 11px;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
flex-shrink: 0;
}
.memory-panel-pin:hover {
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
.memory-panel-pin.pinned {
background: rgba(74, 240, 192, 0.15);
border-color: rgba(74, 240, 192, 0.4);
color: var(--color-primary, #4af0c0);
}
/* Related row — allow wrapping */
.memory-meta-row--related {
align-items: flex-start;
}
.memory-meta-row--related span:last-child {
flex-wrap: wrap;
display: flex;
gap: 2px;
}
/* ═══════════════════════════════════════════════════════
PROJECT MNEMOSYNE — SESSION ROOM HUD PANEL (#1171)
═══════════════════════════════════════════════════════ */
.session-room-panel {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 125;
animation: sessionPanelIn 0.25s ease-out forwards;
}
.session-room-panel.session-panel-fade-out {
animation: sessionPanelOut 0.2s ease-in forwards !important;
}
@keyframes sessionPanelIn {
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes sessionPanelOut {
from { opacity: 1; }
to { opacity: 0; transform: translateX(-50%) translateY(10px); }
}
.session-room-panel-content {
min-width: 320px;
max-width: 480px;
background: rgba(8, 4, 28, 0.93);
backdrop-filter: blur(14px);
border: 1px solid rgba(123, 92, 255, 0.35);
border-radius: 12px;
padding: 14px 18px;
box-shadow: 0 0 32px rgba(123, 92, 255, 0.1), 0 8px 32px rgba(0, 0, 0, 0.45);
}
.session-room-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.session-room-icon {
font-size: 14px;
line-height: 1;
}
.session-room-title {
font-family: var(--font-display, monospace);
font-size: 11px;
letter-spacing: 0.18em;
color: #9b7cff;
text-transform: uppercase;
flex: 1;
}
.session-room-close {
background: none;
border: none;
color: rgba(255, 255, 255, 0.35);
cursor: pointer;
font-size: 14px;
padding: 0 2px;
line-height: 1;
transition: color 0.15s;
}
.session-room-close:hover {
color: rgba(255, 255, 255, 0.8);
}
.session-room-timestamp {
font-family: var(--font-display, monospace);
font-size: 13px;
color: #c8b4ff;
margin-bottom: 6px;
letter-spacing: 0.08em;
}
.session-room-fact-count {
font-size: 11px;
color: rgba(200, 180, 255, 0.55);
margin-bottom: 10px;
}
.session-room-facts {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 140px;
overflow-y: auto;
}
.session-room-fact-item {
font-size: 11px;
color: rgba(220, 210, 255, 0.75);
padding: 4px 8px;
background: rgba(123, 92, 255, 0.07);
border-left: 2px solid rgba(123, 92, 255, 0.4);
border-radius: 0 4px 4px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-room-hint {
margin-top: 10px;
.memory-feed-time {
flex-shrink: 0;
color: rgba(255, 255, 255, 0.3);
font-size: 10px;
color: rgba(200, 180, 255, 0.35);
text-align: center;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.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; }