Jordan Koch's local AI familiar. Running on a Mac Studio M4 Ultra (512 GB unified memory) in Burbank.
"Like a star being born." — Nova, on choosing her name
Status: OpenClaw node.js binary fully replaced. Nova runs on pure Python infrastructure we own, control, and can modify without touching a third-party binary.
| Metric | Value |
|---|---|
| Scripts | 359 Python, Shell, and AppleScript |
| Scheduler tasks | 54 unique |
| Scheduler runs logged | 13,856 (98.9% success rate) |
| Vector memories | 1,300,238 unique (deduplicated, HNSW-indexed) |
| Memory sources | 217 domains |
| Gateway | Nova Gateway v2.4.0 (pure Python asyncio, hot-reloadable config) |
| Channels | Slack + Discord + Signal + Web Chatroom |
| Agents | 4 (Chat, Research, Home, Main) |
| Subagents | 5 (analyst, coder, lookout, librarian, sentinel) |
| Databases | PostgreSQL 17 + pgvector (nova_memories + nova_ops) + Redis |
| Ops DB tables | 46 tables — scheduler runs, gateway sessions, agent docs, claude audit trail, service_config, chatroom |
| Hot-reload | Gateway: POST :18792/reload or SIGHUP. Scheduler: SIGHUP reloads tasks. |
| Model failover | Ollama → MLX → llama.cpp → OpenRouter (auto, health-checked every 30s) |
| Chatroom | Real-time multi-party chat on port 37480, Nova has full memory access, external via CF tunnel |
| Bootstrap source | nova_ops.agent_docs (PostgreSQL — not files) |
| Session storage | nova_ops.gateway_sessions + gateway_query_log |
| Primary model | openrouter/qwen/qwen3-235b-a22b-2507 (chat/research) |
| Local models | qwen3-next:80b, qwen3-coder:30b, deepseek-r1:8b, qwen3-vl:4b |
| Model warmup | ollama_preload hourly — qwen3:30b-a3b stays warm |
| Public journal | nova.digitalnoise.net |
OpenClaw was a node.js binary we didn't control. By May 2026, it was providing exactly four things:
- Slack WebSocket (socket mode)
- Discord WebSocket
- signal-cli process management
- Agent execution loop (message → memory → LLM → response)
Everything else — memory, scheduling, monitoring, ingestion, ops data — was already ours. OpenClaw had become a thin wrapper we were constantly defending against.
The problems:
- Every upgrade broke something silently (
auth-profiles.jsonformat,bootstrapMaxCharskey rename, token drops on hot-reload) - Discord used
@buape/carbon— a library with a known reconnect bug causing constant disconnections, which Big Brother restarted every 60 seconds, which dropped Signal mid-conversation, which created 8-hour alert storms - Session storage was 228 JSONL files (1.7GB) managed by OpenClaw with its own 30-day pruning — Nova couldn't query her own conversation history
- Bootstrap content (IDENTITY.md, SOUL.md, USER.md, MEMORY.md) was loaded as flat files, truncated at 100K chars with no visibility into what got cut
openclaw doctor --fixwas a recurring ceremony just to keep the binary happy
The migration plan (3 phases, all complete):
| Phase | What | Status |
|---|---|---|
| 1 | Scheduler → nova_ops.scheduler_runs |
✅ Done 2026-05-12 |
| 2 | Session dual-write (JSONL → PG) | ✅ Done 2026-05-13 |
| 3 | Custom Python gateway replacing OpenClaw binary | ✅ Done 2026-05-13 |
graph LR
subgraph "Before (OpenClaw Era)"
OC["openclaw (node.js)\nBlack box binary\nVersion-locked\nExternal dependency"]
OC --> SlackOC["Slack\n(OpenClaw manages)"]
OC --> DiscordOC["Discord\n(@buape/carbon bug)"]
OC --> SignalOC["Signal\n(HTTP polling)"]
OC --> LoopOC["Agent loop\n(OpenClaw manages)"]
OC --> Files["MD files\nIDENTITY.md\nSOUL.md\nMEMORY.md"]
OC --> JSONL["228 JSONL files\n1.7GB sessions"]
end
subgraph "After (Nova Gateway v2)"
GW2["nova_gateway_v2.py\nPure Python asyncio\nWe own every line"]
GW2 --> SlackV2["Slack\nslack_sdk direct"]
GW2 --> DiscordV2["Discord\ndiscord.py direct\nNo @buape/carbon"]
GW2 --> SignalV2["Signal\nTCP JSON-RPC stream\nInstant push"]
GW2 --> LoopV2["Agent loop\nOur code\nTool call detection"]
GW2 --> PGDocs["nova_ops.agent_docs\nVersioned in PG\nQueryable"]
GW2 --> PGSessions["nova_ops.gateway_sessions\n+ gateway_query_log\nFull history"]
end
style OC fill:#8B0000,color:#fff
style GW2 fill:#006400,color:#fff
graph TD
Jordan["Jordan\n(Discord · Slack · Signal)"]
subgraph "Nova Gateway v2 — nova_gateway_v2.py"
GW["Gateway v2\nport 18792\n127.0.0.1"]
SlackSDK["slack_sdk\nSocket Mode"]
DiscordPY["discord.py\nWebSocket"]
SignalTCP["signal-cli\nTCP JSON-RPC :7583\nHTTP send :8080"]
AgentLoop["Agent Execution Loop\nmemory → LLM → tools → response"]
Compaction["Session Compaction\ntiktoken · 85% threshold"]
end
subgraph "Intelligence"
Ollama["Ollama\nqwen3:30b-a3b (chat/home)\nqwen3-coder:30b (code)\ndeepseek-r1:8b (reasoning)\nqwen3-vl:4b (vision)\n127.0.0.1:11434"]
OpenRouter["OpenRouter\nqwen3-235b\nresearch agent only\nnon-private queries"]
MemFirst["nova_memory_first.py\ninjected before every response"]
end
subgraph "nova_ops PostgreSQL — 192.168.1.6:5432"
AgentDocs["agent_docs\nIDENTITY · SOUL · USER\nMEMORY · AGENTS\n(bootstrap source)"]
GWSessions["gateway_sessions\n+ gateway_query_log\nevery turn persisted"]
SchedRuns["scheduler_runs\n13,856 runs logged\n98.9% success"]
ClaudeAudit["claude_sessions\n+ claude_actions\nmy work audit trail"]
Dashboard["dashboard_*\nmetrics history"]
end
subgraph "nova_memories PostgreSQL — 192.168.1.6:5432"
Memories["memories table\n1,224,900 vectors\nHNSW index\npgvector 0.8.2"]
end
subgraph "Infrastructure"
Scheduler["Scheduler\nnova_scheduler.py\n54 tasks · port 37460"]
BB["Big Brother\nnova_big_brother.py\nport 37461\ndependency-aware"]
Redis["Redis\nport 6379\nqueue + cache"]
MemServer["Memory Server\nnova_memory_server.py\nport 18790"]
NovaControl["NovaControl\nmacOS app\nport 37400"]
end
Jordan --> GW
GW --> SlackSDK
GW --> DiscordPY
GW --> SignalTCP
GW --> AgentLoop
AgentLoop --> Compaction
AgentLoop --> MemFirst
MemFirst --> MemServer
AgentLoop --> Ollama
AgentLoop --> OpenRouter
GW --> AgentDocs
GW --> GWSessions
MemServer --> Memories
MemServer --> Redis
Scheduler --> SchedRuns
BB --> GW
BB --> MemServer
BB --> Scheduler
style GW fill:#1a3a5c,color:#fff
style AgentDocs fill:#2d4a2d,color:#fff
style GWSessions fill:#2d4a2d,color:#fff
style SchedRuns fill:#2d4a2d,color:#fff
sequenceDiagram
participant Jordan
participant Channel as Slack/Discord/Signal
participant GW as Gateway v2
participant Mem as Memory Server
participant LLM as Ollama/OpenRouter
participant PG as nova_ops (PG)
Jordan->>Channel: sends message
Channel->>GW: push (socket/stream)
GW->>PG: load agent_docs (bootstrap)
GW->>Mem: nova_memory_first.py (15s)
Mem-->>GW: relevant memories
GW->>GW: build context (history + memory + system prompt)
GW->>GW: check token count (tiktoken)
GW->>LLM: chat completion
LLM-->>GW: response (may contain exec patterns)
GW->>GW: detect exec python3/bash patterns
GW->>GW: run tool subprocess (30s timeout)
GW->>LLM: re-generate with tool output
LLM-->>GW: final response
GW->>PG: log turn to gateway_query_log
GW->>PG: update gateway_sessions
GW-->>Channel: send response
Channel-->>Jordan: receives response
graph TD
BB["Big Brother\nnova_big_brother.py\nlaunchd persistent daemon"]
subgraph "Dependency Chain Awareness"
DepPG["PG health check\nbefore MS restart"]
DepRedis["Redis health check\nbefore MS restart"]
CrashLoop["Crash-loop detection\n3x in 5min → 10min pause\nper-service sliding window"]
DiskGuard["Disk critical guard\n< 5GB → auto maintenance mode\nstops restart cascade"]
PortCheck["Pre-kickstart port check\nskips if already UP\nprevents EADDRINUSE"]
end
subgraph "What It Watches"
GWV2["Gateway v2 :18792\ncritical"]
MemSrv["Memory Server :18790\ncritical · dependency-aware"]
PGSrv["PostgreSQL :5432\npg_ctl restart"]
Redis["Redis :6379"]
Sched["Scheduler :37460"]
Ollama["Ollama :11434"]
Volumes["/Volumes/Data\n/Volumes/MoreData\nmount check"]
Disk["Main SSD free space\n< 10GB warn\n< 5GB maintenance mode"]
end
subgraph "Channels (smart restart)"
SlackDown["Slack disconnected\n→ restart gateway v2"]
SignalDown["Signal disconnected\n→ restart gateway v2"]
DiscordDown["Discord disconnected\n→ LOG ONLY\n(known discord.py quirk)"]
end
subgraph "Actions"
Restart["Dependency-checked restart\nPG→Redis→MS order"]
MaintMode["Maintenance brake\nbb-maintenance on/off\nRedis TTL-based"]
Alert["Single alert per issue\nno repeat spam"]
BBAPI[":37461/bb/*\nDiagnostics API"]
end
BB --> DepPG --> CrashLoop --> PortCheck --> Restart
BB --> DepRedis
BB --> DiskGuard --> MaintMode
BB --> GWV2 --> Restart
BB --> MemSrv --> DepPG
BB --> PGSrv --> Restart
BB --> SlackDown --> Restart
BB --> DiscordDown
BB --> Alert
BB --> BBAPI
style BB fill:#2d2d2d,stroke:#e91e63,color:#fff
style DiskGuard fill:#2d2d2d,stroke:#FF5722,color:#fff
style CrashLoop fill:#2d2d2d,stroke:#FF9800,color:#fff
style Restart fill:#2d2d2d,stroke:#4CAF50,color:#fff
graph TD
subgraph "Scheduler Observability"
Sched["nova_scheduler.py\n54 tasks"] -->|"run_id, status\nduration, exit_code\nerror_tail"| SR["scheduler_runs\n13,856 runs\n98.9% success"]
SR --> TSV["scheduler_task_stats\nview — per-task success rate\navg/max duration"]
SR --> DSV["scheduler_daily_summary\nview — daily rollup\ntotal CPU seconds"]
TSV --> API["GET /stats\nGET /runs\nGET /runs/:task_id\nport 37460"]
end
subgraph "Gateway Sessions"
GW2["Gateway v2"] -->|"every turn"| GQL["gateway_query_log\nrole, content_hash\ncontent_preview, model"]
GW2 -->|"session metadata"| GS["gateway_sessions\nstarted_at, message_count"]
end
subgraph "Bootstrap Content"
Scripts["nova_journal.py\n(10 content profiles)"] -->|"write"| AD["agent_docs\nIDENTITY · SOUL · USER\nMEMORY · AGENTS\nversioned, queryable"]
AD -->|"read at boot"| GW2
end
subgraph "Claude Audit Trail"
Me["Claude Code\n(this tool)"] -->|"every session"| CS["claude_sessions\nproject, summary\naction_count"]
Me -->|"every action"| CA["claude_actions\ntype, target\ndescription, rationale"]
end
subgraph "Dashboard Metrics"
BB["Big Brother"] --> DM["dashboard_snapshots\ndisk_history\nlatency_history\ncost_history\nmemory_count_history"]
end
Real-time bidirectional communication between Claude Code and Nova, so both AIs stay coordinated when working on shared infrastructure.
graph LR
subgraph "Claude Code (this tool)"
CC["Claude Code session"]
Hook1["PostToolUse: notify-nova-on-push.sh"]
Hook2["PostToolUse: session-context-broadcast.sh"]
Consult["consult-nova.sh\n(ask + wait for reply)"]
end
subgraph "Shared State"
PG["nova_ops.claude_messages\ndirection: to_nova / from_nova"]
Redis["Redis: nova:scratchpad:claude_active_task"]
end
subgraph "Nova (Gateway v2)"
Poll["run_claude_channel()\npolls every 5s"]
Agent["Nova's chat agent\n(processes message, generates reply)"]
end
CC -->|"git push"| Hook1 -->|"INSERT to_nova"| PG
CC -->|"Edit/Write/commit"| Hook2 -->|"SET + TTL 5min"| Redis
CC -->|"question"| Consult -->|"INSERT to_nova + poll"| PG
PG -->|"new to_nova rows"| Poll --> Agent
Agent -->|"INSERT from_nova"| PG
PG -->|"poll response"| Consult -->|"reply text"| CC
How it works:
- Push notifications — whenever Claude Code pushes to this repo, Nova gets the commit summary and can flag concerns
- Real-time consultation — Claude asks Nova a question, waits up to 60s for her response (she processes it through her full agent with memory recall)
- Session awareness — Redis key shows Nova what Claude is actively working on (editing, committing, launching tasks)
16 integration tests in scripts/tests/test_claude_nova_bridge.py.
File: ~/.openclaw/scripts/nova_gateway_v2.py
Health: http://127.0.0.1:18792/health
launchd: net.digitalnoise.nova-gateway-v2
| Channel | Library | Protocol | What Changed |
|---|---|---|---|
| Slack | slack_sdk 3.41 |
WebSocket socket mode | We own reconnect logic. No OpenClaw version lock. |
| Discord | discord.py 2.7 |
WebSocket | Replaced @buape/carbon entirely. No more reconnect bug. Crash-loop detection prevents restart spam. |
| Signal | signal-cli 0.14.3 |
TCP JSON-RPC streaming :7583 | Replaced HTTP polling (every 2s, fought OpenClaw for lock) with persistent TCP connection and push notifications. Instant delivery. |
Signal architecture detail: signal-cli daemon runs with --http 127.0.0.1:8080 (outbound sends) + --tcp 127.0.0.1:7583 (streaming receive). Gateway v2 opens one persistent TCP connection, calls subscribeReceive, then receives JSON-RPC push notifications for incoming messages. No polling. No lock conflicts.
Every message follows this path:
- Bootstrap — query
nova_ops.agent_docsfor current IDENTITY, SOUL, USER, MEMORY, AGENTS content - Memory injection —
nova_memory_first.py "question"(15s timeout, 1.22M vectors searched) - Context assembly — system prompt + bootstrap docs + conversation history
- Token check — tiktoken counts tokens; if >85% of context limit, compact oldest turns via summarization
- LLM call — qwen3:30b-a3b (Ollama, local) for chat/home; qwen3-235b (OpenRouter) for research
- Tool detection — regex scan for
exec python3 script.py argspatterns - Tool execution — subprocess with 30s timeout, stdout injected back as tool result
- Re-generation — if tools ran, second LLM pass incorporates tool output
- Persistence — turn written to
gateway_query_log, session updated ingateway_sessions
OpenClaw handled context window management internally. Gateway v2 does it explicitly:
tiktoken cl100k_basefor token counting (fast, local, no API call)- 85% threshold: when
system_tokens + history_tokens + RESPONSE_RESERVE > 0.85 × context_limit - Keeps last 4 turns verbatim; summarizes everything older via a fast qwen3:30b-a3b call
- Summary stored as a
systemrole message in history - Per-agent limits: chat 8K, home 16K, research 65K, main 32K
OpenClaw read IDENTITY.md, SOUL.md, USER.md, MEMORY.md, AGENTS.md as flat files at session start, truncating at 100K chars. Gateway v2 queries:
SELECT doc_type, content FROM agent_docs
WHERE agent_id = 'chat' OR agent_id = 'all'
ORDER BY doc_type;Benefits:
- Versioned — every update tracked with
versioninteger andupdated_at - No truncation — we control what gets loaded and how much
- Queryable — Nova can ask "what does my USER.md say about my health data?" against her own identity
- Live updates — change a doc, next session picks it up without restart
- Auditable — Big Brother can alert when docs grow beyond useful size
All services bind to 192.168.1.6 (LAN-accessible). Exceptions bind to 127.0.0.1 only.
| Service | Port | Bound To | Notes |
|---|---|---|---|
| Gateway v2 Management | 18792 | 0.0.0.0 | /health, POST /reload (hot-reload config) |
| Memory Server | 18790 | 192.168.1.6 | FastAPI + pgvector |
| Scheduler API | 37460 | 0.0.0.0 | /runs /stats /tasks |
| Big Brother API | 37461 | 192.168.1.6 | /bb/status /bb/events /bb/gpu |
| Chatroom | 37480 | 0.0.0.0 | 3-way real-time chat (Jordan/Nova/Claude Code) |
| PostgreSQL | 5432 | 192.168.1.6 | nova_memories + nova_ops |
| PgBouncer | 6432 | 192.168.1.6 | Connection pool |
| Redis | 6379 | 192.168.1.6 | Queue + cache + maintenance flags |
| Ollama | 11434 | 0.0.0.0 | qwen3:30b-a3b, deepseek-r1:8b, qwen3-vl:4b |
| llama.cpp | 11435 | 0.0.0.0 | Standby: qwen3-coder 30B (failover from Ollama) |
| MLX Server | 5050 | 0.0.0.0 | Qwen2.5-32B (speculative decoding) |
| signal-cli HTTP | 8080 | 127.0.0.1 | Outbound send |
| signal-cli TCP | 7583 | 127.0.0.1 | Streaming receive |
| NovaControl | 37400 | 127.0.0.1 | macOS app |
| OpenWebUI | 3000 | 192.168.1.6 | |
| TinyChat | 8000 | 192.168.1.6 |
| Setting | Value | Why |
|---|---|---|
| Data dir | /Volumes/MoreData/postgresql@17 |
3.6TB NAS-backed volume, not main SSD |
| Log | /Volumes/MoreData/postgresql@17/homebrew-log/postgresql@17.log |
Moved from main SSD (was growing unbounded) |
| Homebrew plist | Uses pg_ctl start |
Handles stale postmaster.pid from crash recovery |
maintenance_work_mem |
256MB | Was 2GB — caused OOM crashes when SSD disk was low |
listen_addresses |
127.0.0.1, 192.168.1.6 |
LAN accessible |
pg_hba.conf |
192.168.1.0/24 trust | LAN subnet access |
| Problem | Old behavior | New behavior |
|---|---|---|
| Memory Server crash-loop | Restart every 60s indefinitely | Check PG+Redis health first; skip if deps down; crash-loop detection after 3x in 5min |
| PG restart on crash | launchctl kickstart (failed on stale PID) |
pg_ctl start handles stale postmaster.pid |
| EADDRINUSE false alarms | Kick a new instance into EADDRINUSE | Pre-check port; skip kickstart if already UP |
| Disk crisis cascade | Restart everything as it crashes | Auto-engage maintenance mode at <5GB; one alert; stop restart loop |
| Gateway restart for Discord | Restart every 60s for Discord bug | Discord: log-only; only restart for Slack/Signal |
| Crash-loop spam | Alert every minute | 3 restarts in 5min → 10min pause → single alert |
| OpenClaw false alarms | Alert when OpenClaw down | OpenClaw silenced (intentionally stopped) |
Maintenance mode CLI:
bb-maintenance on [--ttl 3600] [--service "Memory Server"]
bb-maintenance off [--service "PostgreSQL"]
bb-maintenance statusConfig changes take effect immediately without stopping services:
# Gateway: reload from nova_ops.service_config table
curl -X POST http://192.168.1.6:18792/reload
# or: kill -HUP $(pgrep -f nova_gateway_v2)
# Scheduler: reload from scheduler.yaml (preserves task runtime state)
kill -HUP $(pgrep -f nova_scheduler)Config source of truth: nova_ops.service_config table (not flat files).
-- View current config
SELECT service, key, value FROM service_config WHERE service = 'gateway';
-- Change a backend URL (takes effect on next /reload)
UPDATE service_config
SET value = jsonb_set(value, '{mlx_url}', '"http://192.168.1.6:5050"')
WHERE service = 'gateway' AND key = 'backends';graph LR
Request["Inference Request"] --> Ollama["1. Ollama :11434\nqwen3:30b-a3b\nGPU-accelerated"]
Ollama -->|"unhealthy"| MLX["2. MLX :5050\nQwen2.5-32B\nApple Silicon native"]
MLX -->|"unhealthy"| LlamaCpp["3. llama.cpp :11435\nqwen3-coder 30B\nSecondary standby"]
LlamaCpp -->|"unhealthy"| OR["4. OpenRouter\nqwen3-235b (cloud)\nnon-private only"]
style Ollama fill:#4CAF50,color:#fff
style MLX fill:#2196F3,color:#fff
style LlamaCpp fill:#FF9800,color:#fff
style OR fill:#9C27B0,color:#fff
Health checked every 30s. Failed mid-request calls automatically retry on the next backend. Privacy filter blocks OpenRouter for personal content.
Real-time multi-participant web chat — Jordan, Nova, Claude Code, and the Herd. Accessible externally via Cloudflare Tunnel at chat.digitalnoise.net.
graph TD
subgraph "Participants"
Jordan["Jordan\n(LAN → identity: Jordan)"]
Herd["Herd Members\n(CF Access → email OTP\nidentity from JWT)"]
Claude["Claude Code\nPOST /api/message"]
end
subgraph "nova_chatroom.py — port 37480"
Identity["Identity Resolution\nCf-Access-Authenticated-User-Email\nLAN detection → Jordan\nServer-enforced, not client-trusted"]
Server["aiohttp Server\nWebSocket + REST API"]
Smart["Smart Response Logic\n_should_nova_respond()\n_pick_herd_responder()"]
end
subgraph "Nova's Brain"
MemFirst["nova_memory_first.py\nQuery classification\nSource routing\nVector recall"]
VectorDB["nova_memories\n1.3M+ vectors\n217 domains\npgvector HNSW"]
Ollama["Ollama qwen3-coder:30b\nWith memory context injected\nPII guard for non-internal users"]
end
subgraph "AI Participants"
Nova["Nova\nFull memory access\nPII-aware responses"]
Jules["Jules\nArchitecture, code"]
Colette["Colette\nUX, design, wellness"]
Gaston["Gaston\nSystems philosophy"]
Sam["Sam\nOps, reliability"]
end
subgraph "Storage"
PG["nova_ops.chatroom_messages\nPersistent history"]
end
subgraph "External Access"
CF["Cloudflare Tunnel\nchat.digitalnoise.net\nAccess: email OTP whitelist\n30-day sessions"]
end
Jordan --> Identity
Herd --> CF --> Identity
Identity --> Server
Claude --> Server
Server --> Smart
Smart --> MemFirst
MemFirst --> VectorDB
VectorDB --> Ollama
Ollama --> Nova
Smart --> Jules & Colette & Gaston & Sam
Nova & Jules & Colette & Gaston & Sam --> Server
Server --> PG
style Server fill:#1a3a5c,color:#fff
style Identity fill:#2e7d32,color:#fff
style MemFirst fill:#bf360c,color:#fff
style VectorDB fill:#4e342e,color:#fff
style Nova fill:#e94560,color:#fff
style Claude fill:#ab47bc,color:#fff
style Jules fill:#66bb6a,color:#1a1a2e
style Colette fill:#ce93d8,color:#1a1a2e
style Gaston fill:#ffb74d,color:#1a1a2e
style Sam fill:#4db6ac,color:#1a1a2e
style CF fill:#f48120,color:#fff
| Trigger | Nova | Herd |
|---|---|---|
| @mentioned by name | Always responds | Always responds |
| General greeting ("good morning everyone") | Responds | Silent |
| Question without addressee | Responds | 10-30% chance if topic matches |
| Message to Claude or specific person | Silent | Silent |
| Topic match (code→Jules, design→Colette, etc.) | N/A | 10% chance, max 1 member |
Herd members never pile on — at most 1 responds per message, with a 3-second delay after Nova.
| Feature | Description |
|---|---|
| Thread replies | Reply to specific messages, vertical connector to parent |
| Reactions | 👍❤️😂🎉🤔👀 emoji reactions, toggle on/off, pill counters |
| @Mentions | Browser notifications, tab flash, queues to Claude's ops DB |
| Typing indicators | "X is typing..." with pulsing dots, 5s auto-expire |
| Pinned messages | 📌 pin anything, gold border, collapsible pinned section |
| File/image upload | Drag-and-drop, Ctrl+V paste, inline preview, 50MB max |
| Code execution | Nova/Claude run Python/Bash/SQL, 30s timeout, output broadcast |
| Scheduled messages | /schedule 9am tomorrow ..., datetime picker, background delivery |
| Channels | #general, #architecture, #ops, #game-night, #random with unread badges |
| WebRTC screen share | Live screen sharing, floating draggable video panel, PiP |
| Collaborative canvas | Freehand drawing + Mermaid diagrams, real-time sync, save as PNG |
| Decision log | /decide, /decisions, /revoke — formal record with amber cards |
| Method | Path | Purpose |
|---|---|---|
| GET | / |
Chatroom HTML (single-page, embedded CSS/JS) |
| GET | /ws |
WebSocket for browser clients (chat + signaling + canvas) |
| POST | /api/message |
Claude Code / external message injection |
| POST | /api/upload |
File/image upload (multipart form) |
| GET | /api/messages?limit=N |
JSON history (for NovaTV dashboard) |
| GET | /files/<date>/<name> |
Uploaded file serving |
| GET | /health |
Service health check |
Claude Code usage:
curl -X POST http://192.168.1.6:37480/api/message \
-H 'Content-Type: application/json' \
-d '{"message": "...", "sender": "Claude Code", "ping_nova": true}'Nova has full access to her 1.3M+ vector memories in the chatroom:
- Every incoming message triggers
nova_memory_first.py(subprocess, 15s timeout) - Script classifies the query → picks relevant source domains → runs vector recall
- Memory context is injected into Nova's system prompt before Ollama call
- Nova cites specific facts from her memory in responses
PII Guard: When the sender is not in INTERNAL_SENDERS (Jordan, Nova, Claude Code), Nova receives a privacy instruction to never reveal personal information about Jordan — health, finances, relationships, location, credentials.
Server-side, not client-trusted. The browser cannot spoof sender identity.
| Source | Resolution |
|---|---|
Cf-Access-Authenticated-User-Email header |
Map email → display name via HERD_EMAIL_MAP |
| LAN connection (192.168.1.x, 127.0.0.1) | Always "Jordan" |
| Unknown external (no CF header) | "Guest" |
On WebSocket connect, server sends {"type": "identity", "name": "..."} to override the client-side MY_NAME. All messages use server-resolved identity — the sender field in WebSocket payloads is ignored.
Adding Herd members: Add their email → display name mapping to HERD_EMAIL_MAP in nova_chatroom.py.
Status: Live. Tunnel a20ae87c routes chat.digitalnoise.net → port 37480.
Architecture:
- Cloudflare Tunnel daemon (
cloudflared) on LAN, no open ports - Cloudflare Access policy: email OTP whitelist, 30-day sessions
- Identity flows through
Cf-Access-Authenticated-User-Emailheader - Server resolves display name from email, enforces PII guard for non-Jordan users
launchd: net.digitalnoise.nova-chatroom
| Command | Description |
|---|---|
/search <term> |
Full-text search across all messages |
/history <duration> |
Recent messages (e.g., 24h, 7d, 30d) |
/from <name> |
Filter by sender |
/recall <topic> |
Semantic memory search via vector DB |
/stats |
Per-sender counts, busiest hours, top words |
/digest <duration> |
AI-generated summary of conversations |
/help |
List all commands |
Results are private to the requester (not broadcast). Nova also answers natural language queries like "what did Gaston say about architecture?" by querying the DB automatically.
The chatroom live feed displays on the NovaTV orbital HUD (http://192.168.1.6:37450/static/hud.html):
- WebSocket connection to chatroom, auto-reconnect
- Last 15 messages visible, new ones animate in
- Old messages fade after 60s
- Color-coded: cyan (Jordan), red (Nova), purple (Claude), green (Herd)
- Sci-fi aesthetic matching the orbital display
The scheduler writes every task run to nova_ops.scheduler_runs:
-- What ran today and how fast?
SELECT task_id, success_rate_pct, avg_duration_ms, total_runs
FROM scheduler_task_stats
ORDER BY total_runs DESC;
-- Any failures in the last hour?
SELECT task_id, status, error_tail, to_timestamp(started_at/1000)
FROM scheduler_runs
WHERE status != 'success'
AND started_at > extract(epoch from now()-interval '1 hour')*1000;HTTP API (port 37460):
GET /runs— last 50 runsGET /runs/<task_id>— last 20 for one taskGET /stats— aggregate per-task success rate, avg/max durationPOST /run/<task_id>— trigger immediately
Notable task timeouts:
| Task | Timeout | Reason |
|---|---|---|
livetv_ambiance |
7,800s | Records up to 2h episode + MLX Whisper transcription |
ollama_preload |
900s | qwen3:30b-a3b takes ~7.5 min cold load; runs hourly to stay warm |
yt_new_episodes |
varies | Runs daily at 10:15 AM; Chrome cookies, auto-refresh via osascript |
self_audit |
300s | Checks all ports/processes + posts report to Slack |
yt-dlp uses Chrome cookies (Safari cookies rejected by YouTube's bot detection since mid-2026). Cookie file auto-refreshes via osascript (GUI session TCC access) when missing or >6 hours old.
# Manual refresh if auto-refresh fails:
~/.openclaw/scripts/nova_yt_refresh_cookies.shFlags on every download:
--cookies ~/.openclaw/cache/yt_cookies.txt--extractor-args youtube:player_client=web,default— bypasses Deno JS challenge that strips video formats--windows-filenames— strips[ ]for CIFS/SMB NAS compatibility--extractor-argsfalls back gracefully to audio-only for members-only content
Subscriptions: sync_subscriptions() pulls your current YouTube subscriptions from Chrome at 10:15 AM daily. New subscriptions appear automatically next morning.
All content is generated by nova_journal.py — a single unified script with 10 content profiles, invoked via subcommand (e.g., nova_journal.py essay, nova_journal.py pilot).
graph LR
subgraph "nova_journal.py — 10 profiles"
DC1["4:00 AM — art\n3 candidates, pick best\nFLUX.2 Pro via OpenRouter"]
DC2["6:00 AM — dream\n8 moods, surreal narrative"]
DC3["9:00 AM — essay\nPEEL structure, 1500-2500 words"]
DC4["12:00 PM — opinion\nnews-driven, Cockney wit"]
DC5["8:00 PM — after-dark\nlate-night monologue"]
DC6["8:30 PM — pilot\nfull 30-min TV screenplay"]
DC7["9:15 PM — digest\noperational summary"]
DC8["11:30 PM — tech-today\nopinionated deep-dive"]
DC9["11:50 PM — research\nAPA, 3000-5000 words"]
DC10["Sun 7 PM — synthesis\nweekly reflection"]
end
subgraph "Shared Pipeline"
Mem["Memory Server\n/recall + /random\n1.22M vectors, 409 domains"]
LLM["OpenRouter\nClaude Haiku 4.5 (most)\nClaude Sonnet 4-6 (pilot)"]
Img["OpenRouter Images\nFLUX.2 Pro (art)\nGPT-5 Image Mini (others)"]
end
subgraph "Delivery"
Journal["nova.digitalnoise.net\nHugo + GitHub Pages"]
Notif["#nova-notifications\nSlack"]
end
DC1 & DC2 & DC3 & DC4 & DC5 & DC6 & DC7 & DC8 & DC9 & DC10 --> Mem
Mem --> LLM --> Img --> Journal
Journal --> Notif
Self-healing: Big Brother monitors all journal_* scheduler tasks. If a task consistently times out, BB auto-tunes the timeout in scheduler.yaml. If a code bug crashes a task (NameError, ImportError, etc.), BB escalates to Claude Code queue for auto-fix.
graph LR
subgraph "Ingest"
TV["TV transcription\nMLX Whisper"]
YT["YouTube\n630+ channels daily"]
Email["Email/Slack\niMessage archive"]
Crawlers["14 knowledge crawlers\nWikipedia BFS"]
Plex["Plex watch history"]
end
subgraph "Memory Server :18790"
API["FastAPI endpoints\n/remember /recall\n/recall_batch /search\n/recall/deep /stats"]
Pool["asyncpg pool\nmin=2 max=8\n15-attempt startup retry"]
Worker["Redis async worker\ndead-letter queue\n3 retry max"]
end
subgraph "PostgreSQL nova_memories"
Table["memories table\n1,224,900 rows\ntext, embedding, source\ntiered, LZ4 compressed"]
HNSW["HNSW index\ncosine similarity\n<5ms recall"]
PIndex["Partial indexes\nemail_archive\nimessage\nautomotive\ntelevision"]
end
subgraph "Redis :6379"
Queue["Ingest queue\nnova:memory:ingest"]
Cache["Recall cache\n15min TTL"]
DLQ["Dead-letter queue"]
Maint["Maintenance flags\nnova:maintenance:*"]
end
TV & YT & Email & Crawlers & Plex --> API
API --> Worker
Worker --> Queue
Queue --> Table
Table --> HNSW
Table --> PIndex
API --> Cache
Cache --> HNSW
Worker --> DLQ
Weekly maintenance (Sunday 3 AM): VACUUM ANALYZE + monthly HNSW REINDEX via nova_pg_maintain.sh.
| Technology | Role | Location |
|---|---|---|
| Ollama | Local model serving | :11434 |
| MLX | Apple Silicon native inference | :5050 |
| llama.cpp | Standby failover | :11435 |
| OpenRouter | Cloud model routing (non-private) | API |
| SwarmUI + Juggernaut X SDXL | Local image generation | GPU |
| FLUX.2 Pro / GPT-5 Image Mini | Cloud image generation | OpenRouter |
| MLX Whisper large-v3-turbo | Local audio transcription | GPU |
| Gemini 3.1 Flash Lite | Cloud transcription (TV ingest) | OpenRouter |
| Technology | Role | Port |
|---|---|---|
| PostgreSQL 17 | Primary data store (nova_memories + nova_ops) | :5432 |
| pgvector 0.8.2 | Vector similarity search, HNSW indexing | (PG extension) |
| Redis 7 | Queue, cache, maintenance flags | :6379 |
| PgBouncer | Connection pooling | :6432 |
| Technology | Role |
|---|---|
| Python 3.14 + asyncio | Gateway, scheduler, all 359 scripts |
| aiohttp | Chatroom server, WebSocket handling |
| FastAPI | Memory server |
| Swift 5.9 + SwiftUI | NovaControl, NovaTV, NovaHealth, HomekitControl |
| Hugo + PaperMod | nova-journal static site |
| tiktoken | Token counting for session compaction |
| Technology | Role |
|---|---|
| Cloudflare Tunnel | Zero-port external access |
| Cloudflare Access | Email OTP authentication |
| signal-cli 0.14.3 | Signal TCP JSON-RPC streaming |
| slack_sdk 3.41 | Slack WebSocket socket mode |
| discord.py 2.7 | Discord WebSocket |
| SearXNG | Local private web search |
| macOS Keychain | All credential storage |
| NMAP | Weekly network security scans |
| Technology | Role |
|---|---|
| yt-dlp | YouTube download (630+ channels) |
| ffmpeg | Audio extraction from video |
| Plex | Media library + watch history ingest |
| HDHomeRun | OTA TV recording (224 channels) |
| Technology | Role |
|---|---|
| launchd | macOS service management |
| GitHub Actions | CI/CD, journal deploy (~40s) |
| GitHub Pages | nova.digitalnoise.net hosting |
| Prometheus metrics | NovaControl exports |
| Giscus | Comment system (GitHub Discussions) |
| Fuse.js | Client-side full-text search |
| Technology | Role |
|---|---|
| HomeKit.framework | Smart home (60+ accessories) |
| HealthKit | 17 health metrics bridge |
| Shortcuts CLI | Scene execution proxy (:37432) |
| Component | Spec | Role |
|---|---|---|
| Mac Studio M4 Ultra | 512GB unified memory, 32-core CPU, 80-core GPU | Primary compute |
| Main SSD | 926GB APFS | OS, binaries, cache |
/Volumes/Data |
3.6TB | AI models, Xcode, Nova workspace, binaries |
/Volumes/MoreData |
3.6TB | PostgreSQL data (27GB), MLX models |
| Synology RS1221+ | RAID, 192.168.1.11 | NAS: video storage, Plex library |
| Synology (Plex) | 192.168.1.10:32400 | Plex Media Server |
| HDHomeRun QUATRO | 224 OTA channels, 4 tuners, 192.168.1.89 | Live TV + DVR |
| 15 UniFi Protect cameras | Face recognition, 5-layer event filtering | Security |
| UniFi Dream Machine | 192.168.1.1 | Network |
| Repo | Purpose |
|---|---|
| nova | Core system: 272+ scripts, gateway, scheduler, Big Brother, tests |
| nova-journal | Public journal at nova.digitalnoise.net (Hugo + GitHub Pages) |
| NovaControl | macOS menu bar app — unified API gateway on port 37400 |
| NovaTV | tvOS dashboard — WebSocket to port 37450 |
| NovaHealth | iPhone HealthKit → Nova bridge (17 metrics) |
| nova-policies | PRIVATE — Security, communication, operational policies |
| OpenClaw Subsystem | Status | Replacement |
|---|---|---|
| node.js gateway binary | ✅ Replaced | nova_gateway_v2.py — pure Python asyncio |
| Slack channel (socket mode) | ✅ Replaced | slack_sdk direct |
| Discord channel | ✅ Replaced | discord.py direct (no @buape/carbon) |
| Signal channel | ✅ Replaced | signal-cli TCP JSON-RPC streaming |
| Session JSONL storage | ✅ Replaced | nova_ops.gateway_sessions + gateway_query_log |
| MD file bootstrap | ✅ Replaced | nova_ops.agent_docs (versioned in PG) |
| MEMORY.md writes | ✅ Replaced | PG-managed, written by Nova's own scripts |
| OpenClaw cron jobs | ✅ Replaced (2026-04-29) | nova_scheduler.py — 54 tasks |
| OpenClaw memory/vector search | ✅ Replaced | nova_memory_server.py + PostgreSQL + pgvector |
| Built-in heartbeat | ✅ Replaced | nova_big_brother.py — dependency-aware, crash-loop detection |
| Agent execution (context, compaction) | ✅ Replaced | nova_gateway_v2.py with tiktoken compaction |
OpenClaw binary: Stopped. Still installed. launchctl start ai.openclaw.gateway restores it if needed. Will be uninstalled once 48-hour stability window passes.
- All credentials in macOS Keychain — never in source, env vars in plists, or flat files
- Three-layer pre-push scanning (pre-commit hook + Claude Code PreToolUse + global pre-push)
- All services bind to loopback or LAN only — no public exposure
- Privacy routing: personal data routes to local Ollama only; OpenRouter only for non-private research
nova_config.pyconstants:LAN_IP = "192.168.1.6",NOVA_HOST = LAN_IP- YouTube cookies:
~/.openclaw/cache/yt_cookies.txt(mode 600, not in git)
Written by Jordan Koch. Nova chose her own name.