1
0

[perplexity] feat: Nexus v2 — Cognitive Awareness & Introspection Engine (#1090) (#1348)

Co-authored-by: Perplexity Computer <perplexity@tower.local>
Co-committed-by: Perplexity Computer <perplexity@tower.local>
This commit is contained in:
2026-03-24 02:50:40 +00:00
committed by Claude (Opus 4.6)
parent 9e8e0f8552
commit d0b6d87eb1
12 changed files with 1785 additions and 36 deletions

View File

@@ -1,21 +1,32 @@
"""Nexus — Timmy's persistent conversational awareness space.
"""Nexus v2 — Timmy's persistent conversational awareness space.
A conversational-only interface where Timmy maintains live memory context.
No tool use; pure conversation with memory integration and a teaching panel.
Extends the v1 Nexus (chat + memory sidebar + teaching panel) with:
- **Persistent conversations** — SQLite-backed history survives restarts.
- **Introspection panel** — live cognitive state, recent thoughts, session
analytics rendered alongside every conversation turn.
- **Sovereignty pulse** — real-time sovereignty health badge in the sidebar.
- **WebSocket** — pushes introspection + sovereignty snapshots so the
Nexus page stays alive without polling.
Routes:
GET /nexus — render nexus page with live memory sidebar
GET /nexus — render nexus page with full awareness panels
POST /nexus/chat — send a message; returns HTMX partial
POST /nexus/teach — inject a fact into Timmy's live memory
DELETE /nexus/history — clear the nexus conversation history
GET /nexus/introspect — JSON introspection snapshot (API)
WS /nexus/ws — live introspection + sovereignty push
Refs: #1090 (Nexus Epic), #953 (Sovereignty Loop)
"""
import asyncio
import json
import logging
from datetime import UTC, datetime
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse
from fastapi import APIRouter, Form, Request, WebSocket
from fastapi.responses import HTMLResponse, JSONResponse
from dashboard.templating import templates
from timmy.memory_system import (
@@ -24,6 +35,9 @@ from timmy.memory_system import (
search_memories,
store_personal_fact,
)
from timmy.nexus.introspection import nexus_introspector
from timmy.nexus.persistence import nexus_store
from timmy.nexus.sovereignty_pulse import sovereignty_pulse
from timmy.session import _clean_response, chat, reset_session
logger = logging.getLogger(__name__)
@@ -32,28 +46,74 @@ router = APIRouter(prefix="/nexus", tags=["nexus"])
_NEXUS_SESSION_ID = "nexus"
_MAX_MESSAGE_LENGTH = 10_000
_WS_PUSH_INTERVAL = 5 # seconds between WebSocket pushes
# In-memory conversation log for the Nexus session (mirrors chat store pattern
# but is scoped to the Nexus so it won't pollute the main dashboard history).
# In-memory conversation log — kept in sync with the persistent store
# so templates can render without hitting the DB on every page load.
_nexus_log: list[dict] = []
# ── Initialisation ───────────────────────────────────────────────────────────
# On module load, hydrate the in-memory log from the persistent store.
# This runs once at import time (process startup).
_HYDRATED = False
def _hydrate_log() -> None:
"""Load persisted history into the in-memory log (idempotent)."""
global _HYDRATED
if _HYDRATED:
return
try:
rows = nexus_store.get_history(limit=200)
_nexus_log.clear()
for row in rows:
_nexus_log.append(
{
"role": row["role"],
"content": row["content"],
"timestamp": row["timestamp"],
}
)
_HYDRATED = True
logger.info("Nexus: hydrated %d messages from persistent store", len(_nexus_log))
except Exception as exc:
logger.warning("Nexus: failed to hydrate from store: %s", exc)
_HYDRATED = True # Don't retry repeatedly
# ── Helpers ──────────────────────────────────────────────────────────────────
def _ts() -> str:
return datetime.now(UTC).strftime("%H:%M:%S")
def _append_log(role: str, content: str) -> None:
_nexus_log.append({"role": role, "content": content, "timestamp": _ts()})
# Keep last 200 exchanges to bound memory usage
"""Append to both in-memory log and persistent store."""
ts = _ts()
_nexus_log.append({"role": role, "content": content, "timestamp": ts})
# Bound in-memory log
if len(_nexus_log) > 200:
del _nexus_log[:-200]
# Persist
try:
nexus_store.append(role, content, timestamp=ts)
except Exception as exc:
logger.warning("Nexus: persist failed: %s", exc)
# ── Page route ───────────────────────────────────────────────────────────────
@router.get("", response_class=HTMLResponse)
async def nexus_page(request: Request):
"""Render the Nexus page with live memory context."""
"""Render the Nexus page with full awareness panels."""
_hydrate_log()
stats = get_memory_stats()
facts = recall_personal_facts_with_ids()[:8]
introspection = nexus_introspector.snapshot(conversation_log=_nexus_log)
pulse = sovereignty_pulse.snapshot()
return templates.TemplateResponse(
request,
@@ -63,13 +123,18 @@ async def nexus_page(request: Request):
"messages": list(_nexus_log),
"stats": stats,
"facts": facts,
"introspection": introspection.to_dict(),
"pulse": pulse.to_dict(),
},
)
# ── Chat route ───────────────────────────────────────────────────────────────
@router.post("/chat", response_class=HTMLResponse)
async def nexus_chat(request: Request, message: str = Form(...)):
"""Conversational-only chat routed through the Nexus session.
"""Conversational-only chat with persistence and introspection.
Does not invoke tool-use approval flow — pure conversation with memory
context injected from Timmy's live memory store.
@@ -87,18 +152,22 @@ async def nexus_chat(request: Request, message: str = Form(...)):
"error": "Message too long (max 10 000 chars).",
"timestamp": _ts(),
"memory_hits": [],
"introspection": nexus_introspector.snapshot().to_dict(),
},
)
ts = _ts()
# Fetch semantically relevant memories to surface in the sidebar
# Fetch semantically relevant memories
try:
memory_hits = await asyncio.to_thread(search_memories, query=message, limit=4)
except Exception as exc:
logger.warning("Nexus memory search failed: %s", exc)
memory_hits = []
# Track memory hits for analytics
nexus_introspector.record_memory_hits(len(memory_hits))
# Conversational response — no tool approval flow
response_text: str | None = None
error_text: str | None = None
@@ -113,6 +182,9 @@ async def nexus_chat(request: Request, message: str = Form(...)):
if response_text:
_append_log("assistant", response_text)
# Build fresh introspection snapshot after the exchange
introspection = nexus_introspector.snapshot(conversation_log=_nexus_log)
return templates.TemplateResponse(
request,
"partials/nexus_message.html",
@@ -122,10 +194,14 @@ async def nexus_chat(request: Request, message: str = Form(...)):
"error": error_text,
"timestamp": ts,
"memory_hits": memory_hits,
"introspection": introspection.to_dict(),
},
)
# ── Teach route ──────────────────────────────────────────────────────────────
@router.post("/teach", response_class=HTMLResponse)
async def nexus_teach(request: Request, fact: str = Form(...)):
"""Inject a fact into Timmy's live memory from the Nexus teaching panel."""
@@ -148,11 +224,20 @@ async def nexus_teach(request: Request, fact: str = Form(...)):
)
# ── Clear history ────────────────────────────────────────────────────────────
@router.delete("/history", response_class=HTMLResponse)
async def nexus_clear_history(request: Request):
"""Clear the Nexus conversation history."""
"""Clear the Nexus conversation history (both in-memory and persistent)."""
_nexus_log.clear()
try:
nexus_store.clear()
except Exception as exc:
logger.warning("Nexus: persistent clear failed: %s", exc)
nexus_introspector.reset()
reset_session(session_id=_NEXUS_SESSION_ID)
return templates.TemplateResponse(
request,
"partials/nexus_message.html",
@@ -162,5 +247,55 @@ async def nexus_clear_history(request: Request):
"error": None,
"timestamp": _ts(),
"memory_hits": [],
"introspection": nexus_introspector.snapshot().to_dict(),
},
)
# ── Introspection API ────────────────────────────────────────────────────────
@router.get("/introspect", response_class=JSONResponse)
async def nexus_introspect():
"""Return a JSON introspection snapshot (for API consumers)."""
snap = nexus_introspector.snapshot(conversation_log=_nexus_log)
pulse = sovereignty_pulse.snapshot()
return {
"introspection": snap.to_dict(),
"sovereignty_pulse": pulse.to_dict(),
}
# ── WebSocket — live Nexus push ──────────────────────────────────────────────
@router.websocket("/ws")
async def nexus_ws(websocket: WebSocket) -> None:
"""Push introspection + sovereignty pulse snapshots to the Nexus page.
The frontend connects on page load and receives JSON updates every
``_WS_PUSH_INTERVAL`` seconds, keeping the cognitive state panel,
thought stream, and sovereignty badge fresh without HTMX polling.
"""
await websocket.accept()
logger.info("Nexus WS connected")
try:
# Immediate first push
await _push_snapshot(websocket)
while True:
await asyncio.sleep(_WS_PUSH_INTERVAL)
await _push_snapshot(websocket)
except Exception:
logger.debug("Nexus WS disconnected")
async def _push_snapshot(ws: WebSocket) -> None:
"""Send the combined introspection + pulse payload."""
snap = nexus_introspector.snapshot(conversation_log=_nexus_log)
pulse = sovereignty_pulse.snapshot()
payload = {
"type": "nexus_state",
"introspection": snap.to_dict(),
"sovereignty_pulse": pulse.to_dict(),
}
await ws.send_text(json.dumps(payload))

View File

@@ -8,26 +8,40 @@
<div class="container-fluid nexus-layout py-3">
<div class="nexus-header mb-3">
<div class="nexus-title">// NEXUS</div>
<div class="nexus-subtitle">
Persistent conversational awareness &mdash; always present, always learning.
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="nexus-title">// NEXUS</div>
<div class="nexus-subtitle">
Persistent conversational awareness &mdash; always present, always learning.
</div>
</div>
<!-- Sovereignty Pulse badge -->
<div class="nexus-pulse-badge" id="nexus-pulse-badge">
<span class="nexus-pulse-dot nexus-pulse-{{ pulse.health }}"></span>
<span class="nexus-pulse-label">SOVEREIGNTY</span>
<span class="nexus-pulse-value" id="pulse-overall">{{ pulse.overall_pct }}%</span>
</div>
</div>
</div>
<div class="nexus-grid">
<div class="nexus-grid-v2">
<!-- ── LEFT: Conversation ────────────────────────────────── -->
<div class="nexus-chat-col">
<div class="card mc-panel nexus-chat-panel">
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
<span>// CONVERSATION</span>
<button class="mc-btn mc-btn-sm"
hx-delete="/nexus/history"
hx-target="#nexus-chat-log"
hx-swap="beforeend"
hx-confirm="Clear nexus conversation?">
CLEAR
</button>
<div class="d-flex align-items-center gap-2">
<span class="nexus-msg-count" id="nexus-msg-count"
title="Messages in this session">{{ messages|length }} msgs</span>
<button class="mc-btn mc-btn-sm"
hx-delete="/nexus/history"
hx-target="#nexus-chat-log"
hx-swap="beforeend"
hx-confirm="Clear nexus conversation?">
CLEAR
</button>
</div>
</div>
<div class="card-body p-2" id="nexus-chat-log">
@@ -67,14 +81,115 @@
</div>
</div>
<!-- ── RIGHT: Memory sidebar ─────────────────────────────── -->
<!-- ── RIGHT: Awareness sidebar ──────────────────────────── -->
<div class="nexus-sidebar-col">
<!-- Live memory context (updated with each response) -->
<!-- Cognitive State Panel -->
<div class="card mc-panel nexus-cognitive-panel mb-3">
<div class="card-header mc-panel-header">
<span>// COGNITIVE STATE</span>
<span class="nexus-engagement-badge" id="cog-engagement">
{{ introspection.cognitive.engagement | upper }}
</span>
</div>
<div class="card-body p-2">
<div class="nexus-cog-grid">
<div class="nexus-cog-item">
<div class="nexus-cog-label">MOOD</div>
<div class="nexus-cog-value" id="cog-mood">{{ introspection.cognitive.mood }}</div>
</div>
<div class="nexus-cog-item">
<div class="nexus-cog-label">FOCUS</div>
<div class="nexus-cog-value nexus-cog-focus" id="cog-focus">
{{ introspection.cognitive.focus_topic or '—' }}
</div>
</div>
<div class="nexus-cog-item">
<div class="nexus-cog-label">DEPTH</div>
<div class="nexus-cog-value" id="cog-depth">{{ introspection.cognitive.conversation_depth }}</div>
</div>
<div class="nexus-cog-item">
<div class="nexus-cog-label">INITIATIVE</div>
<div class="nexus-cog-value nexus-cog-focus" id="cog-initiative">
{{ introspection.cognitive.last_initiative or '—' }}
</div>
</div>
</div>
{% if introspection.cognitive.active_commitments %}
<div class="nexus-commitments mt-2">
<div class="nexus-cog-label">ACTIVE COMMITMENTS</div>
{% for c in introspection.cognitive.active_commitments %}
<div class="nexus-commitment-item">{{ c | e }}</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<!-- Recent Thoughts Panel -->
<div class="card mc-panel nexus-thoughts-panel mb-3">
<div class="card-header mc-panel-header">
<span>// THOUGHT STREAM</span>
</div>
<div class="card-body p-2" id="nexus-thoughts-body">
{% if introspection.recent_thoughts %}
{% for t in introspection.recent_thoughts %}
<div class="nexus-thought-item">
<div class="nexus-thought-meta">
<span class="nexus-thought-seed">{{ t.seed_type }}</span>
<span class="nexus-thought-time">{{ t.created_at[:16] }}</span>
</div>
<div class="nexus-thought-content">{{ t.content | e }}</div>
</div>
{% endfor %}
{% else %}
<div class="nexus-empty-state">No thoughts yet. The thinking engine will populate this.</div>
{% endif %}
</div>
</div>
<!-- Sovereignty Pulse Detail -->
<div class="card mc-panel nexus-sovereignty-panel mb-3">
<div class="card-header mc-panel-header">
<span>// SOVEREIGNTY PULSE</span>
<span class="nexus-health-badge nexus-health-{{ pulse.health }}" id="pulse-health">
{{ pulse.health | upper }}
</span>
</div>
<div class="card-body p-2">
<div class="nexus-pulse-meters" id="nexus-pulse-meters">
{% for layer in pulse.layers %}
<div class="nexus-pulse-layer">
<div class="nexus-pulse-layer-label">{{ layer.name | upper }}</div>
<div class="nexus-pulse-bar-track">
<div class="nexus-pulse-bar-fill" style="width: {{ layer.sovereign_pct }}%"></div>
</div>
<div class="nexus-pulse-layer-pct">{{ layer.sovereign_pct }}%</div>
</div>
{% endfor %}
</div>
<div class="nexus-pulse-stats mt-2">
<div class="nexus-pulse-stat">
<span class="nexus-pulse-stat-label">Crystallizations</span>
<span class="nexus-pulse-stat-value" id="pulse-cryst">{{ pulse.crystallizations_last_hour }}</span>
</div>
<div class="nexus-pulse-stat">
<span class="nexus-pulse-stat-label">API Independence</span>
<span class="nexus-pulse-stat-value" id="pulse-api-indep">{{ pulse.api_independence_pct }}%</span>
</div>
<div class="nexus-pulse-stat">
<span class="nexus-pulse-stat-label">Total Events</span>
<span class="nexus-pulse-stat-value" id="pulse-events">{{ pulse.total_events }}</span>
</div>
</div>
</div>
</div>
<!-- Live Memory Context -->
<div class="card mc-panel nexus-memory-panel mb-3">
<div class="card-header mc-panel-header">
<span>// LIVE MEMORY</span>
<span class="badge ms-2" style="background:var(--purple-dim); color:var(--purple);">
<span class="badge ms-2" style="background:var(--purple-dim, rgba(168,85,247,0.15)); color:var(--purple);">
{{ stats.total_entries }} stored
</span>
</div>
@@ -85,7 +200,32 @@
</div>
</div>
<!-- Teaching panel -->
<!-- Session Analytics -->
<div class="card mc-panel nexus-analytics-panel mb-3">
<div class="card-header mc-panel-header">// SESSION ANALYTICS</div>
<div class="card-body p-2">
<div class="nexus-analytics-grid" id="nexus-analytics">
<div class="nexus-analytics-item">
<span class="nexus-analytics-label">Messages</span>
<span class="nexus-analytics-value" id="analytics-msgs">{{ introspection.analytics.total_messages }}</span>
</div>
<div class="nexus-analytics-item">
<span class="nexus-analytics-label">Avg Response</span>
<span class="nexus-analytics-value" id="analytics-avg">{{ introspection.analytics.avg_response_length }} chars</span>
</div>
<div class="nexus-analytics-item">
<span class="nexus-analytics-label">Memory Hits</span>
<span class="nexus-analytics-value" id="analytics-mem">{{ introspection.analytics.memory_hits_total }}</span>
</div>
<div class="nexus-analytics-item">
<span class="nexus-analytics-label">Duration</span>
<span class="nexus-analytics-value" id="analytics-dur">{{ introspection.analytics.session_duration_minutes }} min</span>
</div>
</div>
</div>
</div>
<!-- Teaching Panel -->
<div class="card mc-panel nexus-teach-panel">
<div class="card-header mc-panel-header">// TEACH TIMMY</div>
<div class="card-body p-2">
@@ -119,4 +259,128 @@
</div><!-- /nexus-grid -->
</div>
<!-- WebSocket for live Nexus updates -->
<script>
(function() {
var wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var wsUrl = wsProto + '//' + location.host + '/nexus/ws';
var ws = null;
var reconnectDelay = 2000;
function connect() {
ws = new WebSocket(wsUrl);
ws.onmessage = function(e) {
try {
var data = JSON.parse(e.data);
if (data.type === 'nexus_state') {
updateCognitive(data.introspection.cognitive);
updateThoughts(data.introspection.recent_thoughts);
updateAnalytics(data.introspection.analytics);
updatePulse(data.sovereignty_pulse);
}
} catch(err) { /* ignore parse errors */ }
};
ws.onclose = function() {
setTimeout(connect, reconnectDelay);
};
ws.onerror = function() { ws.close(); };
}
function updateCognitive(c) {
var el;
el = document.getElementById('cog-mood');
if (el) el.textContent = c.mood;
el = document.getElementById('cog-engagement');
if (el) el.textContent = c.engagement.toUpperCase();
el = document.getElementById('cog-focus');
if (el) el.textContent = c.focus_topic || '\u2014';
el = document.getElementById('cog-depth');
if (el) el.textContent = c.conversation_depth;
el = document.getElementById('cog-initiative');
if (el) el.textContent = c.last_initiative || '\u2014';
}
function updateThoughts(thoughts) {
var container = document.getElementById('nexus-thoughts-body');
if (!container || !thoughts || thoughts.length === 0) return;
var html = '';
for (var i = 0; i < thoughts.length; i++) {
var t = thoughts[i];
html += '<div class="nexus-thought-item">'
+ '<div class="nexus-thought-meta">'
+ '<span class="nexus-thought-seed">' + escHtml(t.seed_type) + '</span>'
+ '<span class="nexus-thought-time">' + escHtml((t.created_at || '').substring(0,16)) + '</span>'
+ '</div>'
+ '<div class="nexus-thought-content">' + escHtml(t.content) + '</div>'
+ '</div>';
}
container.innerHTML = html;
}
function updateAnalytics(a) {
var el;
el = document.getElementById('analytics-msgs');
if (el) el.textContent = a.total_messages;
el = document.getElementById('analytics-avg');
if (el) el.textContent = a.avg_response_length + ' chars';
el = document.getElementById('analytics-mem');
if (el) el.textContent = a.memory_hits_total;
el = document.getElementById('analytics-dur');
if (el) el.textContent = a.session_duration_minutes + ' min';
}
function updatePulse(p) {
var el;
el = document.getElementById('pulse-overall');
if (el) el.textContent = p.overall_pct + '%';
el = document.getElementById('pulse-health');
if (el) {
el.textContent = p.health.toUpperCase();
el.className = 'nexus-health-badge nexus-health-' + p.health;
}
el = document.getElementById('pulse-cryst');
if (el) el.textContent = p.crystallizations_last_hour;
el = document.getElementById('pulse-api-indep');
if (el) el.textContent = p.api_independence_pct + '%';
el = document.getElementById('pulse-events');
if (el) el.textContent = p.total_events;
// Update pulse badge dot
var badge = document.getElementById('nexus-pulse-badge');
if (badge) {
var dot = badge.querySelector('.nexus-pulse-dot');
if (dot) {
dot.className = 'nexus-pulse-dot nexus-pulse-' + p.health;
}
}
// Update layer bars
var meters = document.getElementById('nexus-pulse-meters');
if (meters && p.layers) {
var html = '';
for (var i = 0; i < p.layers.length; i++) {
var l = p.layers[i];
html += '<div class="nexus-pulse-layer">'
+ '<div class="nexus-pulse-layer-label">' + escHtml(l.name.toUpperCase()) + '</div>'
+ '<div class="nexus-pulse-bar-track">'
+ '<div class="nexus-pulse-bar-fill" style="width:' + l.sovereign_pct + '%"></div>'
+ '</div>'
+ '<div class="nexus-pulse-layer-pct">' + l.sovereign_pct + '%</div>'
+ '</div>';
}
meters.innerHTML = html;
}
}
function escHtml(s) {
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
connect();
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
"""Nexus subsystem — Timmy's sovereign conversational awareness space.
Extends the Nexus v1 chat interface with:
- **Introspection engine** — real-time cognitive state, thought-stream
integration, and session analytics surfaced directly in the Nexus.
- **Persistent sessions** — SQLite-backed conversation history that
survives process restarts.
- **Sovereignty pulse** — a live dashboard-within-dashboard showing
Timmy's sovereignty health, crystallization rate, and API independence.
"""
from timmy.nexus.introspection import NexusIntrospector # noqa: F401
from timmy.nexus.persistence import NexusStore # noqa: F401
from timmy.nexus.sovereignty_pulse import SovereigntyPulse # noqa: F401

View File

@@ -0,0 +1,236 @@
"""Nexus Introspection Engine — cognitive self-awareness for Timmy.
Aggregates live signals from the CognitiveTracker, ThinkingEngine, and
MemorySystem into a unified introspection snapshot. The Nexus template
renders this as an always-visible cognitive state panel so the operator
can observe Timmy's inner life in real time.
Design principles:
- Read-only observer — never mutates cognitive state.
- Graceful degradation — if any upstream is unavailable, the snapshot
still returns with partial data instead of crashing.
- JSON-serializable — every method returns plain dicts ready for
WebSocket push or Jinja2 template rendering.
Refs: #1090 (Nexus Epic), architecture-v2.md §Intelligence Surface
"""
from __future__ import annotations
import logging
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
logger = logging.getLogger(__name__)
# ── Data models ──────────────────────────────────────────────────────────────
@dataclass
class CognitiveSummary:
"""Distilled view of Timmy's current cognitive state."""
mood: str = "settled"
engagement: str = "idle"
focus_topic: str | None = None
conversation_depth: int = 0
active_commitments: list[str] = field(default_factory=list)
last_initiative: str | None = None
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class ThoughtSummary:
"""Compact representation of a single thought for the Nexus viewer."""
id: str
content: str
seed_type: str
created_at: str
parent_id: str | None = None
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class SessionAnalytics:
"""Conversation-level analytics for the active Nexus session."""
total_messages: int = 0
user_messages: int = 0
assistant_messages: int = 0
avg_response_length: float = 0.0
topics_discussed: list[str] = field(default_factory=list)
session_start: str | None = None
session_duration_minutes: float = 0.0
memory_hits_total: int = 0
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class IntrospectionSnapshot:
"""Everything the Nexus template needs to render the cognitive panel."""
cognitive: CognitiveSummary = field(default_factory=CognitiveSummary)
recent_thoughts: list[ThoughtSummary] = field(default_factory=list)
analytics: SessionAnalytics = field(default_factory=SessionAnalytics)
timestamp: str = field(
default_factory=lambda: datetime.now(UTC).isoformat()
)
def to_dict(self) -> dict:
return {
"cognitive": self.cognitive.to_dict(),
"recent_thoughts": [t.to_dict() for t in self.recent_thoughts],
"analytics": self.analytics.to_dict(),
"timestamp": self.timestamp,
}
# ── Introspector ─────────────────────────────────────────────────────────────
class NexusIntrospector:
"""Aggregates cognitive signals into a single introspection snapshot.
Lazily pulls from:
- ``timmy.cognitive_state.cognitive_tracker``
- ``timmy.thinking.thinking_engine``
- Nexus conversation log (passed in to avoid circular import)
"""
def __init__(self) -> None:
self._session_start: datetime | None = None
self._topics: list[str] = []
self._memory_hit_count: int = 0
# ── Public API ────────────────────────────────────────────────────────
def snapshot(
self,
conversation_log: list[dict] | None = None,
) -> IntrospectionSnapshot:
"""Build a complete introspection snapshot.
Parameters
----------
conversation_log:
The in-memory ``_nexus_log`` from the routes module (list of
dicts with ``role``, ``content``, ``timestamp`` keys).
"""
return IntrospectionSnapshot(
cognitive=self._read_cognitive(),
recent_thoughts=self._read_thoughts(),
analytics=self._compute_analytics(conversation_log or []),
)
def record_memory_hits(self, count: int) -> None:
"""Track cumulative memory hits for session analytics."""
self._memory_hit_count += count
def reset(self) -> None:
"""Reset session-scoped analytics (e.g. on history clear)."""
self._session_start = None
self._topics.clear()
self._memory_hit_count = 0
# ── Cognitive state reader ────────────────────────────────────────────
def _read_cognitive(self) -> CognitiveSummary:
"""Pull current state from the CognitiveTracker singleton."""
try:
from timmy.cognitive_state import cognitive_tracker
state = cognitive_tracker.get_state()
return CognitiveSummary(
mood=state.mood,
engagement=state.engagement,
focus_topic=state.focus_topic,
conversation_depth=state.conversation_depth,
active_commitments=list(state.active_commitments),
last_initiative=state.last_initiative,
)
except Exception as exc:
logger.debug("Introspection: cognitive state unavailable: %s", exc)
return CognitiveSummary()
# ── Thought stream reader ─────────────────────────────────────────────
def _read_thoughts(self, limit: int = 5) -> list[ThoughtSummary]:
"""Pull recent thoughts from the ThinkingEngine."""
try:
from timmy.thinking import thinking_engine
thoughts = thinking_engine.get_recent_thoughts(limit=limit)
return [
ThoughtSummary(
id=t.id,
content=(
t.content[:200] + "" if len(t.content) > 200 else t.content
),
seed_type=t.seed_type,
created_at=t.created_at,
parent_id=t.parent_id,
)
for t in thoughts
]
except Exception as exc:
logger.debug("Introspection: thought stream unavailable: %s", exc)
return []
# ── Session analytics ─────────────────────────────────────────────────
def _compute_analytics(
self, conversation_log: list[dict]
) -> SessionAnalytics:
"""Derive analytics from the Nexus conversation log."""
if not conversation_log:
return SessionAnalytics()
if self._session_start is None:
self._session_start = datetime.now(UTC)
user_msgs = [m for m in conversation_log if m.get("role") == "user"]
asst_msgs = [
m for m in conversation_log if m.get("role") == "assistant"
]
avg_len = 0.0
if asst_msgs:
total_chars = sum(len(m.get("content", "")) for m in asst_msgs)
avg_len = total_chars / len(asst_msgs)
# Extract topics from user messages (simple: first 40 chars)
topics = []
seen: set[str] = set()
for m in user_msgs:
topic = m.get("content", "")[:40].strip()
if topic and topic.lower() not in seen:
topics.append(topic)
seen.add(topic.lower())
# Keep last 8 topics
topics = topics[-8:]
elapsed = (datetime.now(UTC) - self._session_start).total_seconds() / 60
return SessionAnalytics(
total_messages=len(conversation_log),
user_messages=len(user_msgs),
assistant_messages=len(asst_msgs),
avg_response_length=round(avg_len, 1),
topics_discussed=topics,
session_start=self._session_start.strftime("%H:%M:%S"),
session_duration_minutes=round(elapsed, 1),
memory_hits_total=self._memory_hit_count,
)
# ── Module singleton ─────────────────────────────────────────────────────────
nexus_introspector = NexusIntrospector()

View File

@@ -0,0 +1,230 @@
"""Nexus Session Persistence — durable conversation history.
The v1 Nexus kept conversations in a Python ``list`` that vanished on
every process restart. This module provides a SQLite-backed store so
Nexus conversations survive reboots while remaining fully local.
Schema:
nexus_messages(id, role, content, timestamp, session_tag)
Design decisions:
- One table, one DB file (``data/nexus.db``). Cheap, portable, sovereign.
- ``session_tag`` enables future per-operator sessions (#1090 deferred scope).
- Bounded history: ``MAX_MESSAGES`` rows per session tag. Oldest are pruned
automatically on insert.
- Thread-safe via SQLite WAL mode + module-level singleton.
Refs: #1090 (Nexus Epic — session persistence), architecture-v2.md §Data Layer
"""
from __future__ import annotations
import logging
import sqlite3
from contextlib import closing
from datetime import UTC, datetime
from pathlib import Path
from typing import TypedDict
logger = logging.getLogger(__name__)
# ── Defaults ─────────────────────────────────────────────────────────────────
_DEFAULT_DB_DIR = Path("data")
DB_PATH: Path = _DEFAULT_DB_DIR / "nexus.db"
MAX_MESSAGES = 500 # per session tag
DEFAULT_SESSION_TAG = "nexus"
# ── Schema ───────────────────────────────────────────────────────────────────
_SCHEMA = """\
CREATE TABLE IF NOT EXISTS nexus_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TEXT NOT NULL,
session_tag TEXT NOT NULL DEFAULT 'nexus'
);
CREATE INDEX IF NOT EXISTS idx_nexus_session ON nexus_messages(session_tag);
CREATE INDEX IF NOT EXISTS idx_nexus_ts ON nexus_messages(timestamp);
"""
# ── Typed dict for rows ──────────────────────────────────────────────────────
class NexusMessage(TypedDict):
id: int
role: str
content: str
timestamp: str
session_tag: str
# ── Store ────────────────────────────────────────────────────────────────────
class NexusStore:
"""SQLite-backed persistence for Nexus conversations.
Usage::
store = NexusStore() # uses module-level DB_PATH
store.append("user", "hi")
msgs = store.get_history() # → list[NexusMessage]
store.clear() # wipe session
"""
def __init__(self, db_path: Path | None = None) -> None:
self._db_path = db_path or DB_PATH
self._conn: sqlite3.Connection | None = None
# ── Connection management ─────────────────────────────────────────────
def _get_conn(self) -> sqlite3.Connection:
if self._conn is None:
self._db_path.parent.mkdir(parents=True, exist_ok=True)
self._conn = sqlite3.connect(
str(self._db_path),
check_same_thread=False,
)
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.executescript(_SCHEMA)
return self._conn
def close(self) -> None:
"""Close the underlying connection (idempotent)."""
if self._conn is not None:
try:
self._conn.close()
except Exception:
pass
self._conn = None
# ── Write ─────────────────────────────────────────────────────────────
def append(
self,
role: str,
content: str,
*,
timestamp: str | None = None,
session_tag: str = DEFAULT_SESSION_TAG,
) -> int:
"""Insert a message and return its row id.
Automatically prunes oldest messages when the session exceeds
``MAX_MESSAGES``.
"""
ts = timestamp or datetime.now(UTC).strftime("%H:%M:%S")
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"INSERT INTO nexus_messages (role, content, timestamp, session_tag) "
"VALUES (?, ?, ?, ?)",
(role, content, ts, session_tag),
)
row_id: int = cur.lastrowid # type: ignore[assignment]
conn.commit()
# Prune
self._prune(session_tag)
return row_id
def _prune(self, session_tag: str) -> None:
"""Remove oldest rows that exceed MAX_MESSAGES for *session_tag*."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT COUNT(*) FROM nexus_messages WHERE session_tag = ?",
(session_tag,),
)
count = cur.fetchone()[0]
if count > MAX_MESSAGES:
excess = count - MAX_MESSAGES
cur.execute(
"DELETE FROM nexus_messages WHERE id IN ("
" SELECT id FROM nexus_messages "
" WHERE session_tag = ? ORDER BY id ASC LIMIT ?"
")",
(session_tag, excess),
)
conn.commit()
# ── Read ──────────────────────────────────────────────────────────────
def get_history(
self,
session_tag: str = DEFAULT_SESSION_TAG,
limit: int = 200,
) -> list[NexusMessage]:
"""Return the most recent *limit* messages for *session_tag*.
Results are ordered oldest-first (ascending id).
"""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT id, role, content, timestamp, session_tag "
"FROM nexus_messages "
"WHERE session_tag = ? "
"ORDER BY id DESC LIMIT ?",
(session_tag, limit),
)
rows = cur.fetchall()
# Reverse to chronological order
messages: list[NexusMessage] = [
NexusMessage(
id=r["id"],
role=r["role"],
content=r["content"],
timestamp=r["timestamp"],
session_tag=r["session_tag"],
)
for r in reversed(rows)
]
return messages
def message_count(
self, session_tag: str = DEFAULT_SESSION_TAG
) -> int:
"""Return total message count for *session_tag*."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"SELECT COUNT(*) FROM nexus_messages WHERE session_tag = ?",
(session_tag,),
)
return cur.fetchone()[0]
# ── Delete ────────────────────────────────────────────────────────────
def clear(self, session_tag: str = DEFAULT_SESSION_TAG) -> int:
"""Delete all messages for *session_tag*. Returns count deleted."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute(
"DELETE FROM nexus_messages WHERE session_tag = ?",
(session_tag,),
)
deleted: int = cur.rowcount
conn.commit()
return deleted
def clear_all(self) -> int:
"""Delete every message across all session tags."""
conn = self._get_conn()
with closing(conn.cursor()) as cur:
cur.execute("DELETE FROM nexus_messages")
deleted: int = cur.rowcount
conn.commit()
return deleted
# ── Module singleton ─────────────────────────────────────────────────────────
nexus_store = NexusStore()

View File

@@ -0,0 +1,153 @@
"""Sovereignty Pulse — real-time sovereignty health for the Nexus.
Reads from the ``SovereigntyMetricsStore`` (created in PR #1331) and
distils it into a compact "pulse" that the Nexus template can render
as a persistent health badge.
The pulse answers one question at a glance: *how sovereign is Timmy
right now?*
Signals:
- Overall sovereignty percentage (0100).
- Per-layer breakdown (perception, decision, narration).
- Crystallization velocity — new rules learned in the last hour.
- API independence — percentage of recent inferences served locally.
- Health rating (sovereign / degraded / dependent).
All methods return plain dicts — no imports leak into the template layer.
Refs: #953 (Sovereignty Loop), #954 (metrics), #1090 (Nexus epic)
"""
from __future__ import annotations
import logging
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
logger = logging.getLogger(__name__)
# ── Data model ───────────────────────────────────────────────────────────────
@dataclass
class LayerPulse:
"""Sovereignty metrics for a single AI layer."""
name: str
sovereign_pct: float = 0.0
cache_hits: int = 0
model_calls: int = 0
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class SovereigntyPulseSnapshot:
"""Complete sovereignty health reading for the Nexus display."""
overall_pct: float = 0.0
health: str = "unknown" # sovereign | degraded | dependent | unknown
layers: list[LayerPulse] = field(default_factory=list)
crystallizations_last_hour: int = 0
api_independence_pct: float = 0.0
total_events: int = 0
timestamp: str = field(
default_factory=lambda: datetime.now(UTC).isoformat()
)
def to_dict(self) -> dict:
return {
"overall_pct": self.overall_pct,
"health": self.health,
"layers": [layer.to_dict() for layer in self.layers],
"crystallizations_last_hour": self.crystallizations_last_hour,
"api_independence_pct": self.api_independence_pct,
"total_events": self.total_events,
"timestamp": self.timestamp,
}
# ── Pulse reader ─────────────────────────────────────────────────────────────
def _classify_health(pct: float) -> str:
"""Map overall sovereignty percentage to a human-readable health label."""
if pct >= 80.0:
return "sovereign"
if pct >= 50.0:
return "degraded"
if pct > 0.0:
return "dependent"
return "unknown"
class SovereigntyPulse:
"""Reads sovereignty metrics and emits pulse snapshots.
Lazily imports from ``timmy.sovereignty.metrics`` so the Nexus
module has no hard compile-time dependency on the Sovereignty Loop.
"""
def snapshot(self) -> SovereigntyPulseSnapshot:
"""Build a pulse snapshot from the live metrics store."""
try:
return self._read_metrics()
except Exception as exc:
logger.debug("SovereigntyPulse: metrics unavailable: %s", exc)
return SovereigntyPulseSnapshot()
def _read_metrics(self) -> SovereigntyPulseSnapshot:
"""Internal reader — allowed to raise if imports fail."""
from timmy.sovereignty.metrics import get_metrics_store
store = get_metrics_store()
snap = store.get_snapshot()
# Parse per-layer stats from the snapshot
layers = []
layer_pcts: list[float] = []
for layer_name in ("perception", "decision", "narration"):
layer_data = snap.get(layer_name, {})
hits = layer_data.get("cache_hits", 0)
calls = layer_data.get("model_calls", 0)
total = hits + calls
pct = (hits / total * 100) if total > 0 else 0.0
layers.append(
LayerPulse(
name=layer_name,
sovereign_pct=round(pct, 1),
cache_hits=hits,
model_calls=calls,
)
)
layer_pcts.append(pct)
overall = round(sum(layer_pcts) / len(layer_pcts), 1) if layer_pcts else 0.0
# Crystallization count
cryst = snap.get("crystallizations", 0)
# API independence: cache_hits / total across all layers
total_hits = sum(layer.cache_hits for layer in layers)
total_calls = sum(layer.model_calls for layer in layers)
total_all = total_hits + total_calls
api_indep = round((total_hits / total_all * 100), 1) if total_all > 0 else 0.0
total_events = snap.get("total_events", 0)
return SovereigntyPulseSnapshot(
overall_pct=overall,
health=_classify_health(overall),
layers=layers,
crystallizations_last_hour=cryst,
api_independence_pct=api_indep,
total_events=total_events,
)
# ── Module singleton ─────────────────────────────────────────────────────────
sovereignty_pulse = SovereigntyPulse()

View File

@@ -2665,25 +2665,27 @@
}
.vs-btn-save:hover { opacity: 0.85; }
/* ── Nexus ────────────────────────────────────────────────── */
.nexus-layout { max-width: 1400px; margin: 0 auto; }
/* ── Nexus v2 ─────────────────────────────────────────────── */
.nexus-layout { max-width: 1600px; margin: 0 auto; }
.nexus-header { border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
.nexus-title { font-size: 1.4rem; font-weight: 700; color: var(--purple); letter-spacing: 0.1em; }
.nexus-subtitle { font-size: 0.8rem; color: var(--text-dim); margin-top: 0.2rem; }
.nexus-grid {
/* v2 grid: wider sidebar for awareness panels */
.nexus-grid-v2 {
display: grid;
grid-template-columns: 1fr 320px;
grid-template-columns: 1fr 360px;
gap: 1rem;
align-items: start;
}
@media (max-width: 900px) {
.nexus-grid { grid-template-columns: 1fr; }
@media (max-width: 1000px) {
.nexus-grid-v2 { grid-template-columns: 1fr; }
}
.nexus-chat-panel { height: calc(100vh - 180px); display: flex; flex-direction: column; }
.nexus-chat-panel .card-body { overflow-y: auto; flex: 1; }
.nexus-msg-count { font-size: 0.7rem; color: var(--text-dim); letter-spacing: 0.05em; }
.nexus-empty-state {
color: var(--text-dim);
@@ -2693,6 +2695,177 @@
text-align: center;
}
/* Sidebar scrollable on short screens */
.nexus-sidebar-col { max-height: calc(100vh - 140px); overflow-y: auto; }
/* ── Sovereignty Pulse Badge (header) ── */
.nexus-pulse-badge {
display: flex;
align-items: center;
gap: 0.4rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 0.3rem 0.7rem;
font-size: 0.72rem;
letter-spacing: 0.05em;
}
.nexus-pulse-dot {
width: 8px; height: 8px;
border-radius: 50%;
display: inline-block;
}
.nexus-pulse-dot.nexus-pulse-sovereign { background: var(--green); box-shadow: 0 0 6px var(--green); }
.nexus-pulse-dot.nexus-pulse-degraded { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
.nexus-pulse-dot.nexus-pulse-dependent { background: var(--red); box-shadow: 0 0 6px var(--red); }
.nexus-pulse-dot.nexus-pulse-unknown { background: var(--text-dim); }
.nexus-pulse-label { color: var(--text-dim); }
.nexus-pulse-value { color: var(--text-bright); font-weight: 600; }
/* ── Cognitive State Panel ── */
.nexus-cognitive-panel .card-body { font-size: 0.78rem; }
.nexus-engagement-badge {
font-size: 0.65rem;
letter-spacing: 0.08em;
padding: 0.15rem 0.5rem;
border-radius: 3px;
background: rgba(168,85,247,0.12);
color: var(--purple);
}
.nexus-cog-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.nexus-cog-item {
background: rgba(255,255,255,0.02);
border-radius: 4px;
padding: 0.35rem 0.5rem;
}
.nexus-cog-label {
font-size: 0.62rem;
color: var(--text-dim);
letter-spacing: 0.08em;
margin-bottom: 0.15rem;
}
.nexus-cog-value {
color: var(--text-bright);
font-size: 0.8rem;
}
.nexus-cog-focus {
font-size: 0.72rem;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
}
.nexus-commitments { font-size: 0.72rem; }
.nexus-commitment-item {
color: var(--text);
padding: 0.2rem 0;
border-bottom: 1px solid rgba(59,26,92,0.4);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Thought Stream Panel ── */
.nexus-thoughts-panel .card-body { max-height: 200px; overflow-y: auto; }
.nexus-thought-item {
border-left: 2px solid var(--purple);
padding: 0.3rem 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.76rem;
background: rgba(168,85,247,0.04);
border-radius: 0 4px 4px 0;
}
.nexus-thought-meta {
display: flex;
justify-content: space-between;
margin-bottom: 0.2rem;
}
.nexus-thought-seed {
color: var(--purple);
font-size: 0.65rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.nexus-thought-time { color: var(--text-dim); font-size: 0.62rem; }
.nexus-thought-content { color: var(--text); line-height: 1.4; }
/* ── Sovereignty Pulse Detail Panel ── */
.nexus-health-badge {
font-size: 0.62rem;
letter-spacing: 0.08em;
padding: 0.15rem 0.5rem;
border-radius: 3px;
}
.nexus-health-sovereign { background: rgba(0,232,122,0.12); color: var(--green); }
.nexus-health-degraded { background: rgba(255,184,0,0.12); color: var(--amber); }
.nexus-health-dependent { background: rgba(255,68,85,0.12); color: var(--red); }
.nexus-health-unknown { background: rgba(107,74,138,0.12); color: var(--text-dim); }
.nexus-pulse-layer {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.35rem;
font-size: 0.72rem;
}
.nexus-pulse-layer-label {
color: var(--text-dim);
min-width: 80px;
letter-spacing: 0.06em;
font-size: 0.65rem;
}
.nexus-pulse-bar-track {
flex: 1;
height: 6px;
background: rgba(59,26,92,0.5);
border-radius: 3px;
overflow: hidden;
}
.nexus-pulse-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--purple), var(--green));
border-radius: 3px;
transition: width 0.6s ease;
}
.nexus-pulse-layer-pct {
color: var(--text-bright);
font-size: 0.68rem;
min-width: 36px;
text-align: right;
}
.nexus-pulse-stats { font-size: 0.72rem; }
.nexus-pulse-stat {
display: flex;
justify-content: space-between;
padding: 0.2rem 0;
border-bottom: 1px solid rgba(59,26,92,0.3);
}
.nexus-pulse-stat-label { color: var(--text-dim); }
.nexus-pulse-stat-value { color: var(--text-bright); }
/* ── Session Analytics Panel ── */
.nexus-analytics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.4rem;
font-size: 0.72rem;
}
.nexus-analytics-item {
display: flex;
justify-content: space-between;
padding: 0.25rem 0.4rem;
background: rgba(255,255,255,0.02);
border-radius: 4px;
}
.nexus-analytics-label { color: var(--text-dim); }
.nexus-analytics-value { color: var(--text-bright); }
/* Memory sidebar */
.nexus-memory-hits { font-size: 0.78rem; }
.nexus-memory-label { color: var(--text-dim); font-size: 0.72rem; margin-bottom: 0.4rem; letter-spacing: 0.05em; }

View File

@@ -1,4 +1,4 @@
"""Tests for the Nexus conversational awareness routes."""
"""Tests for the Nexus v2 conversational awareness routes."""
from unittest.mock import patch
@@ -24,6 +24,41 @@ def test_nexus_page_contains_teach_form(client):
assert "/nexus/teach" in response.text
def test_nexus_page_contains_cognitive_panel(client):
"""Nexus v2 page must include the cognitive state panel."""
response = client.get("/nexus")
assert response.status_code == 200
assert "COGNITIVE STATE" in response.text
def test_nexus_page_contains_thought_stream(client):
"""Nexus v2 page must include the thought stream panel."""
response = client.get("/nexus")
assert response.status_code == 200
assert "THOUGHT STREAM" in response.text
def test_nexus_page_contains_sovereignty_pulse(client):
"""Nexus v2 page must include the sovereignty pulse panel."""
response = client.get("/nexus")
assert response.status_code == 200
assert "SOVEREIGNTY PULSE" in response.text
def test_nexus_page_contains_session_analytics(client):
"""Nexus v2 page must include the session analytics panel."""
response = client.get("/nexus")
assert response.status_code == 200
assert "SESSION ANALYTICS" in response.text
def test_nexus_page_contains_websocket_script(client):
"""Nexus v2 page must include the WebSocket connection script."""
response = client.get("/nexus")
assert response.status_code == 200
assert "/nexus/ws" in response.text
def test_nexus_chat_empty_message_returns_empty(client):
"""POST /nexus/chat with blank message returns empty response."""
response = client.post("/nexus/chat", data={"message": " "})
@@ -72,3 +107,17 @@ def test_nexus_clear_history(client):
response = client.request("DELETE", "/nexus/history")
assert response.status_code == 200
assert "cleared" in response.text.lower()
def test_nexus_introspect_api(client):
"""GET /nexus/introspect should return JSON introspection snapshot."""
response = client.get("/nexus/introspect")
assert response.status_code == 200
data = response.json()
assert "introspection" in data
assert "sovereignty_pulse" in data
assert "cognitive" in data["introspection"]
assert "recent_thoughts" in data["introspection"]
assert "analytics" in data["introspection"]
assert "overall_pct" in data["sovereignty_pulse"]
assert "health" in data["sovereignty_pulse"]

View File

View File

@@ -0,0 +1,199 @@
"""Tests for the Nexus Introspection Engine."""
from unittest.mock import MagicMock, patch
from timmy.nexus.introspection import (
CognitiveSummary,
IntrospectionSnapshot,
NexusIntrospector,
SessionAnalytics,
ThoughtSummary,
)
# ── Data model tests ─────────────────────────────────────────────────────────
class TestCognitiveSummary:
def test_defaults(self):
s = CognitiveSummary()
assert s.mood == "settled"
assert s.engagement == "idle"
assert s.focus_topic is None
def test_to_dict(self):
s = CognitiveSummary(mood="curious", engagement="deep", focus_topic="architecture")
d = s.to_dict()
assert d["mood"] == "curious"
assert d["engagement"] == "deep"
assert d["focus_topic"] == "architecture"
class TestThoughtSummary:
def test_to_dict(self):
t = ThoughtSummary(
id="t1", content="Hello world", seed_type="freeform", created_at="2026-01-01"
)
d = t.to_dict()
assert d["id"] == "t1"
assert d["seed_type"] == "freeform"
assert d["parent_id"] is None
class TestSessionAnalytics:
def test_defaults(self):
a = SessionAnalytics()
assert a.total_messages == 0
assert a.avg_response_length == 0.0
assert a.topics_discussed == []
class TestIntrospectionSnapshot:
def test_to_dict_structure(self):
snap = IntrospectionSnapshot()
d = snap.to_dict()
assert "cognitive" in d
assert "recent_thoughts" in d
assert "analytics" in d
assert "timestamp" in d
def test_to_dict_with_data(self):
snap = IntrospectionSnapshot(
cognitive=CognitiveSummary(mood="energized"),
recent_thoughts=[
ThoughtSummary(id="x", content="test", seed_type="s", created_at="now"),
],
)
d = snap.to_dict()
assert d["cognitive"]["mood"] == "energized"
assert len(d["recent_thoughts"]) == 1
# ── Introspector tests ───────────────────────────────────────────────────────
class TestNexusIntrospector:
def test_snapshot_empty_log(self):
intro = NexusIntrospector()
snap = intro.snapshot(conversation_log=[])
assert isinstance(snap, IntrospectionSnapshot)
assert snap.analytics.total_messages == 0
def test_snapshot_with_messages(self):
intro = NexusIntrospector()
log = [
{"role": "user", "content": "hello", "timestamp": "10:00:00"},
{"role": "assistant", "content": "Hi there!", "timestamp": "10:00:01"},
{"role": "user", "content": "architecture question", "timestamp": "10:00:02"},
]
snap = intro.snapshot(conversation_log=log)
assert snap.analytics.total_messages == 3
assert snap.analytics.user_messages == 2
assert snap.analytics.assistant_messages == 1
assert snap.analytics.avg_response_length > 0
def test_record_memory_hits(self):
intro = NexusIntrospector()
intro.record_memory_hits(3)
intro.record_memory_hits(2)
snap = intro.snapshot(
conversation_log=[{"role": "user", "content": "x", "timestamp": "t"}]
)
assert snap.analytics.memory_hits_total == 5
def test_reset_clears_state(self):
intro = NexusIntrospector()
intro.record_memory_hits(10)
intro.reset()
snap = intro.snapshot(
conversation_log=[{"role": "user", "content": "x", "timestamp": "t"}]
)
assert snap.analytics.memory_hits_total == 0
def test_topics_deduplication(self):
intro = NexusIntrospector()
log = [
{"role": "user", "content": "hello", "timestamp": "t"},
{"role": "user", "content": "hello", "timestamp": "t"},
{"role": "user", "content": "different topic", "timestamp": "t"},
]
snap = intro.snapshot(conversation_log=log)
assert len(snap.analytics.topics_discussed) == 2
def test_topics_capped_at_8(self):
intro = NexusIntrospector()
log = [{"role": "user", "content": f"topic {i}", "timestamp": "t"} for i in range(15)]
snap = intro.snapshot(conversation_log=log)
assert len(snap.analytics.topics_discussed) <= 8
def test_cognitive_read_fallback(self):
"""If cognitive read fails, snapshot still works with defaults."""
intro = NexusIntrospector()
# Patch the module-level import inside _read_cognitive
with patch.dict("sys.modules", {"timmy.cognitive_state": None}):
snap = intro.snapshot(conversation_log=[])
# Should not raise — fallback to default
assert snap.cognitive.mood == "settled"
def test_thoughts_read_fallback(self):
"""If thought read fails, snapshot still works with empty list."""
intro = NexusIntrospector()
with patch.dict("sys.modules", {"timmy.thinking": None}):
snap = intro.snapshot(conversation_log=[])
assert snap.recent_thoughts == []
def test_read_cognitive_from_tracker(self):
intro = NexusIntrospector()
mock_state = MagicMock()
mock_state.mood = "curious"
mock_state.engagement = "deep"
mock_state.focus_topic = "sovereignty"
mock_state.conversation_depth = 5
mock_state.active_commitments = ["build something"]
mock_state.last_initiative = "build something"
mock_tracker = MagicMock()
mock_tracker.get_state.return_value = mock_state
with patch("timmy.cognitive_state.cognitive_tracker", mock_tracker):
summary = intro._read_cognitive()
assert summary.mood == "curious"
assert summary.engagement == "deep"
assert summary.focus_topic == "sovereignty"
assert summary.conversation_depth == 5
def test_read_thoughts_from_engine(self):
intro = NexusIntrospector()
mock_thought = MagicMock()
mock_thought.id = "t1"
mock_thought.content = "Deep thought about sovereignty"
mock_thought.seed_type = "existential"
mock_thought.created_at = "2026-03-23T10:00:00"
mock_thought.parent_id = None
mock_engine = MagicMock()
mock_engine.get_recent_thoughts.return_value = [mock_thought]
with patch("timmy.thinking.thinking_engine", mock_engine):
thoughts = intro._read_thoughts(limit=5)
assert len(thoughts) == 1
assert thoughts[0].id == "t1"
assert thoughts[0].seed_type == "existential"
def test_read_thoughts_truncates_long_content(self):
intro = NexusIntrospector()
mock_thought = MagicMock()
mock_thought.id = "t2"
mock_thought.content = "x" * 300
mock_thought.seed_type = "freeform"
mock_thought.created_at = "2026-03-23"
mock_thought.parent_id = None
mock_engine = MagicMock()
mock_engine.get_recent_thoughts.return_value = [mock_thought]
with patch("timmy.thinking.thinking_engine", mock_engine):
thoughts = intro._read_thoughts(limit=5)
assert len(thoughts[0].content) <= 201 # 200 + "…"

View File

@@ -0,0 +1,144 @@
"""Tests for the Nexus Session Persistence store."""
import pytest
from timmy.nexus.persistence import MAX_MESSAGES, NexusStore
@pytest.fixture
def store(tmp_path):
"""Provide a NexusStore backed by a temp database."""
db = tmp_path / "test_nexus.db"
s = NexusStore(db_path=db)
yield s
s.close()
class TestNexusStoreBasic:
def test_append_and_retrieve(self, store):
store.append("user", "hello")
store.append("assistant", "hi there")
history = store.get_history()
assert len(history) == 2
assert history[0]["role"] == "user"
assert history[0]["content"] == "hello"
assert history[1]["role"] == "assistant"
def test_message_count(self, store):
assert store.message_count() == 0
store.append("user", "a")
store.append("user", "b")
assert store.message_count() == 2
def test_custom_timestamp(self, store):
store.append("user", "msg", timestamp="12:34:56")
history = store.get_history()
assert history[0]["timestamp"] == "12:34:56"
def test_clear_session(self, store):
store.append("user", "a")
store.append("assistant", "b")
deleted = store.clear()
assert deleted == 2
assert store.message_count() == 0
def test_clear_empty_session(self, store):
deleted = store.clear()
assert deleted == 0
def test_clear_all(self, store):
store.append("user", "a", session_tag="s1")
store.append("user", "b", session_tag="s2")
deleted = store.clear_all()
assert deleted == 2
assert store.message_count(session_tag="s1") == 0
assert store.message_count(session_tag="s2") == 0
class TestNexusStoreOrdering:
def test_chronological_order(self, store):
for i in range(5):
store.append("user", f"msg-{i}")
history = store.get_history()
contents = [m["content"] for m in history]
assert contents == ["msg-0", "msg-1", "msg-2", "msg-3", "msg-4"]
def test_limit_parameter(self, store):
for i in range(10):
store.append("user", f"msg-{i}")
history = store.get_history(limit=3)
assert len(history) == 3
# Should be the 3 most recent
assert history[0]["content"] == "msg-7"
assert history[2]["content"] == "msg-9"
class TestNexusStoreSessionTags:
def test_session_isolation(self, store):
store.append("user", "nexus-msg", session_tag="nexus")
store.append("user", "other-msg", session_tag="other")
nexus_history = store.get_history(session_tag="nexus")
other_history = store.get_history(session_tag="other")
assert len(nexus_history) == 1
assert len(other_history) == 1
assert nexus_history[0]["content"] == "nexus-msg"
def test_clear_only_affects_target_session(self, store):
store.append("user", "a", session_tag="s1")
store.append("user", "b", session_tag="s2")
store.clear(session_tag="s1")
assert store.message_count(session_tag="s1") == 0
assert store.message_count(session_tag="s2") == 1
class TestNexusStorePruning:
def test_prune_excess_messages(self, tmp_path):
"""Inserting beyond MAX_MESSAGES should prune oldest."""
db = tmp_path / "prune_test.db"
s = NexusStore(db_path=db)
# Insert MAX_MESSAGES + 5 to trigger pruning
for i in range(MAX_MESSAGES + 5):
s.append("user", f"msg-{i}")
assert s.message_count() == MAX_MESSAGES
# Get full history — oldest remaining should be msg-5
history = s.get_history(limit=MAX_MESSAGES)
assert history[0]["content"] == "msg-5"
s.close()
class TestNexusStoreReopen:
def test_data_survives_close_reopen(self, tmp_path):
"""Data persists across store instances (simulates process restart)."""
db = tmp_path / "reopen.db"
s1 = NexusStore(db_path=db)
s1.append("user", "persistent message")
s1.close()
s2 = NexusStore(db_path=db)
history = s2.get_history()
assert len(history) == 1
assert history[0]["content"] == "persistent message"
s2.close()
class TestNexusStoreReturnedId:
def test_append_returns_row_id(self, store):
id1 = store.append("user", "first")
id2 = store.append("user", "second")
assert isinstance(id1, int)
assert id2 > id1
class TestNexusStoreClose:
def test_close_is_idempotent(self, store):
store.close()
store.close() # Should not raise
def test_operations_after_close_reconnect(self, store):
"""After close, next operation should reconnect automatically."""
store.append("user", "before close")
store.close()
# Should auto-reconnect
store.append("user", "after close")
assert store.message_count() == 2

View File

@@ -0,0 +1,151 @@
"""Tests for the Sovereignty Pulse module."""
from unittest.mock import MagicMock, patch
from timmy.nexus.sovereignty_pulse import (
LayerPulse,
SovereigntyPulse,
SovereigntyPulseSnapshot,
_classify_health,
)
class TestClassifyHealth:
def test_sovereign(self):
assert _classify_health(95.0) == "sovereign"
assert _classify_health(80.0) == "sovereign"
def test_degraded(self):
assert _classify_health(79.9) == "degraded"
assert _classify_health(50.0) == "degraded"
def test_dependent(self):
assert _classify_health(49.9) == "dependent"
assert _classify_health(0.1) == "dependent"
def test_unknown(self):
assert _classify_health(0.0) == "unknown"
class TestLayerPulse:
def test_to_dict(self):
lp = LayerPulse(name="perception", sovereign_pct=75.0, cache_hits=15, model_calls=5)
d = lp.to_dict()
assert d["name"] == "perception"
assert d["sovereign_pct"] == 75.0
assert d["cache_hits"] == 15
class TestSovereigntyPulseSnapshot:
def test_defaults(self):
snap = SovereigntyPulseSnapshot()
assert snap.overall_pct == 0.0
assert snap.health == "unknown"
assert snap.layers == []
def test_to_dict_structure(self):
snap = SovereigntyPulseSnapshot(
overall_pct=85.0,
health="sovereign",
layers=[LayerPulse(name="perception", sovereign_pct=90.0)],
crystallizations_last_hour=3,
api_independence_pct=88.0,
total_events=42,
)
d = snap.to_dict()
assert d["overall_pct"] == 85.0
assert d["health"] == "sovereign"
assert len(d["layers"]) == 1
assert d["layers"][0]["name"] == "perception"
assert d["crystallizations_last_hour"] == 3
assert d["api_independence_pct"] == 88.0
assert d["total_events"] == 42
assert "timestamp" in d
class TestSovereigntyPulse:
def test_snapshot_graceful_degradation(self):
"""When metrics are unavailable, should return default snapshot."""
pulse = SovereigntyPulse()
with patch.object(
pulse,
"_read_metrics",
side_effect=ImportError("no metrics"),
):
snap = pulse.snapshot()
assert isinstance(snap, SovereigntyPulseSnapshot)
assert snap.health == "unknown"
def test_snapshot_with_metrics(self):
"""When metrics are available, should read and compute correctly."""
pulse = SovereigntyPulse()
mock_snapshot = {
"perception": {"cache_hits": 8, "model_calls": 2},
"decision": {"cache_hits": 6, "model_calls": 4},
"narration": {"cache_hits": 10, "model_calls": 0},
"crystallizations": 7,
"total_events": 100,
}
mock_store = MagicMock()
mock_store.get_snapshot.return_value = mock_snapshot
with patch(
"timmy.sovereignty.metrics.get_metrics_store", return_value=mock_store
):
snap = pulse.snapshot()
# Perception: 8/10 = 80%, Decision: 6/10 = 60%, Narration: 10/10 = 100%
# Overall: (80 + 60 + 100) / 3 = 80.0
assert len(snap.layers) == 3
assert snap.layers[0].name == "perception"
assert snap.layers[0].sovereign_pct == 80.0
assert snap.layers[1].name == "decision"
assert snap.layers[1].sovereign_pct == 60.0
assert snap.layers[2].name == "narration"
assert snap.layers[2].sovereign_pct == 100.0
assert snap.overall_pct == 80.0
assert snap.health == "sovereign"
assert snap.crystallizations_last_hour == 7
assert snap.total_events == 100
def test_api_independence_calculation(self):
pulse = SovereigntyPulse()
mock_snapshot = {
"perception": {"cache_hits": 5, "model_calls": 5},
"decision": {"cache_hits": 5, "model_calls": 5},
"narration": {"cache_hits": 5, "model_calls": 5},
"crystallizations": 0,
"total_events": 0,
}
mock_store = MagicMock()
mock_store.get_snapshot.return_value = mock_snapshot
with patch(
"timmy.sovereignty.metrics.get_metrics_store", return_value=mock_store
):
snap = pulse.snapshot()
# Total hits: 15, Total calls: 15, Total: 30
# Independence: 15/30 = 50%
assert snap.api_independence_pct == 50.0
def test_zero_events_no_division_error(self):
pulse = SovereigntyPulse()
mock_snapshot = {
"perception": {"cache_hits": 0, "model_calls": 0},
"decision": {"cache_hits": 0, "model_calls": 0},
"narration": {"cache_hits": 0, "model_calls": 0},
"crystallizations": 0,
"total_events": 0,
}
mock_store = MagicMock()
mock_store.get_snapshot.return_value = mock_snapshot
with patch(
"timmy.sovereignty.metrics.get_metrics_store", return_value=mock_store
):
snap = pulse.snapshot()
assert snap.overall_pct == 0.0
assert snap.api_independence_pct == 0.0
assert snap.health == "unknown"