Compare commits

..

15 Commits

Author SHA1 Message Date
Alexander Whitestone
339f7d6ef2 fix: [PORTAL] Add honest local Bannerlord readiness/status to the Nexus (closes #724)
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 12s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-10 20:17:11 -04:00
cc4af009c7 [claude] Mnemosyne session rooms — holographic chambers per session (#1171) (#1178)
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Verification Gate / verify-staging (push) Failing after 3s
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-10 22:45:10 +00:00
089b06b6f8 [claude] Mnemosyne category regions — spatial zones for fact types (#1168) (#1179)
Some checks failed
Deploy Nexus / deploy (push) Failing after 2s
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-10 22:45:04 +00:00
8beae5ecc1 [claude] Mnemosyne holographic fact detail panel (#1172) (#1177)
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Verification Gate / verify-staging (push) Failing after 6s
2026-04-10 22:29:17 +00:00
e2edfd3318 [claude] Mnemosyne gravity well clustering — related memories attract in 3D (#1175) (#1176)
Some checks failed
Deploy Nexus / deploy (push) Failing after 4s
Staging Verification Gate / verify-staging (push) Failing after 4s
2026-04-10 22:18:46 +00:00
8e18fa5311 Merge pull request 'fix: Missing Source Code Investigation — Classical AI Commits Disappearing' (#1163) from mimo/code/issue-1145 into main
Some checks failed
Deploy Nexus / deploy (push) Failing after 2s
Staging Verification Gate / verify-staging (push) Failing after 3s
Auto-merged by Timmy
2026-04-10 21:00:40 +00:00
1bf2af15a0 Merge pull request 'fix: [DEFERRED] Hermes Trismegistus — New Wizard Proposal' (#1162) from mimo/code/issue-1146 into main
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
Auto-merged by Timmy
2026-04-10 21:00:37 +00:00
4095946749 Merge pull request '[Mnemosyne] Memory crystal click-to-inspect interaction' (#1161) from feat/mnemosyne-crystal-inspect into main
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Verification Gate / verify-staging (push) Failing after 3s
Auto-merged by Timmy
2026-04-10 21:00:24 +00:00
Alexander Whitestone
845e2f2ced fix: Missing Source Code Investigation — Classical AI Commits Disappearing (closes #1145)
Some checks failed
CI / test (pull_request) Failing after 8s
CI / validate (pull_request) Failing after 12s
Review Approval Gate / verify-review (pull_request) Failing after 3s
Root cause: duplicate agents wrote GOFAI code to public/nexus/app.js (wrong path)
instead of root app.js. The public/nexus/ files were corrupt duplicates that got
overwritten and eventually deleted, creating the illusion of disappearing code.

The classical AI code is fully present in root app.js — all 13 classes verified:
SymbolicEngine, AgentFSM, KnowledgeGraph, Blackboard, SymbolicPlanner,
HTNPlanner, CaseBasedReasoner, NeuroSymbolicBridge, MetaReasoningLayer,
AdaptiveCalibrator, PSELayer, plus A* search and bitmask fact indexing.

Prevention:
- Added public/nexus/ to .gitignore
- Added canonical file path documentation to CLAUDE.md
- Filed investigation report (INVESTIGATION_ISSUE_1145.md)
2026-04-10 16:25:57 -04:00
Mimo Swarm (mimo-code-1146)
60af11ec2f fix: [DEFERRED] Hermes Trismegistus — New Wizard Proposal (closes #1146)
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 3s
- Added fleet/hermes-trismegistus/README.md with full proposal
- Added fleet/hermes-trismegistus/lane.md with routing definition
- Filled in acceptance criteria from issue #1146
- Status remains DEFERRED — unblock conditions documented

Automated by mimo-v2-pro swarm.
2026-04-10 16:14:13 -04:00
c387708892 feat(mnemosyne): add memory crystal inspection panel 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 2s
2026-04-10 19:38:57 +00:00
8694c0f5ad feat(mnemosyne): add memory crystal inspection panel HTML overlay 2026-04-10 19:38:55 +00:00
c3547196d8 feat(mnemosyne): memory crystal click-to-inspect — raycast crystals, show panel, dismiss on empty click 2026-04-10 19:38:54 +00:00
87bfe9b332 feat(mnemosyne): add crystal mesh query + highlight/select API for click-to-inspect 2026-04-10 19:38:53 +00:00
a0964a2fbf auto-merge PR #1159
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Verification Gate / verify-staging (push) Failing after 2s
2026-04-10 19:03:41 +00:00
11 changed files with 1735 additions and 20 deletions

3
.gitignore vendored
View File

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

View File

@@ -42,6 +42,17 @@ 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`
@@ -50,6 +61,7 @@ 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

@@ -0,0 +1,72 @@
# 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)

388
app.js
View File

@@ -4,6 +4,7 @@ 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
@@ -705,6 +706,7 @@ async function init() {
createWorkshopTerminal();
createAshStorm();
SpatialMemory.init(scene);
SessionRooms.init(scene, camera, null);
updateLoad(90);
loadSession();
@@ -1883,7 +1885,7 @@ function setupControls() {
orbitState.lastX = e.clientX;
orbitState.lastY = e.clientY;
// Raycasting for portals
// Raycasting for portals and memory crystals
if (!portalOverlayActive) {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
@@ -1891,12 +1893,43 @@ function setupControls() {
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
if (intersects.length > 0) {
const clickedRing = intersects[0].object;
// Priority 1: Portals
const portalHits = raycaster.intersectObjects(portals.map(p => p.ring));
if (portalHits.length > 0) {
const clickedRing = portalHits[0].object;
const portal = portals.find(p => p.ring === clickedRing);
if (portal) activatePortal(portal);
if (portal) { activatePortal(portal); return; }
}
// 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();
}
}
});
@@ -2396,6 +2429,15 @@ function activatePortal(portal) {
overlay.style.display = 'flex';
// Readiness detail for game-world portals
const readinessEl = document.getElementById('portal-readiness-detail');
if (portal.config.portal_type === 'game-world' && portal.config.readiness_steps) {
renderReadinessDetail(readinessEl, portal.config);
readinessEl.style.display = 'block';
} else {
readinessEl.style.display = 'none';
}
if (portal.config.destination && portal.config.destination.url) {
redirectBox.style.display = 'block';
errorBox.style.display = 'none';
@@ -2417,6 +2459,37 @@ function activatePortal(portal) {
}
}
// ═══ READINESS RENDERING ═══
function renderReadinessDetail(container, config) {
const steps = config.readiness_steps || {};
const stepKeys = ['downloaded', 'runtime_ready', 'launched', 'harness_bridged'];
let html = '<div class="portal-readiness-title">READINESS PIPELINE</div>';
let firstUndone = true;
stepKeys.forEach(key => {
const step = steps[key];
if (!step) return;
const cls = step.done ? 'done' : (firstUndone ? 'current' : '');
if (!step.done) firstUndone = false;
html += `<div class="portal-readiness-step ${cls}">
<span class="step-dot"></span>
<span>${step.label || key}</span>
</div>`;
});
if (config.blocked_reason) {
html += `<div class="portal-readiness-blocked">&#x26A0; ${config.blocked_reason}</div>`;
}
const doneCount = stepKeys.filter(k => steps[k]?.done).length;
const canEnter = doneCount === stepKeys.length && config.destination?.url;
if (!canEnter) {
html += `<div class="portal-readiness-hint">Cannot enter yet — ${stepKeys.length - doneCount} step${stepKeys.length - doneCount > 1 ? 's' : ''} remaining.</div>`;
}
container.innerHTML = html;
}
function closePortalOverlay() {
portalOverlayActive = false;
document.getElementById('portal-overlay').style.display = 'none';
@@ -2497,12 +2570,42 @@ function populateAtlas() {
const statusClass = `status-${config.status || 'online'}`;
// Build readiness section for game-world portals
let readinessHtml = '';
if (config.portal_type === 'game-world' && config.readiness_steps) {
const stepKeys = ['downloaded', 'runtime_ready', 'launched', 'harness_bridged'];
const steps = config.readiness_steps;
const doneCount = stepKeys.filter(k => steps[k]?.done).length;
const pct = Math.round((doneCount / stepKeys.length) * 100);
const barColor = config.color || '#ffd700';
readinessHtml = `<div class="atlas-card-readiness">
<div class="readiness-bar-track">
<div class="readiness-bar-fill" style="width:${pct}%;background:${barColor};"></div>
</div>
<div class="readiness-steps-mini">`;
let firstUndone = true;
stepKeys.forEach(key => {
const step = steps[key];
if (!step) return;
const cls = step.done ? 'done' : (firstUndone ? 'current' : '');
if (!step.done) firstUndone = false;
readinessHtml += `<span class="readiness-step ${cls}">${step.label || key}</span>`;
});
readinessHtml += '</div>';
if (config.blocked_reason) {
readinessHtml += `<div class="atlas-card-blocked">&#x26A0; ${config.blocked_reason}</div>`;
}
readinessHtml += '</div>';
}
card.innerHTML = `
<div class="atlas-card-header">
<div class="atlas-card-name">${config.name}</div>
<div class="atlas-card-status ${statusClass}">${config.status || 'ONLINE'}</div>
<div class="atlas-card-status ${statusClass}">${config.readiness_state || config.status || 'ONLINE'}</div>
</div>
<div class="atlas-card-desc">${config.description}</div>
${readinessHtml}
<div class="atlas-card-footer">
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
@@ -2520,11 +2623,14 @@ function populateAtlas() {
document.getElementById('atlas-online-count').textContent = onlineCount;
document.getElementById('atlas-standby-count').textContent = standbyCount;
// Update Bannerlord HUD status
// Update Bannerlord HUD status with honest readiness state
const bannerlord = portals.find(p => p.config.id === 'bannerlord');
if (bannerlord) {
const statusEl = document.getElementById('bannerlord-status');
statusEl.className = 'hud-status-item ' + (bannerlord.config.status || 'offline');
const state = bannerlord.config.readiness_state || bannerlord.config.status || 'offline';
statusEl.className = 'hud-status-item ' + state;
const labelEl = statusEl.querySelector('.status-label');
if (labelEl) labelEl.textContent = state.toUpperCase().replace(/_/g, ' ');
}
}
@@ -2551,6 +2657,226 @@ 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);
@@ -2581,6 +2907,9 @@ 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');
@@ -3104,9 +3433,52 @@ 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

@@ -0,0 +1,72 @@
# 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

@@ -0,0 +1,43 @@
# 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

@@ -196,6 +196,7 @@
</div>
<h2 id="portal-name-display">MORROWIND</h2>
<p id="portal-desc-display">The Vvardenfell harness. Ash storms and ancient mysteries.</p>
<div id="portal-readiness-detail" class="portal-readiness-detail" style="display:none;"></div>
<div class="portal-redirect-box" id="portal-redirect-box">
<div class="portal-redirect-label">REDIRECTING IN</div>
<div class="portal-redirect-timer" id="portal-timer">5</div>
@@ -207,6 +208,50 @@
</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">

View File

@@ -0,0 +1,413 @@
// ═══════════════════════════════════════════════════════
// 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,12 +8,20 @@
// holographic archive.
//
// World layout (hex cylinder, radius 25):
// North (z-) → Documents & Knowledge
// South (z+) → Projects & Tasks
// East (x+) → Code & Engineering
// West (x-) → Conversations & Social
// Center → Active Working Memory
// Below (y-) → Archive (cold storage)
//
// 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]
//
// Usage from app.js:
// SpatialMemory.init(scene);
@@ -73,6 +81,44 @@ 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
}
};
@@ -99,6 +145,7 @@ 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({
@@ -126,6 +173,22 @@ 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;
@@ -139,11 +202,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, 3, cz);
sprite.position.set(cx, labelY, cz);
sprite.scale.set(4, 1, 1);
_scene.add(sprite);
return { ring, disc, sprite };
return { ring, disc, glowDisc, sprite };
}
// ─── PLACE A MEMORY ──────────────────────────────────
@@ -283,6 +346,9 @@ 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;
}
});
}
@@ -456,6 +522,81 @@ 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;
@@ -471,11 +612,53 @@ 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;
}
_selectedId = null;
_selectedOriginalEmissive = null;
}
function getSelectedId() {
return _selectedId;
}
return {
init, placeMemory, removeMemory, update,
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
exportIndex, importIndex, searchNearby, REGIONS,
saveToStorage, loadFromStorage, clearStorage
saveToStorage, loadFromStorage, clearStorage,
runGravityLayout
};
})();

View File

@@ -17,7 +17,7 @@
"id": "bannerlord",
"name": "Bannerlord",
"description": "Calradia battle harness. Massive armies, tactical command.",
"status": "active",
"status": "downloaded",
"color": "#ffd700",
"position": { "x": -15, "y": 0, "z": -10 },
"rotation": { "y": 0.5 },
@@ -25,13 +25,20 @@
"world_category": "strategy-rpg",
"environment": "production",
"access_mode": "operator",
"readiness_state": "active",
"readiness_state": "downloaded",
"readiness_steps": {
"downloaded": { "label": "Downloaded", "done": true },
"runtime_ready": { "label": "Runtime Ready", "done": false },
"launched": { "label": "Launched", "done": false },
"harness_bridged": { "label": "Harness Bridged", "done": false }
},
"blocked_reason": null,
"telemetry_source": "hermes-harness:bannerlord",
"owner": "Timmy",
"app_id": 261550,
"window_title": "Mount & Blade II: Bannerlord",
"destination": {
"url": "https://bannerlord.timmy.foundation",
"url": null,
"type": "harness",
"action_label": "Enter Calradia",
"params": { "world": "calradia" }

493
style.css
View File

@@ -367,6 +367,142 @@ canvas#nexus-canvas {
.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
.status-active { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
.status-blocked { background: rgba(255, 68, 102, 0.3); color: #ff4466; border: 1px solid #ff4466; }
.status-downloaded { background: rgba(100, 149, 237, 0.2); color: #6495ed; border: 1px solid #6495ed; }
.status-runtime_ready { background: rgba(255, 165, 0, 0.2); color: #ffa500; border: 1px solid #ffa500; }
.status-launched { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
.status-harness_bridged { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
/* Readiness Progress Bar (atlas card) */
.atlas-card-readiness {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.readiness-bar-track {
width: 100%;
height: 4px;
background: rgba(255,255,255,0.08);
border-radius: 2px;
overflow: hidden;
margin-bottom: 6px;
}
.readiness-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.4s ease;
}
.readiness-steps-mini {
display: flex;
gap: 6px;
font-size: 9px;
font-family: var(--font-body);
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.readiness-step {
padding: 1px 5px;
border-radius: 2px;
background: rgba(255,255,255,0.04);
}
.readiness-step.done {
background: rgba(74, 240, 192, 0.15);
color: var(--color-primary);
}
.readiness-step.current {
background: rgba(255, 215, 0, 0.15);
color: var(--color-gold);
}
.atlas-card-blocked {
margin-top: 6px;
font-size: 10px;
color: #ff4466;
font-family: var(--font-body);
}
/* Readiness Detail (portal overlay) */
.portal-readiness-detail {
margin-top: 16px;
padding: 12px 16px;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 4px;
}
.portal-readiness-title {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.15em;
color: var(--color-text-muted);
margin-bottom: 10px;
text-transform: uppercase;
}
.portal-readiness-step {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-family: var(--font-body);
font-size: 11px;
color: rgba(255,255,255,0.4);
}
.portal-readiness-step .step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.15);
flex-shrink: 0;
}
.portal-readiness-step.done .step-dot {
background: var(--color-primary);
box-shadow: 0 0 6px var(--color-primary);
}
.portal-readiness-step.done {
color: var(--color-primary);
}
.portal-readiness-step.current .step-dot {
background: var(--color-gold);
box-shadow: 0 0 6px var(--color-gold);
animation: pulse-dot 1.5s ease-in-out infinite;
}
.portal-readiness-step.current {
color: #fff;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.portal-readiness-blocked {
margin-top: 8px;
padding: 6px 10px;
background: rgba(255, 68, 102, 0.1);
border: 1px solid rgba(255, 68, 102, 0.3);
border-radius: 3px;
font-size: 11px;
color: #ff4466;
font-family: var(--font-body);
}
.portal-readiness-hint {
margin-top: 8px;
font-size: 10px;
color: var(--color-text-muted);
font-family: var(--font-body);
font-style: italic;
}
/* HUD Status for readiness states */
.hud-status-item.downloaded .status-dot { background: #6495ed; box-shadow: 0 0 5px #6495ed; }
.hud-status-item.runtime_ready .status-dot { background: #ffa500; box-shadow: 0 0 5px #ffa500; }
.hud-status-item.launched .status-dot { background: var(--color-gold); box-shadow: 0 0 5px var(--color-gold); }
.hud-status-item.harness_bridged .status-dot { background: var(--color-primary); box-shadow: 0 0 5px var(--color-primary); }
.hud-status-item.blocked .status-dot { background: #ff4466; box-shadow: 0 0 5px #ff4466; }
.hud-status-item.downloaded .status-label,
.hud-status-item.runtime_ready .status-label,
.hud-status-item.launched .status-label,
.hud-status-item.harness_bridged .status-label,
.hud-status-item.blocked .status-label {
color: #fff;
}
.atlas-card-desc {
font-size: 12px;
@@ -1223,3 +1359,360 @@ canvas#nexus-canvas {
.l402-msg { color: #fff; }
.pse-status { color: #4af0c0; font-weight: 600; }
/* ═══════════════════════════════════════════
MNEMOSYNE — MEMORY CRYSTAL INSPECTION PANEL
═══════════════════════════════════════════ */
.memory-panel {
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);
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);
}
.memory-panel-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.memory-panel-region-dot {
width: 10px;
height: 10px;
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;
flex-shrink: 0;
}
.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 {
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;
}
.session-room-hint {
margin-top: 10px;
font-size: 10px;
color: rgba(200, 180, 255, 0.35);
text-align: center;
letter-spacing: 0.1em;
text-transform: uppercase;
}