Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
9771472983 WIP: issue #728 (mimo swarm)
Some checks failed
CI / test (pull_request) Failing after 10s
CI / validate (pull_request) Failing after 15s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-12 11:54:27 -04:00
8 changed files with 380 additions and 145 deletions

131
app.js
View File

@@ -58,6 +58,11 @@ let performanceTier = 'high';
let hermesWs = null;
let wsReconnectTimer = null;
let wsConnected = false;
// ═══ EVENNIA ROOM STATE ═══
let evenniaRoom = null; // {title, desc, exits[], objects[], occupants[], timestamp, roomKey}
let evenniaConnected = false;
let evenniaStaleTimer = null;
const EVENNIA_STALE_MS = 60000; // mark stale after 60s without update
let recentToolOutputs = [];
let workshopPanelCtx = null;
let workshopPanelTexture = null;
@@ -755,6 +760,8 @@ async function init() {
enterPrompt.addEventListener('click', () => {
enterPrompt.classList.add('fade-out');
document.getElementById('hud').style.display = 'block';
const erpPanel = document.getElementById('evennia-room-panel');
if (erpPanel) erpPanel.style.display = 'block';
setTimeout(() => { enterPrompt.remove(); }, 600);
}, { once: true });
@@ -2169,10 +2176,134 @@ function handleHermesMessage(data) {
else addChatMessage(msg.agent, msg.text, false);
});
}
} else if (data.type && data.type.startsWith('evennia.')) {
handleEvenniaEvent(data);
}
// ═══════════════════════════════════════════
// ═══════════════════════════════════════════
// EVENNIA ROOM SNAPSHOT PANEL (Issue #728)
// ═══════════════════════════════════════════
function handleEvenniaEvent(data) {
const evtType = data.type;
if (evtType === 'evennia.room_snapshot') {
evenniaRoom = {
roomKey: data.room_key || data.room_id || '',
title: data.title || 'Unknown Room',
desc: data.desc || '',
exits: data.exits || [],
objects: data.objects || [],
occupants: data.occupants || [],
timestamp: data.timestamp || new Date().toISOString()
};
evenniaConnected = true;
renderEvenniaRoomPanel();
resetEvenniaStaleTimer();
} else if (evtType === 'evennia.player_move') {
// Movement may indicate current room changed; update location text
if (data.to_room) {
const locEl = document.getElementById('hud-location-text');
if (locEl) locEl.textContent = data.to_room;
}
} else if (evtType === 'evennia.session_bound') {
evenniaConnected = true;
renderEvenniaRoomPanel();
} else if (evtType === 'evennia.player_join' || evtType === 'evennia.player_leave') {
// Refresh occupant display if we have room data
if (evenniaRoom) renderEvenniaRoomPanel();
}
}
function resetEvenniaStaleTimer() {
if (evenniaStaleTimer) clearTimeout(evenniaStaleTimer);
const dot = document.getElementById('erp-live-dot');
const status = document.getElementById('erp-status');
if (dot) dot.className = 'erp-live-dot connected';
if (status) { status.textContent = 'LIVE'; status.className = 'erp-status online'; }
evenniaStaleTimer = setTimeout(() => {
if (dot) dot.className = 'erp-live-dot stale';
if (status) { status.textContent = 'STALE'; status.className = 'erp-status stale'; }
}, EVENNIA_STALE_MS);
}
function renderEvenniaRoomPanel() {
const panel = document.getElementById('evennia-room-panel');
if (!panel) return;
panel.style.display = 'block';
const emptyEl = document.getElementById('erp-empty');
const roomEl = document.getElementById('erp-room');
if (!evenniaRoom) {
if (emptyEl) emptyEl.style.display = 'flex';
if (roomEl) roomEl.style.display = 'none';
return;
}
if (emptyEl) emptyEl.style.display = 'none';
if (roomEl) roomEl.style.display = 'block';
const titleEl = document.getElementById('erp-room-title');
const descEl = document.getElementById('erp-room-desc');
if (titleEl) titleEl.textContent = evenniaRoom.title;
if (descEl) descEl.textContent = evenniaRoom.desc;
renderEvenniaList('erp-exits', evenniaRoom.exits, (item) => {
const name = item.key || item.destination_id || item.name || '?';
const dest = item.destination_key || item.destination_id || '';
return { icon: '→', label: name, extra: dest && dest !== name ? dest : '' };
});
renderEvenniaList('erp-objects', evenniaRoom.objects, (item) => {
const name = item.short_desc || item.key || item.id || item.name || '?';
return { icon: '◇', label: name };
});
renderEvenniaList('erp-occupants', evenniaRoom.occupants, (item) => {
const name = item.character || item.name || item.account || '?';
return { icon: '◉', label: name };
});
const tsEl = document.getElementById('erp-footer-ts');
const roomKeyEl = document.getElementById('erp-footer-room');
if (tsEl) {
try {
const d = new Date(evenniaRoom.timestamp);
tsEl.textContent = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
} catch(e) { tsEl.textContent = '—'; }
}
if (roomKeyEl) roomKeyEl.textContent = evenniaRoom.roomKey;
}
function renderEvenniaList(containerId, items, mapFn) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '';
if (!items || items.length === 0) {
const empty = document.createElement('div');
empty.className = 'erp-section-empty';
empty.textContent = 'none';
container.appendChild(empty);
return;
}
items.forEach(item => {
const mapped = mapFn(item);
const row = document.createElement('div');
row.className = 'erp-item';
row.innerHTML = `<span class="erp-item-icon">${mapped.icon}</span><span>${mapped.label}</span>`;
if (mapped.extra) {
row.innerHTML += `<span class="erp-item-dest">${mapped.extra}</span>`;
}
container.appendChild(row);
});
}
// MNEMOSYNE — LIVE MEMORY BRIDGE
// ═══════════════════════════════════════════

View File

@@ -102,6 +102,44 @@
</div>
</div>
<!-- Evennia Room Snapshot Panel -->
<div id="evennia-room-panel" class="evennia-room-panel" style="display:none;">
<div class="erp-header">
<div class="erp-header-left">
<div class="erp-live-dot" id="erp-live-dot"></div>
<span class="erp-title">EVENNIA — ROOM SNAPSHOT</span>
</div>
<span class="erp-status" id="erp-status">OFFLINE</span>
</div>
<div class="erp-body" id="erp-body">
<div class="erp-empty" id="erp-empty">
<span class="erp-empty-icon"></span>
<span class="erp-empty-text">No Evennia connection</span>
<span class="erp-empty-sub">Waiting for room data...</span>
</div>
<div class="erp-room" id="erp-room" style="display:none;">
<div class="erp-room-title" id="erp-room-title"></div>
<div class="erp-room-desc" id="erp-room-desc"></div>
<div class="erp-section">
<div class="erp-section-header">EXITS</div>
<div class="erp-exits" id="erp-exits"></div>
</div>
<div class="erp-section">
<div class="erp-section-header">OBJECTS</div>
<div class="erp-objects" id="erp-objects"></div>
</div>
<div class="erp-section">
<div class="erp-section-header">OCCUPANTS</div>
<div class="erp-occupants" id="erp-occupants"></div>
</div>
</div>
</div>
<div class="erp-footer">
<span class="erp-footer-ts" id="erp-footer-ts"></span>
<span class="erp-footer-room" id="erp-footer-room"></span>
</div>
</div>
<!-- Top Left: Debug -->
<div id="debug-overlay" class="hud-debug"></div>

View File

@@ -1,18 +1,13 @@
class MemoryOptimizer {
constructor(options = {}) {
this.threshold = options.threshold || 0.3;
this.decayRate = options.decayRate || 0.01;
this.lastRun = Date.now();
this.threshold = options.threshold || 0.8;
this.decayRate = options.decayRate || 0.05;
}
optimize(memories) {
const now = Date.now();
const elapsed = (now - this.lastRun) / 1000;
this.lastRun = now;
return memories.map(m => {
const decay = (m.importance || 1) * this.decayRate * elapsed;
return { ...m, strength: Math.max(0, (m.strength || 1) - decay) };
}).filter(m => m.strength > this.threshold || m.locked);
optimize(memory) {
console.log('Optimizing memory...');
// Heuristic-based pruning
return memory.filter(m => m.strength > this.threshold);
}
}
export default MemoryOptimizer;

View File

@@ -173,9 +173,7 @@ const SpatialMemory = (() => {
let _entityLines = []; // entity resolution lines (issue #1167)
let _camera = null; // set by setCamera() for LOD culling
const ENTITY_LOD_DIST = 50; // hide entity lines when camera > this from midpoint
const CONNECTION_LOD_DIST = 60; // hide connection lines when camera > this from midpoint
let _initialized = false;
let _constellationVisible = true; // toggle for constellation view
// ─── CRYSTAL GEOMETRY (persistent memories) ───────────
function createCrystalGeometry(size) {
@@ -320,43 +318,10 @@ const SpatialMemory = (() => {
if (!obj || !obj.data.connections) return;
obj.data.connections.forEach(targetId => {
const target = _memoryObjects[targetId];
if (target) _drawSingleConnection(obj, target);
if (target) _createConnectionLine(obj, target);
});
}
function _drawSingleConnection(src, tgt) {
const srcId = src.data.id;
const tgtId = tgt.data.id;
// Deduplicate — only draw from lower ID to higher
if (srcId > tgtId) return;
// Skip if already exists
const exists = _connectionLines.some(l =>
(l.userData.from === srcId && l.userData.to === tgtId) ||
(l.userData.from === tgtId && l.userData.to === srcId)
);
if (exists) return;
const points = [src.mesh.position.clone(), tgt.mesh.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
const srcStrength = src.mesh.userData.strength || 0.7;
const tgtStrength = tgt.mesh.userData.strength || 0.7;
const blendedStrength = (srcStrength + tgtStrength) / 2;
const lineOpacity = 0.15 + blendedStrength * 0.55;
const srcColor = new THREE.Color(REGIONS[src.region]?.color || 0x334455);
const tgtColor = new THREE.Color(REGIONS[tgt.region]?.color || 0x334455);
const lineColor = new THREE.Color().lerpColors(srcColor, tgtColor, 0.5);
const mat = new THREE.LineBasicMaterial({
color: lineColor,
transparent: true,
opacity: lineOpacity
});
const line = new THREE.Line(geo, mat);
line.userData = { type: 'connection', from: srcId, to: tgtId, baseOpacity: lineOpacity };
line.visible = _constellationVisible;
_scene.add(line);
_connectionLines.push(line);
}
return { ring, disc, glowDisc, sprite };
}
@@ -434,7 +399,7 @@ const SpatialMemory = (() => {
return [cx + Math.cos(angle) * dist, cy + height, cz + Math.sin(angle) * dist];
}
// ─── CONNECTIONS (constellation-aware) ───────────────
// ─── CONNECTIONS ─────────────────────────────────────
function _drawConnections(memId, connections) {
const src = _memoryObjects[memId];
if (!src) return;
@@ -445,23 +410,9 @@ const SpatialMemory = (() => {
const points = [src.mesh.position.clone(), tgt.mesh.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
// Strength-encoded opacity: blend source/target strengths, min 0.15, max 0.7
const srcStrength = src.mesh.userData.strength || 0.7;
const tgtStrength = tgt.mesh.userData.strength || 0.7;
const blendedStrength = (srcStrength + tgtStrength) / 2;
const lineOpacity = 0.15 + blendedStrength * 0.55;
// Blend source/target region colors for the line
const srcColor = new THREE.Color(REGIONS[src.region]?.color || 0x334455);
const tgtColor = new THREE.Color(REGIONS[tgt.region]?.color || 0x334455);
const lineColor = new THREE.Color().lerpColors(srcColor, tgtColor, 0.5);
const mat = new THREE.LineBasicMaterial({
color: lineColor,
transparent: true,
opacity: lineOpacity
});
const mat = new THREE.LineBasicMaterial({ color: 0x334455, transparent: true, opacity: 0.2 });
const line = new THREE.Line(geo, mat);
line.userData = { type: 'connection', from: memId, to: targetId, baseOpacity: lineOpacity };
line.visible = _constellationVisible;
line.userData = { type: 'connection', from: memId, to: targetId };
_scene.add(line);
_connectionLines.push(line);
});
@@ -538,43 +489,6 @@ const SpatialMemory = (() => {
});
}
function _updateConnectionLines() {
if (!_constellationVisible) return;
if (!_camera) return;
const camPos = _camera.position;
_connectionLines.forEach(line => {
const posArr = line.geometry.attributes.position.array;
const mx = (posArr[0] + posArr[3]) / 2;
const my = (posArr[1] + posArr[4]) / 2;
const mz = (posArr[2] + posArr[5]) / 2;
const dist = camPos.distanceTo(new THREE.Vector3(mx, my, mz));
if (dist > CONNECTION_LOD_DIST) {
line.visible = false;
} else {
line.visible = true;
const fade = Math.max(0, 1 - (dist / CONNECTION_LOD_DIST));
// Restore base opacity from userData if stored, else use material default
const base = line.userData.baseOpacity || line.material.opacity || 0.4;
line.material.opacity = base * fade;
}
});
}
function toggleConstellation() {
_constellationVisible = !_constellationVisible;
_connectionLines.forEach(line => {
line.visible = _constellationVisible;
});
console.info('[Mnemosyne] Constellation', _constellationVisible ? 'shown' : 'hidden');
return _constellationVisible;
}
function isConstellationVisible() {
return _constellationVisible;
}
// ─── REMOVE A MEMORY ─────────────────────────────────
function removeMemory(memId) {
const obj = _memoryObjects[memId];
@@ -630,7 +544,6 @@ const SpatialMemory = (() => {
});
_updateEntityLines();
_updateConnectionLines();
Object.values(_regionMarkers).forEach(marker => {
if (marker.ring && marker.ring.material) {
@@ -953,7 +866,7 @@ const SpatialMemory = (() => {
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
exportIndex, importIndex, searchNearby, REGIONS,
saveToStorage, loadFromStorage, clearStorage,
runGravityLayout, setCamera, toggleConstellation, isConstellationVisible
runGravityLayout, setCamera
};
})();

View File

@@ -1,14 +0,0 @@
class Reasoner:
def __init__(self, rules):
self.rules = rules
def evaluate(self, entries):
return [r['action'] for r in self.rules if self._check(r['condition'], entries)]
def _check(self, cond, entries):
if cond.startswith('count'):
# e.g. count(type=anomaly)>3
p = cond.replace('count(', '').split(')')
key, val = p[0].split('=')
count = sum(1 for e in entries if e.get(key) == val)
return eval(f"{count}{p[1]}")
return False

View File

@@ -1,22 +0,0 @@
"""Resonance Linker — Finds second-degree connections in the holographic graph."""
class ResonanceLinker:
def __init__(self, archive):
self.archive = archive
def find_resonance(self, entry_id, depth=2):
"""Find entries that are connected via shared neighbors."""
if entry_id not in self.archive._entries: return []
entry = self.archive._entries[entry_id]
neighbors = set(entry.links)
resonance = {}
for neighbor_id in neighbors:
if neighbor_id in self.archive._entries:
for second_neighbor in self.archive._entries[neighbor_id].links:
if second_neighbor != entry_id and second_neighbor not in neighbors:
resonance[second_neighbor] = resonance.get(second_neighbor, 0) + 1
return sorted(resonance.items(), key=lambda x: x[1], reverse=True)

View File

@@ -1,6 +0,0 @@
[
{
"condition": "count(type=anomaly)>3",
"action": "alert"
}
]

200
style.css
View File

@@ -2077,3 +2077,203 @@ canvas#nexus-canvas {
font-style: italic;
padding: 4px 0;
}
/* ═══ EVENNIA ROOM SNAPSHOT PANEL (Issue #728) ═══ */
.evennia-room-panel {
position: fixed;
right: 20px;
top: 80px;
width: 300px;
background: rgba(5, 5, 16, 0.85);
border: 1px solid rgba(74, 240, 192, 0.2);
border-right: 3px solid #4af0c0;
border-radius: var(--panel-radius);
backdrop-filter: blur(var(--panel-blur));
font-family: var(--font-body);
font-size: 11px;
color: var(--color-text);
z-index: 100;
overflow: hidden;
}
.erp-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid rgba(74, 240, 192, 0.12);
background: rgba(74, 240, 192, 0.03);
}
.erp-header-left {
display: flex;
align-items: center;
gap: 8px;
}
.erp-live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--color-text-muted);
transition: background 0.3s ease;
}
.erp-live-dot.connected {
background: var(--color-primary);
animation: blink 1.4s ease-in-out infinite;
}
.erp-live-dot.stale {
background: var(--color-warning);
animation: blink 2s ease-in-out infinite;
}
.erp-title {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.12em;
color: var(--color-primary);
}
.erp-status {
font-size: 9px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-muted);
padding: 2px 6px;
border-radius: 3px;
background: rgba(138, 154, 184, 0.1);
}
.erp-status.online {
color: var(--color-primary);
background: rgba(74, 240, 192, 0.1);
}
.erp-status.stale {
color: var(--color-warning);
background: rgba(255, 170, 34, 0.1);
}
.erp-body {
padding: 8px 12px;
max-height: 360px;
overflow-y: auto;
}
/* Empty/offline state */
.erp-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 20px 0;
text-align: center;
}
.erp-empty-icon {
font-size: 20px;
opacity: 0.4;
}
.erp-empty-text {
font-size: 11px;
color: var(--color-text-muted);
}
.erp-empty-sub {
font-size: 10px;
color: rgba(138, 154, 184, 0.5);
font-style: italic;
}
/* Room content */
.erp-room-title {
font-family: var(--font-display);
font-size: 13px;
font-weight: 600;
color: var(--color-primary);
margin-bottom: 6px;
letter-spacing: 0.04em;
}
.erp-room-desc {
font-size: 11px;
color: var(--color-text);
line-height: 1.5;
margin-bottom: 10px;
opacity: 0.85;
}
.erp-section {
margin-bottom: 8px;
}
.erp-section-header {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--color-secondary);
margin-bottom: 4px;
padding-bottom: 2px;
border-bottom: 1px solid rgba(123, 92, 255, 0.15);
}
.erp-item {
font-size: 11px;
color: var(--color-text);
padding: 2px 0;
display: flex;
align-items: center;
gap: 6px;
}
.erp-item-icon {
color: var(--color-primary);
opacity: 0.6;
flex-shrink: 0;
font-size: 9px;
}
.erp-item-dest {
font-size: 10px;
color: var(--color-text-muted);
margin-left: auto;
}
.erp-objects .erp-item-icon {
color: var(--color-gold);
}
.erp-occupants .erp-item-icon {
color: var(--color-secondary);
}
.erp-section-empty {
font-size: 10px;
color: rgba(138, 154, 184, 0.4);
font-style: italic;
padding: 2px 0;
}
/* Footer */
.erp-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
border-top: 1px solid rgba(74, 240, 192, 0.1);
background: rgba(74, 240, 192, 0.02);
}
.erp-footer-ts {
font-size: 10px;
color: var(--color-text-muted);
}
.erp-footer-room {
font-size: 10px;
color: var(--color-secondary);
font-weight: 600;
}