Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f701aa208 | ||
| ee5ae27c9e | |||
|
|
ecc05b5442 |
131
app.js
131
app.js
@@ -104,7 +104,13 @@ const orbitState = {
|
||||
|
||||
let flyY = 2;
|
||||
|
||||
// ═══ INIT ═══
|
||||
// ╡══ POV CAMERA SYSTEM ══╡
|
||||
let povMode = false; // true when viewing through agent's eyes
|
||||
let povAgentIdx = -1; // index into agents[] for POV target (-1 = none)
|
||||
let savedCameraState = null; // { position: Vector3, rotation: Euler } to restore on exit
|
||||
const DEFAULT_AGENT_FOV = 75; // default field-of-view for agent POV cameras
|
||||
|
||||
// ╡══ INIT ══╡
|
||||
|
||||
import {
|
||||
SymbolicEngine, AgentFSM, KnowledgeGraph, Blackboard,
|
||||
@@ -1334,10 +1340,10 @@ function updateNexusCommand(state) {
|
||||
// ═══ AGENT PRESENCE SYSTEM ═══
|
||||
function createAgentPresences() {
|
||||
const agentData = [
|
||||
{ id: 'timmy', name: 'TIMMY', color: NEXUS.colors.primary, pos: { x: -4, z: -4 }, station: { x: -4, z: -4 } },
|
||||
{ id: 'kimi', name: 'KIMI', color: NEXUS.colors.secondary, pos: { x: 4, z: -4 }, station: { x: 4, z: -4 } },
|
||||
{ id: 'claude', name: 'CLAUDE', color: NEXUS.colors.gold, pos: { x: 0, z: -6 }, station: { x: 0, z: -6 } },
|
||||
{ id: 'perplexity', name: 'PERPLEXITY', color: 0x4488ff, pos: { x: -6, z: -2 }, station: { x: -6, z: -2 } },
|
||||
{ id: 'timmy', name: 'TIMMY', color: NEXUS.colors.primary, pos: { x: -4, z: -4 }, station: { x: -4, z: -4 }, fov: 70 },
|
||||
{ id: 'kimi', name: 'KIMI', color: NEXUS.colors.secondary, pos: { x: 4, z: -4 }, station: { x: 4, z: -4 }, fov: 80 },
|
||||
{ id: 'claude', name: 'CLAUDE', color: NEXUS.colors.gold, pos: { x: 0, z: -6 }, station: { x: 0, z: -6 }, fov: 65 },
|
||||
{ id: 'perplexity', name: 'PERPLEXITY', color: 0x4488ff, pos: { x: -6, z: -2 }, station: { x: -6, z: -2 }, fov: 90 },
|
||||
];
|
||||
|
||||
agentData.forEach(data => {
|
||||
@@ -1393,7 +1399,8 @@ function createAgentPresences() {
|
||||
color,
|
||||
station: data.station,
|
||||
targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
|
||||
wanderTimer: 0
|
||||
wanderTimer: 0,
|
||||
fov: data.fov || DEFAULT_AGENT_FOV,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1957,7 +1964,97 @@ function updateNavModeUI(mode) {
|
||||
if (el) el.textContent = mode.toUpperCase();
|
||||
}
|
||||
|
||||
// ═══ CONTROLS ═══
|
||||
// ╡══ AGENT POV CAMERA TOGGLE ══╡
|
||||
|
||||
function toggleAgentPOV() {
|
||||
if (!agents.length) {
|
||||
addChatMessage('system', 'No agents present to observe.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (povMode) {
|
||||
// Exit POV mode
|
||||
exitAgentPOV();
|
||||
} else {
|
||||
// Enter POV mode on first agent
|
||||
enterAgentPOV(0);
|
||||
}
|
||||
}
|
||||
|
||||
function cycleAgentPOV() {
|
||||
if (!agents.length) return;
|
||||
if (!povMode) {
|
||||
enterAgentPOV(0);
|
||||
return;
|
||||
}
|
||||
const nextIdx = (povAgentIdx + 1) % agents.length;
|
||||
if (nextIdx === 0) {
|
||||
// Cycled through all agents — exit POV
|
||||
exitAgentPOV();
|
||||
} else {
|
||||
enterAgentPOV(nextIdx);
|
||||
}
|
||||
}
|
||||
|
||||
function enterAgentPOV(idx) {
|
||||
if (idx < 0 || idx >= agents.length) return;
|
||||
|
||||
// Save current camera state before switching
|
||||
if (!povMode) {
|
||||
savedCameraState = {
|
||||
position: camera.position.clone(),
|
||||
rotation: camera.rotation.clone(),
|
||||
fov: camera.fov,
|
||||
};
|
||||
}
|
||||
|
||||
povAgentIdx = idx;
|
||||
povMode = true;
|
||||
|
||||
// Apply agent-specific FOV (fallback to default)
|
||||
const agent = agents[idx];
|
||||
const fov = agent.fov || DEFAULT_AGENT_FOV;
|
||||
camera.fov = fov;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
updatePOVUI();
|
||||
addChatMessage('system', `Observing through ${agent.id.toUpperCase()}'s eyes. FOV: ${fov}°`);
|
||||
}
|
||||
|
||||
function exitAgentPOV() {
|
||||
if (!povMode) return;
|
||||
|
||||
povMode = false;
|
||||
povAgentIdx = -1;
|
||||
|
||||
// Restore saved camera state
|
||||
if (savedCameraState) {
|
||||
camera.position.copy(savedCameraState.position);
|
||||
camera.rotation.copy(savedCameraState.rotation);
|
||||
camera.fov = savedCameraState.fov;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
updatePOVUI();
|
||||
addChatMessage('system', 'Returned to God View.');
|
||||
}
|
||||
|
||||
function updatePOVUI() {
|
||||
const label = document.getElementById('pov-label');
|
||||
const btn = document.getElementById('pov-toggle-btn');
|
||||
if (!label || !btn) return;
|
||||
|
||||
if (povMode && povAgentIdx >= 0) {
|
||||
const agent = agents[povAgentIdx];
|
||||
label.textContent = agent.id.toUpperCase();
|
||||
btn.classList.add('pov-active');
|
||||
} else {
|
||||
label.textContent = 'AGENT POV';
|
||||
btn.classList.remove('pov-active');
|
||||
}
|
||||
}
|
||||
|
||||
// ╡══ CONTROLS ══╡
|
||||
function setupControls() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
keys[e.key.toLowerCase()] = true;
|
||||
@@ -1984,6 +2081,9 @@ function setupControls() {
|
||||
if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
|
||||
cycleNavMode();
|
||||
}
|
||||
if (e.key.toLowerCase() === 'p' && document.activeElement !== document.getElementById('chat-input')) {
|
||||
cycleAgentPOV();
|
||||
}
|
||||
if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) {
|
||||
activatePortal(activePortal);
|
||||
}
|
||||
@@ -2133,6 +2233,7 @@ function setupControls() {
|
||||
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
|
||||
|
||||
document.getElementById('mode-toggle-btn').addEventListener('click', toggleUIMode);
|
||||
document.getElementById('pov-toggle-btn').addEventListener('click', cycleAgentPOV);
|
||||
document.getElementById('atlas-toggle-btn').addEventListener('click', openPortalAtlas);
|
||||
document.getElementById('atlas-close-btn').addEventListener('click', closePortalAtlas);
|
||||
initAtlasControls();
|
||||
@@ -3370,7 +3471,21 @@ function gameLoop() {
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||||
|
||||
if (mode === 'walk') {
|
||||
// Agent POV mode overrides other camera modes
|
||||
if (povMode && povAgentIdx >= 0 && agents[povAgentIdx]) {
|
||||
const agent = agents[povAgentIdx];
|
||||
const orbPos = agent.orb.getWorldPosition(new THREE.Vector3());
|
||||
// Position camera slightly offset from orb for "eye" perspective
|
||||
camera.position.copy(orbPos);
|
||||
camera.position.y += 0.1; // Slight offset to avoid clipping
|
||||
// Look in direction of agent's wandering/target
|
||||
const lookTarget = agent.targetPos.clone();
|
||||
lookTarget.y = camera.position.y;
|
||||
camera.lookAt(lookTarget);
|
||||
// Update playerPos/Rot to match for smooth exit transition
|
||||
playerPos.copy(camera.position);
|
||||
playerRot.y = Math.atan2(lookTarget.x - camera.position.x, lookTarget.z - camera.position.z);
|
||||
} else if (mode === 'walk') {
|
||||
if (!chatActive && !portalOverlayActive) {
|
||||
const speed = 6 * delta;
|
||||
const dir = new THREE.Vector3();
|
||||
|
||||
98
docs/nostr-migration/CONSOLIDATION.md
Normal file
98
docs/nostr-migration/CONSOLIDATION.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Nostr Migration Consolidation Plan
|
||||
|
||||
> Issue #862 | Canonical Epic: the-nexus #819
|
||||
> Consolidated From: the-nexus #819 + timmy-config #138
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Two epics tracked the same Telegram -> Nostr migration with overlapping scope:
|
||||
|
||||
| Epic | Repo | Focus | Status |
|
||||
|------|------|-------|--------|
|
||||
| #819 | the-nexus | Client fork (Nostur), UI/UX, agent presence | **CANONICAL** |
|
||||
| #138 | timmy-config | Relay/infrastructure, deployment, ops | Tracked child |
|
||||
|
||||
Neither was the parent. Work risked duplication and drift.
|
||||
|
||||
---
|
||||
|
||||
## Resolution
|
||||
|
||||
**#819 is the canonical parent epic.** All Nostr migration work rolls up here.
|
||||
|
||||
### Scope Boundaries
|
||||
|
||||
| Component | Owner Repo | Epic / Issue |
|
||||
|-----------|-----------|--------------|
|
||||
| Nostur client fork | the-nexus | #819 |
|
||||
| Agent Nostr presence (JS) | the-nexus | #819 |
|
||||
| Relay deployment & infra | timmy-config | #138 (child of #819) |
|
||||
| Key management (NIP-49) | timmy-config | #138 (child of #819) |
|
||||
| Telegram-Nostr bridge | **NEW** | File as child of #819 |
|
||||
| Nostr identity (Python) | the-nexus | #819 |
|
||||
|
||||
### Child Issue Map
|
||||
|
||||
```
|
||||
#819 [EPIC] Operation Exodus: Telegram -> Nostr Migration (CANONICAL)
|
||||
|-- #138 [CHILD] Relay/infrastructure migration (timmy-config)
|
||||
| |-- Relay deployment (nostr-rs-relay or strfry)
|
||||
| |-- NIP-49 encrypted nsec keystore
|
||||
| +-- Health checks & alerting
|
||||
|-- [CHILD] Nostur client fork + UI skinning
|
||||
|-- [CHILD] Agent Nostr presence (JS bridge)
|
||||
+-- [CHILD] Telegram-Nostr bridge <- HIGHEST PRIORITY
|
||||
|-- Bidirectional message relay
|
||||
|-- Dual-presence period (both platforms active)
|
||||
+-- Graceful Telegram deprecation path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Implementation State
|
||||
|
||||
### Python Stack (the-nexus)
|
||||
- `nexus/nostr_identity.py` - Pure-Python BIP340 Schnorr signatures
|
||||
- WARNING **Timing side-channel vulnerabilities** (see FINDINGS-issue-801.md)
|
||||
- Suitable for prototyping; production needs `coincurve` or constant-time rewrite
|
||||
- `nexus/nostr_publisher.py` - Async WebSocket publisher to public relays
|
||||
|
||||
### Browser Stack (the-nexus)
|
||||
- `app.js:NostrAgent` - Browser-side agent presence
|
||||
- WARNING Uses **mock signatures** (`mock_id`, `mock_sig`)
|
||||
- Needs real crypto integration or delegation to Python backend
|
||||
|
||||
### Infrastructure (timmy-config)
|
||||
- `nostr-bridge.service` - Running but source file deleted, only `.pyc` remains
|
||||
- `/root/nostr-relay/keystore.json` - NIP-49 encrypted nsec storage
|
||||
|
||||
---
|
||||
|
||||
## Highest Priority: Telegram-Nostr Bridge
|
||||
|
||||
The bridge is the critical path. Without it, migration strands users on Telegram.
|
||||
|
||||
**Requirements:**
|
||||
1. Bidirectional message relay (Telegram <-> Nostr)
|
||||
2. Dual-presence period: both platforms active during transition
|
||||
3. Graceful deprecation: Telegram bot stays online until 90% of active users have Nostr handles
|
||||
4. Channel/topic mapping: preserve conversation structure
|
||||
|
||||
**File this as a new child issue under #819.**
|
||||
|
||||
---
|
||||
|
||||
## Action Items
|
||||
|
||||
- [ ] Close #138 in timmy-config with comment: "Consolidated into the-nexus #819. Relay/infrastructure work tracked as child of canonical epic."
|
||||
- [ ] Update #819 title/body to reference this consolidation plan
|
||||
- [ ] File child issue: Telegram-Nostr bridge (bidirectional, dual-presence)
|
||||
- [ ] File child issue: Fix timing side-channel in `nostr_identity.py` (or replace with `coincurve`)
|
||||
- [ ] File child issue: Replace mock signatures in `app.js:NostrAgent` with real crypto
|
||||
- [ ] Assign owners to each child issue
|
||||
|
||||
---
|
||||
|
||||
*Sovereignty and service always.*
|
||||
140
docs/nostr-migration/TELEGRAM-NOSTR-BRIDGE-SPEC.md
Normal file
140
docs/nostr-migration/TELEGRAM-NOSTR-BRIDGE-SPEC.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Telegram-Nostr Bridge Specification
|
||||
|
||||
> Child of Epic #819 (Operation Exodus: Telegram -> Nostr Migration)
|
||||
> Priority: HIGHEST
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Bidirectional message relay between Telegram and Nostr during the migration period.
|
||||
Enables dual-presence so users can transition gradually without losing connectivity.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
1. **Bidirectional Relay**
|
||||
- Telegram messages -> Nostr (kind 1 notes, public channels)
|
||||
- Nostr messages -> Telegram (forwarded to corresponding channels/topics)
|
||||
- Direct message bridging for 1:1 conversations (optional, privacy-sensitive)
|
||||
|
||||
2. **Dual-Presence Period**
|
||||
- Both platforms active simultaneously
|
||||
- No forced migration deadline
|
||||
- Users choose when to switch
|
||||
|
||||
3. **Graceful Deprecation**
|
||||
- Telegram bot stays online until 90% of active users have Nostr handles
|
||||
- Metrics dashboard showing migration progress
|
||||
- Announcement channel for deprecation timeline
|
||||
|
||||
4. **Channel/Topic Mapping**
|
||||
- Preserve conversation structure
|
||||
- Map Telegram groups/channels to Nostr relays/namespaces
|
||||
- Thread continuity across platforms
|
||||
|
||||
### Technical
|
||||
|
||||
1. **Nostr Side**
|
||||
- Publish to configured relays (damus.io, nos.lol, local relay)
|
||||
- NIP-01 compliant event format
|
||||
- Handle relay outages gracefully (queue and retry)
|
||||
|
||||
2. **Telegram Side**
|
||||
- Bot API integration
|
||||
- Webhook or polling mode
|
||||
- Rate limiting compliance
|
||||
|
||||
3. **Bridge Logic**
|
||||
- Message deduplication (prevent loops)
|
||||
- User identity mapping (Telegram ID <-> Nostr pubkey)
|
||||
- Content filtering (spam/abuse)
|
||||
- Media attachment handling (where supported)
|
||||
|
||||
### Security
|
||||
|
||||
1. **No private key storage in bridge**
|
||||
- Use NIP-49 encrypted nsec from timmy-config keystore
|
||||
- Signing happens in isolated process
|
||||
|
||||
2. **Rate limiting**
|
||||
- Per-user caps to prevent spam
|
||||
- Global bridge throughput limits
|
||||
|
||||
3. **Audit logging**
|
||||
- All bridged messages logged for 30 days
|
||||
- Log rotation and cleanup
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
+-------------+ +----------------+ +-------------+
|
||||
| Telegram |<--->| Bridge Core |<--->| Nostr |
|
||||
| Bot API | | (Python/JS) | | Relays |
|
||||
+-------------+ +----------------+ +-------------+
|
||||
|
|
||||
+----------------+
|
||||
| Identity Map |
|
||||
| (user mappings)|
|
||||
+----------------+
|
||||
|
|
||||
+----------------+
|
||||
| Keystore |
|
||||
| (NIP-49 nsec) |
|
||||
+----------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Basic Unidirectional (Telegram -> Nostr)
|
||||
- [ ] Telegram bot setup
|
||||
- [ ] Nostr publisher integration
|
||||
- [ ] Simple text message relay
|
||||
- [ ] Public channel bridging only
|
||||
|
||||
### Phase 2: Bidirectional
|
||||
- [ ] Nostr listener (WebSocket subscription)
|
||||
- [ ] Message relay Nostr -> Telegram
|
||||
- [ ] User identity mapping
|
||||
- [ ] Loop detection
|
||||
|
||||
### Phase 3: Production Hardening
|
||||
- [ ] Error handling and retry logic
|
||||
- [ ] Queue persistence (SQLite/Redis)
|
||||
- [ ] Metrics and monitoring
|
||||
- [ ] Rate limiting
|
||||
|
||||
### Phase 4: Graceful Deprecation
|
||||
- [ ] Migration progress dashboard
|
||||
- [ ] User notification system
|
||||
- [ ] Telegram sunset timeline
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Messages from Telegram public channels appear on Nostr within 5 seconds
|
||||
- [ ] Messages from Nostr appear in Telegram within 5 seconds
|
||||
- [ ] No duplicate messages (loop prevention)
|
||||
- [ ] Bridge survives relay outages (queues and retries)
|
||||
- [ ] Metrics show message throughput and lag
|
||||
- [ ] 30-day audit logs retained
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
- `nexus/nostr_publisher.py` - Nostr publishing (reusable)
|
||||
- `nexus/nostr_identity.py` - Signing (needs hardening)
|
||||
- `docs/nostr-migration/CONSOLIDATION.md` - Parent epic context
|
||||
|
||||
---
|
||||
|
||||
*Part of Operation Exodus.*
|
||||
@@ -1,71 +0,0 @@
|
||||
# GOFAI Symbolic Engine Debugger Overlay
|
||||
|
||||
Refs: the-nexus #871
|
||||
|
||||
## Overview
|
||||
|
||||
A specialized debug overlay that shows the internal state of the Symbolic Engine in real-time. Press **Ctrl+Shift+G** to toggle.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Active Symbols Panel
|
||||
Displays all facts currently in the symbolic engine with their truth values:
|
||||
- Green ● = true
|
||||
- Red ○ = false
|
||||
|
||||
### 2. FSM States Panel
|
||||
Shows the current state of all registered finite state machines:
|
||||
- Agent ID on the left
|
||||
- Current state on the right (highlighted)
|
||||
|
||||
### 3. Reasoning Paths Panel
|
||||
Chronological log of all reasoning steps:
|
||||
- Timestamp
|
||||
- Rule that fired
|
||||
- Outcome produced
|
||||
|
||||
### 4. Knowledge Graph Panel
|
||||
- Node count and edge count
|
||||
- Mini visualization of graph topology (up to 15 nodes)
|
||||
- Color-coded by type (Agent, Location, etc.)
|
||||
|
||||
### 5. Performance Metrics
|
||||
- Number of facts
|
||||
- Number of rules
|
||||
- JavaScript heap usage (if available)
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { SymbolicDebugger } from './nexus/components/symbolic-debugger.js';
|
||||
import { SymbolicEngine } from './nexus/symbolic-engine.js';
|
||||
|
||||
// Create engine
|
||||
const engine = new SymbolicEngine();
|
||||
|
||||
// Initialize debugger
|
||||
SymbolicDebugger.init({
|
||||
engine: engine,
|
||||
fsmRegistry: new Map(), // optional
|
||||
knowledgeGraph: null // optional
|
||||
});
|
||||
|
||||
// Show the overlay
|
||||
SymbolicDebugger.show();
|
||||
|
||||
// Or toggle with Ctrl+Shift+G
|
||||
```
|
||||
|
||||
## Auto-refresh
|
||||
|
||||
The debugger updates automatically every second when visible. Manual refresh via the ↻ button.
|
||||
|
||||
## Dragging
|
||||
|
||||
The overlay can be dragged by its header to reposition on screen.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
node tests/test_symbolic_debugger.js
|
||||
```
|
||||
@@ -173,6 +173,10 @@
|
||||
<span class="hud-icon">👁</span>
|
||||
<span class="hud-btn-label" id="mode-label">VISITOR</span>
|
||||
</button>
|
||||
<button id="pov-toggle-btn" class="hud-icon-btn" title="Agent POV Camera">
|
||||
<span class="hud-icon">👁</span>
|
||||
<span class="hud-btn-label" id="pov-label">AGENT POV</span>
|
||||
</button>
|
||||
<button id="atlas-toggle-btn" class="hud-icon-btn" title="Portal Atlas">
|
||||
<span class="hud-icon">🌐</span>
|
||||
<span class="hud-btn-label">WORLDS</span>
|
||||
@@ -229,6 +233,7 @@
|
||||
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
||||
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
|
||||
<span id="nav-mode-hint" class="nav-mode-hint"></span>
|
||||
<span>P</span> agent POV
|
||||
<span>H</span> archive
|
||||
<span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
|
||||
</div>
|
||||
|
||||
@@ -1,506 +0,0 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// GOFAI Symbolic Engine Debugger Overlay (issue #871)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// Specialized debug overlay showing internal state of the Symbolic Engine:
|
||||
// — Active symbols and their truth values
|
||||
// — Reasoning paths with timestamps
|
||||
// — FSM state visualizations
|
||||
// — Knowledge graph topology
|
||||
// — Performance metrics
|
||||
//
|
||||
// Usage:
|
||||
// import { SymbolicDebugger } from './symbolic-debugger.js';
|
||||
// SymbolicDebugger.init({ engine, blackboard, fsmRegistry });
|
||||
// SymbolicDebugger.show();
|
||||
// SymbolicDebugger.hide();
|
||||
// SymbolicDebugger.update(); // refresh from current state
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const SymbolicDebugger = (() => {
|
||||
let _overlay = null;
|
||||
let _engine = null;
|
||||
let _blackboard = null;
|
||||
let _fsmRegistry = null;
|
||||
let _kg = null;
|
||||
let _visible = false;
|
||||
let _refreshInterval = null;
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function init(opts = {}) {
|
||||
_engine = opts.engine || null;
|
||||
_blackboard = opts.blackboard || null;
|
||||
_fsmRegistry = opts.fsmRegistry || null;
|
||||
_kg = opts.knowledgeGraph || null;
|
||||
|
||||
// Create overlay if not exists
|
||||
if (!document.getElementById('symbolic-debugger-overlay')) {
|
||||
_createOverlay();
|
||||
}
|
||||
_overlay = document.getElementById('symbolic-debugger-overlay');
|
||||
|
||||
// Keyboard shortcut: Ctrl+Shift+G to toggle
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'G') {
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SymbolicDebugger] Initialized. Press Ctrl+Shift+G to toggle.');
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _createOverlay() {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'symbolic-debugger-overlay';
|
||||
div.className = 'sym-debug-overlay';
|
||||
div.style.cssText = `
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
width: 480px;
|
||||
max-height: calc(100vh - 80px);
|
||||
background: rgba(10, 15, 30, 0.95);
|
||||
border: 1px solid #4af0c0;
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #e0f0ff;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
box-shadow: 0 0 20px rgba(74, 240, 192, 0.3);
|
||||
`;
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="sym-debug-header" style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
border-bottom: 1px solid #4af0c0;
|
||||
cursor: move;
|
||||
">
|
||||
<span style="color: #4af0c0; font-weight: bold;">◈ GOFAI SYMBOLIC DEBUGGER</span>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button id="sym-debug-refresh" style="
|
||||
background: transparent;
|
||||
border: 1px solid #4af0c0;
|
||||
color: #4af0c0;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
">↻ Refresh</button>
|
||||
<button id="sym-debug-close" style="
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #4af0c0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sym-debug-content" style="padding: 12px;">
|
||||
<!-- Active Symbols Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Active Symbols</div>
|
||||
<div id="sym-debug-symbols" style="
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 4px 12px;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- FSM States Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">FSM States</div>
|
||||
<div id="sym-debug-fsm" style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- Reasoning Log Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Reasoning Paths</div>
|
||||
<div id="sym-debug-reasoning" style="
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
font-size: 11px;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge Graph Section -->
|
||||
<div class="sym-section" style="margin-bottom: 16px;">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Knowledge Graph</div>
|
||||
<div id="sym-debug-kg-stats" style="margin-bottom: 8px; font-size: 11px;"></div>
|
||||
<div id="sym-debug-kg-viz" style="
|
||||
height: 100px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(74, 240, 192, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Metrics Section -->
|
||||
<div class="sym-section">
|
||||
<div class="sym-section-title" style="
|
||||
color: #4af0c0;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(74, 240, 192, 0.3);
|
||||
padding-bottom: 4px;
|
||||
">Performance</div>
|
||||
<div id="sym-debug-metrics" style="
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 12px;
|
||||
font-size: 11px;
|
||||
"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(div);
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('sym-debug-close').onclick = hide;
|
||||
document.getElementById('sym-debug-refresh').onclick = update;
|
||||
|
||||
// Make draggable
|
||||
let isDragging = false;
|
||||
let dragOffsetX = 0;
|
||||
let dragOffsetY = 0;
|
||||
|
||||
const header = div.querySelector('.sym-debug-header');
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
isDragging = true;
|
||||
dragOffsetX = e.clientX - div.offsetLeft;
|
||||
dragOffsetY = e.clientY - div.offsetTop;
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging) return;
|
||||
div.style.left = (e.clientX - dragOffsetX) + 'px';
|
||||
div.style.top = (e.clientY - dragOffsetY) + 'px';
|
||||
div.style.right = 'auto';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging = false;
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function update() {
|
||||
if (!_visible || !_overlay) return;
|
||||
|
||||
// Update symbols
|
||||
_updateSymbols();
|
||||
|
||||
// Update FSM states
|
||||
_updateFSM();
|
||||
|
||||
// Update reasoning log
|
||||
_updateReasoning();
|
||||
|
||||
// Update knowledge graph
|
||||
_updateKnowledgeGraph();
|
||||
|
||||
// Update metrics
|
||||
_updateMetrics();
|
||||
}
|
||||
|
||||
function _updateSymbols() {
|
||||
const container = document.getElementById('sym-debug-symbols');
|
||||
if (!container || !_engine) {
|
||||
if (container) container.innerHTML = '<span style="color: #888;">No engine connected</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
if (_engine.facts && _engine.facts.size > 0) {
|
||||
for (const [key, value] of _engine.facts) {
|
||||
const truthColor = value ? '#4af0c0' : '#ff4466';
|
||||
const truthIcon = value ? '●' : '○';
|
||||
html += `
|
||||
<span style="color: #e0f0ff;">${_esc(key)}</span>
|
||||
<span style="color: ${truthColor};">${truthIcon} ${_esc(String(value))}</span>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
html = '<span style="color: #888; grid-column: 1 / -1;">No active symbols</span>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function _updateFSM() {
|
||||
const container = document.getElementById('sym-debug-fsm');
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
if (_fsmRegistry && _fsmRegistry.size > 0) {
|
||||
for (const [agentId, fsm] of _fsmRegistry) {
|
||||
const transitions = fsm.transitions ? Object.keys(fsm.transitions).length : 0;
|
||||
html += `
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
background: rgba(74, 240, 192, 0.05);
|
||||
border-left: 2px solid #4af0c0;
|
||||
">
|
||||
<span style="color: #e0f0ff;">${_esc(agentId)}</span>
|
||||
<span style="
|
||||
color: #4af0c0;
|
||||
font-size: 10px;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
">${_esc(fsm.state)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else if (_engine && _engine.fsm) {
|
||||
// Single FSM mode
|
||||
html += `
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
background: rgba(74, 240, 192, 0.05);
|
||||
border-left: 2px solid #4af0c0;
|
||||
">
|
||||
<span style="color: #e0f0ff;">Primary FSM</span>
|
||||
<span style="
|
||||
color: #4af0c0;
|
||||
font-size: 10px;
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
">${_esc(_engine.fsm.state)}</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
html = '<span style="color: #888;">No FSM registered</span>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function _updateReasoning() {
|
||||
const container = document.getElementById('sym-debug-reasoning');
|
||||
if (!container || !_engine) {
|
||||
if (container) container.innerHTML = '<span style="color: #888;">No reasoning log</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
if (_engine.reasoningLog && _engine.reasoningLog.length > 0) {
|
||||
for (const entry of _engine.reasoningLog) {
|
||||
const time = entry.timestamp
|
||||
? new Date(entry.timestamp).toLocaleTimeString()
|
||||
: '--:--:--';
|
||||
html += `
|
||||
<div style="
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 6px;
|
||||
background: rgba(74, 240, 192, 0.03);
|
||||
border-left: 2px solid rgba(74, 240, 192, 0.3);
|
||||
">
|
||||
<div style="color: #888; font-size: 10px;">${time}</div>
|
||||
<div style="color: #4af0c0;">${_esc(entry.rule || 'Unknown rule')}</div>
|
||||
<div style="color: #e0f0ff;">→ ${_esc(entry.outcome || 'No outcome')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
html = '<span style="color: #888;">No reasoning paths recorded</span>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function _updateKnowledgeGraph() {
|
||||
const statsContainer = document.getElementById('sym-debug-kg-stats');
|
||||
const vizContainer = document.getElementById('sym-debug-kg-viz');
|
||||
if (!statsContainer || !vizContainer) return;
|
||||
|
||||
if (!_kg) {
|
||||
statsContainer.innerHTML = '<span style="color: #888;">No knowledge graph connected</span>';
|
||||
vizContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeCount = _kg.nodes ? _kg.nodes.size : 0;
|
||||
const edgeCount = _kg.edges ? _kg.edges.length : 0;
|
||||
|
||||
statsContainer.innerHTML = `
|
||||
<span style="color: #4af0c0;">${nodeCount}</span> nodes
|
||||
<span style="color: #888;">|</span>
|
||||
<span style="color: #4af0c0;">${edgeCount}</span> edges
|
||||
`;
|
||||
|
||||
// Simple visualization
|
||||
if (nodeCount > 0 && vizContainer) {
|
||||
let vizHtml = '';
|
||||
let idx = 0;
|
||||
for (const [nodeId, node] of _kg.nodes) {
|
||||
const x = 20 + (idx % 5) * 80;
|
||||
const y = 20 + Math.floor(idx / 5) * 30;
|
||||
const color = node.type === 'Agent' ? '#4af0c0' :
|
||||
node.type === 'Location' ? '#ffaa22' : '#4488ff';
|
||||
vizHtml += `
|
||||
<div style="
|
||||
position: absolute;
|
||||
left: ${x}px;
|
||||
top: ${y}px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: ${color};
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px ${color};
|
||||
" title="${_esc(nodeId)}"></div>
|
||||
`;
|
||||
idx++;
|
||||
if (idx >= 15) break; // Limit visualization
|
||||
}
|
||||
vizContainer.innerHTML = vizHtml;
|
||||
} else {
|
||||
vizContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function _updateMetrics() {
|
||||
const container = document.getElementById('sym-debug-metrics');
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
|
||||
// Engine metrics
|
||||
if (_engine) {
|
||||
const factCount = _engine.facts ? _engine.facts.size : 0;
|
||||
const ruleCount = _engine.rules ? _engine.rules.length : 0;
|
||||
html += `
|
||||
<span style="color: #888;">Facts:</span> <span style="color: #4af0c0;">${factCount}</span>
|
||||
<span style="color: #888;">Rules:</span> <span style="color: #4af0c0;">${ruleCount}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Memory usage (rough estimate)
|
||||
if (performance && performance.memory) {
|
||||
const usedMB = Math.round(performance.memory.usedJSHeapSize / 1048576);
|
||||
html += `
|
||||
<span style="color: #888;">Heap:</span> <span style="color: #4af0c0;">${usedMB} MB</span>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html || '<span style="color: #888;">No metrics available</span>';
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function show() {
|
||||
if (!_overlay) return;
|
||||
_overlay.style.display = 'block';
|
||||
_visible = true;
|
||||
update();
|
||||
_startAutoRefresh();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!_overlay) return;
|
||||
_overlay.style.display = 'none';
|
||||
_visible = false;
|
||||
_stopAutoRefresh();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (_visible) hide();
|
||||
else show();
|
||||
}
|
||||
|
||||
function isVisible() {
|
||||
return _visible;
|
||||
}
|
||||
|
||||
function _startAutoRefresh() {
|
||||
if (_refreshInterval) return;
|
||||
_refreshInterval = setInterval(update, 1000); // Update every second
|
||||
}
|
||||
|
||||
function _stopAutoRefresh() {
|
||||
if (_refreshInterval) {
|
||||
clearInterval(_refreshInterval);
|
||||
_refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function _esc(str) {
|
||||
if (typeof str !== 'string') return String(str);
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
return {
|
||||
init,
|
||||
show,
|
||||
hide,
|
||||
toggle,
|
||||
isVisible,
|
||||
update,
|
||||
};
|
||||
})();
|
||||
|
||||
// Auto-export for module or global usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SymbolicDebugger };
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.SymbolicDebugger = SymbolicDebugger;
|
||||
}
|
||||
@@ -200,6 +200,13 @@ canvas#nexus-canvas {
|
||||
box-shadow: 0 0 20px var(--color-primary);
|
||||
}
|
||||
|
||||
.hud-icon-btn.pov-active {
|
||||
background: var(--color-gold);
|
||||
border-color: var(--color-gold);
|
||||
color: var(--color-bg);
|
||||
box-shadow: 0 0 20px var(--color-gold);
|
||||
}
|
||||
|
||||
.hud-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Tests for SymbolicDebugger component (issue #871)
|
||||
*/
|
||||
|
||||
import { SymbolicDebugger } from '../nexus/components/symbolic-debugger.js';
|
||||
import { SymbolicEngine, AgentFSM, KnowledgeGraph } from '../nexus/symbolic-engine.js';
|
||||
|
||||
// Mock DOM for Node.js testing
|
||||
if (typeof document === 'undefined') {
|
||||
const mockElements = new Map();
|
||||
|
||||
global.document = {
|
||||
createElement: (tag) => {
|
||||
const el = {
|
||||
tagName: tag,
|
||||
style: {},
|
||||
innerHTML: '',
|
||||
children: [],
|
||||
appendChild: function(child) { this.children.push(child); return child; },
|
||||
prepend: function(child) { this.children.unshift(child); return child; },
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
addEventListener: () => {},
|
||||
setAttribute: () => {},
|
||||
getAttribute: () => null,
|
||||
};
|
||||
return el;
|
||||
},
|
||||
body: {
|
||||
appendChild: (el) => {
|
||||
mockElements.set(el.id, el);
|
||||
return el;
|
||||
}
|
||||
},
|
||||
addEventListener: () => {},
|
||||
getElementById: (id) => mockElements.get(id) || {
|
||||
style: {},
|
||||
innerHTML: '',
|
||||
onclick: null,
|
||||
addEventListener: () => {},
|
||||
},
|
||||
};
|
||||
global.window = { SymbolicDebugger: null };
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
console.error(`❌ FAILED: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✔ PASSED: ${message}`);
|
||||
}
|
||||
|
||||
console.log('--- Running Symbolic Debugger Tests ---');
|
||||
|
||||
// Test 1: Module exports
|
||||
assert(typeof SymbolicDebugger === 'object', 'SymbolicDebugger exports an object');
|
||||
assert(typeof SymbolicDebugger.init === 'function', 'SymbolicDebugger has init method');
|
||||
assert(typeof SymbolicDebugger.show === 'function', 'SymbolicDebugger has show method');
|
||||
assert(typeof SymbolicDebugger.hide === 'function', 'SymbolicDebugger has hide method');
|
||||
assert(typeof SymbolicDebugger.toggle === 'function', 'SymbolicDebugger has toggle method');
|
||||
assert(typeof SymbolicDebugger.update === 'function', 'SymbolicDebugger has update method');
|
||||
|
||||
// Test 2: Initial state
|
||||
assert(SymbolicDebugger.isVisible() === false, 'Debugger starts hidden');
|
||||
|
||||
// Test 3: Engine integration (mock)
|
||||
const mockEngine = {
|
||||
facts: new Map([['energy', 75], ['stable', true]]),
|
||||
rules: [{ condition: () => true, action: () => 'test' }],
|
||||
reasoningLog: [
|
||||
{ timestamp: Date.now(), rule: 'TestRule', outcome: 'TestOutcome' }
|
||||
]
|
||||
};
|
||||
|
||||
SymbolicDebugger.init({ engine: mockEngine });
|
||||
assert(true, 'Debugger initializes with engine');
|
||||
|
||||
// Test 4: FSM integration
|
||||
const mockFSM = { state: 'IDLE', transitions: { IDLE: [] } };
|
||||
SymbolicDebugger.init({ engine: mockEngine, fsmRegistry: new Map([['Agent1', mockFSM]]) });
|
||||
assert(true, 'Debugger initializes with FSM registry');
|
||||
|
||||
// Test 5: Knowledge Graph integration
|
||||
const mockKG = {
|
||||
nodes: new Map([
|
||||
['A', { id: 'A', type: 'Agent' }],
|
||||
['B', { id: 'B', type: 'Location' }]
|
||||
]),
|
||||
edges: [{ from: 'A', to: 'B', relation: 'AT' }]
|
||||
};
|
||||
SymbolicDebugger.init({ engine: mockEngine, knowledgeGraph: mockKG });
|
||||
assert(true, 'Debugger initializes with Knowledge Graph');
|
||||
|
||||
console.log('--- All Symbolic Debugger Tests Passed ---');
|
||||
Reference in New Issue
Block a user