openclaw-nerve/docs/ARCHITECTURE.md
Kim AI c9ad1bd745
chore(release): prepare 1.5.2 (#201)
* chore(release): prepare 1.5.2

* docs(changelog): include kanban assignee picker in 1.5.2

* fix(sessions): preserve status timers across resubscribe

* fix(sessions): use latest gateway for delayed refreshes

* fix(sessions): refresh unknown sessions via latest gateway
2026-03-31 03:27:03 +03:00

44 KiB
Raw Permalink Blame History

Architecture

Nerve is a web interface for OpenClaw — chat, voice input, TTS, and agent monitoring in the browser. It connects to the OpenClaw gateway over WebSocket and provides a rich UI for interacting with AI agents.

System Diagram

┌──────────────────────────────────────────────────────────────────┐
│  Browser (React SPA)                                             │
│                                                                  │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  ┌────────────────┐  │
│  │ ChatPanel│  │ Sessions │  │ Workspace │  │ Command Palette│  │
│  └────┬─────┘  └────┬─────┘  └─────┬─────┘  └────────────────┘  │
│       │              │              │                             │
│  ┌────┴──────────────┴──────────────┴────────────────────────┐   │
│  │           React Contexts (Gateway, Session, Chat, Settings)│   │
│  └────────────────────────────┬──────────────────────────────┘   │
│                               │ WebSocket (/ws proxy)            │
└───────────────────────────────┼──────────────────────────────────┘
                                │
┌───────────────────────────────┼──────────────────────────────────┐
│  Nerve Server (Hono + Node)   │                                  │
│                               │                                  │
│  ┌────────────────────────────┴─────────────┐                    │
│  │         WebSocket Proxy (ws-proxy.ts)     │                   │
│  │  - Intercepts connect.challenge           │                   │
│  │  - Injects device identity (Ed25519)      │                   │
│  └────────────────────────────┬──────────────┘                   │
│                               │                                  │
│  ┌───────────────┐  ┌────────┴─────┐  ┌───────────────────────┐ │
│  │ REST API      │  │ SSE Stream   │  │ Static File Server    │ │
│  │ /api/*        │  │ /api/events  │  │ Vite build → dist/    │ │
│  └───────┬───────┘  └──────────────┘  └───────────────────────┘ │
│          │                                                       │
│  ┌───────┴──────────────────────────────────────────────────┐    │
│  │  Services: TTS (OpenAI, Replicate, Edge), Whisper,       │    │
│  │  Claude Usage, TTS Cache, Usage Tracker                  │    │
│  └──────────────────────────────────────────────────────────┘    │
└──────────────────────────────┬───────────────────────────────────┘
                               │ HTTP / WS
                    ┌──────────┴──────────┐
                    │  OpenClaw Gateway    │
                    │  (ws://127.0.0.1:    │
                    │       18789)         │
                    └─────────────────────┘

Frontend Structure

Built with React 19, TypeScript, Vite, and Tailwind CSS v4.

Entry Point

File Purpose
src/main.tsx Mounts the React tree: ErrorBoundary → StrictMode → AuthGate. The auth gate checks session status before rendering the app
src/App.tsx Root layout — wires contexts to lazy-loaded panels, manages keyboard shortcuts and command palette

Context Providers (State Management)

All global state flows through four React contexts, nested in dependency order:

Context File Responsibilities
GatewayContext src/contexts/GatewayContext.tsx WebSocket connection lifecycle, RPC method calls, event fan-out via pub/sub pattern, model/thinking status polling, activity sparkline
SettingsContext src/contexts/SettingsContext.tsx Sound, TTS provider/model, wake word, panel ratio, theme, font, font size, telemetry/events visibility. Persists to localStorage
SessionContext src/contexts/SessionContext.tsx Session list (via gateway RPC), granular agent status tracking (IDLE/THINKING/STREAMING/DONE/ERROR), busy state derivation, unread session tracking, agent log, event log, session CRUD (delete, spawn, rename, abort)
ChatContext src/contexts/ChatContext.tsx Thin orchestrator composing 4 hooks: useChatMessages (CRUD, history, scroll), useChatStreaming (deltas, processing stage, activity log), useChatRecovery (reconnect, retry, gap detection), useChatTTS (playback, voice fallback, sound feedback)

Data flow pattern: Contexts subscribe to gateway events via GatewayContext.subscribe(). The SessionContext listens for agent and chat events to update granular status. The ChatContext listens for streaming deltas and lifecycle events to render real-time responses.

Feature Modules

Each feature lives in src/features/<name>/ with its own components, hooks, types, and operations.

features/auth/

Authentication gate and login UI.

File Purpose
AuthGate.tsx Top-level component — shows loading spinner, login page, or the full app depending on auth state
LoginPage.tsx Full-screen password form matching Nerve's dark theme. Auto-focus, Enter-to-submit, gateway token hint
useAuth.ts Auth state via useSyncExternalStore. Module-level fetch checks /api/auth/status once on load. Exposes login/logout callbacks
index.ts Barrel export

features/chat/

The main chat interface.

File Purpose
ChatPanel.tsx Full chat view — message list with infinite scroll, input bar, streaming indicator, search
InputBar.tsx Text input with voice recording, image attachment, tab completion, input history
MessageBubble.tsx Renders individual messages (user, assistant, tool, system) with markdown
ToolCallBlock.tsx Renders tool call blocks with name, arguments, and results
DiffView.tsx Side-by-side diff rendering for file edits
FileContentView.tsx Syntax-highlighted file content display
ImageLightbox.tsx Full-screen image viewer
SearchBar.tsx In-chat message search (Cmd+F)
MemoriesSection.tsx Inline memory display within chat
edit-blocks.ts Parses edit/diff blocks from tool output
extractImages.ts Extracts image content blocks from messages
image-compress.ts Client-side image compression before upload
types.ts Chat-specific types (ChatMsg, ImageAttachment)
utils.ts Chat utility functions
useMessageSearch.ts Hook for message search filtering
operations/ Pure business logic (no React): loadHistory.ts, sendMessage.ts, streamEventHandler.ts
components/ Sub-components: ActivityLog, ChatHeader, HeartbeatPulse, ProcessingIndicator, ScrollToBottomButton, StreamingMessage, ThinkingDots, ToolGroupBlock, useModelEffort

features/sessions/

Session management sidebar.

File Purpose
SessionList.tsx Hierarchical session tree with parent-child relationships
SessionNode.tsx Individual session row with status indicator, context menu
SessionInfoPanel.tsx Session detail panel (model, tokens, thinking level)
SpawnAgentDialog.tsx Dialog for spawning top-level agents and subagents with task, model, thinking, and subagent After run cleanup config
sessionTree.ts Builds tree structure from flat session list using parentId
statusUtils.ts Maps agent status to icons and labels

features/file-browser/

Full workspace file browser with tabbed CodeMirror editor.

File Purpose
FileTreePanel.tsx Collapsible file tree sidebar with directory expand/collapse
FileTreeNode.tsx Individual file/directory row with icon and indent
EditorTabBar.tsx Tab bar for open files with close buttons
EditorTab.tsx Single editor tab with modified indicator
FileEditor.tsx CodeMirror 6 editor — syntax highlighting, line numbers, search, Cmd+S save
TabbedContentArea.tsx Manages chat/editor tab switching (chat never unmounts)
editorTheme.ts One Dark-inspired CodeMirror theme matching Nerve's dark aesthetic
hooks/useFileTree.ts File tree data fetching and directory toggle state
hooks/useOpenFiles.ts Open file tab management, save with mtime conflict detection
utils/fileIcons.tsx File extension → icon mapping
utils/languageMap.ts File extension → CodeMirror language extension mapping
types.ts Shared types (FileNode, OpenFile, etc.)

features/workspace/

Workspace file editor and management tabs.

The workspace scope is derived from the owning top-level agent. Memory, Config, Skills, file-browser state, and persisted drafts follow that top-level agent. Crons and Kanban stay global.

File Purpose
WorkspacePanel.tsx Container for workspace tabs, scoped by the current top-level workspace agent
WorkspaceTabs.tsx Tab switcher (Memory, Config, Crons, Skills)
tabs/MemoryTab.tsx View/edit the selected top-level agent's MEMORY.md and daily files
tabs/ConfigTab.tsx Edit scoped workspace files (SOUL.md, TOOLS.md, USER.md, etc.) for the selected top-level agent
tabs/CronsTab.tsx Cron job management (list, create, toggle, run). Remains global
tabs/CronDialog.tsx Cron creation/edit dialog
tabs/SkillsTab.tsx View installed skills for the selected top-level agent workspace
hooks/useWorkspaceFile.ts Fetch/save scoped workspace files via REST API + agentId
hooks/useCrons.ts Cron CRUD operations via REST API
hooks/useSkills.ts Fetch scoped skills list via agentId
workspaceScope.ts Derives workspace scope and localStorage keys from the owning top-level agent
workspaceSwitchGuard.ts Pure guard logic for dirty-file prompts when switching between top-level agents

Workspace scope rules

  • Root sessions use their own top-level agent as workspace scope
  • Subagent and cron-run views inherit the owning top-level agent workspace
  • The child session itself does not create a separate workspace scope
  • Cross-agent dirty file prompts only fire when the owning top-level agent changes
  • Crons and Kanban stay global even while Memory, Config, Skills, and file-browser state switch per agent

features/settings/

Settings drawer with tabbed sections.

File Purpose
SettingsDrawer.tsx Slide-out drawer container. Includes logout button when auth is enabled
ConnectionSettings.tsx Gateway URL/token, reconnect
AudioSettings.tsx TTS provider, model, voice, wake word
AppearanceSettings.tsx Theme, font family, font size selection

features/tts/

Text-to-speech integration.

File Purpose
useTTS.ts Core TTS hook, speaks text via server /api/tts endpoint. Supports OpenAI, Replicate, Edge (default), and Xiaomi MiMo providers
useTTSConfig.ts Server-side TTS voice configuration management for Qwen, OpenAI, Edge, and Xiaomi MiMo settings

features/voice/

Voice input and audio feedback.

File Purpose
useVoiceInput.ts Web Speech API integration for voice-to-text with Whisper fallback
audio-feedback.ts Notification sounds (ping on response complete)

features/markdown/

Markdown rendering pipeline.

File Purpose
MarkdownRenderer.tsx react-markdown with remark-gfm, syntax highlighting via highlight.js
CodeBlockActions.tsx Copy/run buttons on code blocks

features/charts/

Inline chart rendering with three renderers: TradingView widgets for live financial data, Lightweight Charts for custom time-series and candlestick data, and Recharts for bar/pie charts.

File Purpose
InlineChart.tsx Chart router — dispatches [chart:{...}] markers to the correct renderer based on type
extractCharts.ts Bracket-balanced parser for [chart:{...}] markers. Validates chart data by type (bar, line, pie, area, candle, tv)
LightweightChart.tsx Renders line, area, and candlestick charts using lightweight-charts (TradingView). Dark theme, gradient fills, crosshair, percentage change badges
TradingViewWidget.tsx Embeds TradingView Advanced Chart widget via official script injection for real financial tickers (e.g. TVC:GOLD, BITSTAMP:BTCUSD)

Chart type routing:

Type Renderer Use case
tv TradingView Widget Live financial tickers (stocks, crypto, forex, commodities)
line, area Lightweight Charts Custom time-series data
candle Lightweight Charts Custom OHLC candlestick data
bar, pie Recharts Category comparisons, proportions

features/command-palette/

Cmd+K command palette.

File Purpose
CommandPalette.tsx Fuzzy-search command list
commands.ts Command definitions (new session, reset, theme, TTS, etc.)

features/connect/

File Purpose
ConnectDialog.tsx Initial gateway connection dialog. Uses official gateway URL + serverSideAuth metadata from /api/connect-defaults to decide whether the token field is needed

features/activity/

File Purpose
AgentLog.tsx Scrolling agent activity log (tool calls, lifecycle events)
EventLog.tsx Raw gateway event stream display

features/dashboard/

File Purpose
TokenUsage.tsx Token usage and cost display
MemoryList.tsx Memory listing component
useLimits.ts Claude Code / Codex rate limit polling

features/memory/

File Purpose
MemoryEditor.tsx Inline memory editing
MemoryItem.tsx Individual memory display with edit/delete
AddMemoryDialog.tsx Dialog for adding new memories
ConfirmDeleteDialog.tsx Delete confirmation
useMemories.ts Memory CRUD operations

Shared Components

Path Purpose
components/TopBar.tsx Header with agent log, token data, event indicators
components/StatusBar.tsx Footer with connection state, session count, sparkline, context meter
components/ResizablePanels.tsx Draggable split layout (chat left, panels right)
components/ContextMeter.tsx Visual context window usage bar
components/ConfirmDialog.tsx Reusable confirmation modal
components/ErrorBoundary.tsx Top-level error boundary
components/PanelErrorBoundary.tsx Per-panel error boundary (isolates failures)
components/NerveLogo.tsx SVG logo component
components/skeletons/ Loading skeleton components (Message, Session, Memory)
components/ui/ Primitives: button, card, dialog, input, switch, scroll-area, collapsible, AnimatedNumber, InlineSelect

Hooks

Hook File Purpose
useWebSocket hooks/useWebSocket.ts Core WebSocket management — connect, RPC, auto-reconnect with exponential backoff
useConnectionManager hooks/useConnectionManager.ts Auto-connect logic, official gateway resolution, serverSideAuth gating, and credential persistence in localStorage
useDashboardData hooks/useDashboardData.ts Fetches memories and token data via REST + SSE
useServerEvents hooks/useServerEvents.ts SSE client for /api/events
useInputHistory hooks/useInputHistory.ts Up/down arrow input history
useTabCompletion hooks/useTabCompletion.ts Tab completion for slash commands
useKeyboardShortcuts hooks/useKeyboardShortcuts.ts Global keyboard shortcut registration
useGitInfo hooks/useGitInfo.ts Git branch/status display

Libraries

File Purpose
lib/constants.ts App constants: context window limits (with dynamic getContextLimit() fallback), wake/stop/cancel phrase builders, attachment limits
lib/themes.ts Theme definitions and CSS variable application
lib/fonts.ts Font configuration
lib/formatting.ts Message formatting utilities
lib/sanitize.ts HTML sanitization via DOMPurify
lib/highlight.ts Syntax highlighting configuration
lib/utils.ts cn() classname merge utility (clsx + tailwind-merge)
lib/progress-colors.ts Color scales for progress indicators
lib/text/isStructuredMarkdown.ts Detects structured markdown for rendering decisions

Backend Structure

Built with Hono (lightweight web framework), TypeScript, running on Node.js ≥22.

Entry Point

File Purpose
server/index.ts Starts HTTP + HTTPS servers, sets up WebSocket proxy, file watchers, graceful shutdown
server/app.ts Hono app definition — middleware stack, route mounting, static file serving with SPA fallback

Middleware Stack

Applied in order in app.ts:

Middleware File Purpose
Error handler middleware/error-handler.ts Catches unhandled errors, returns consistent JSON. Shows stack in dev
Logger Hono built-in Request logging
CORS Hono built-in + custom Whitelist of localhost origins + ALLOWED_ORIGINS env var. Validates via URL constructor. Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Security headers middleware/security-headers.ts Standard security headers (CSP, X-Frame-Options, etc.)
Body limit Hono built-in Configurable max body size (from config.limits.maxBodyBytes)
Auth middleware/auth.ts When NERVE_AUTH=true, requires a valid signed session cookie on /api/* routes (except auth endpoints and health). WebSocket upgrades checked separately in ws-proxy.ts
Compression Hono built-in gzip/brotli on all routes except SSE (/api/events)
Cache headers middleware/cache-headers.ts Hashed assets → immutable, API → no-cache, non-hashed static → must-revalidate
Rate limiting middleware/rate-limit.ts Per-IP sliding window. Separate limits for general API vs TTS/transcribe. Client ID from socket or custom header

API Routes

Route File Methods Purpose
/health routes/health.ts GET Health check with gateway connectivity probe
/api/auth/status routes/auth.ts GET Check whether auth is enabled and current session validity
/api/auth/login routes/auth.ts POST Authenticate with password, set signed session cookie
/api/auth/logout routes/auth.ts POST Clear session cookie
/api/connect-defaults routes/connect-defaults.ts GET Returns the official gateway WS URL plus authEnabled / serverSideAuth metadata. token is always null; trusted flows use server-side token injection
/api/events routes/events.ts GET, POST SSE stream for real-time push (memory.changed, tokens.updated, status.changed, ping). POST for test events
/api/tts routes/tts.ts POST Text-to-speech with provider auto-selection (OpenAI → Replicate → Edge). LRU cache with TTL
/api/tts/config routes/tts.ts GET, PUT TTS voice configuration per provider (read / partial update)
/api/transcribe routes/transcribe.ts POST Audio transcription via OpenAI Whisper or local whisper.cpp (STT_PROVIDER). Multipart file upload, MIME validation
/api/transcribe/config routes/transcribe.ts GET, PUT STT runtime config (provider/model/language), model readiness/download status, hot-reload updates
/api/language routes/transcribe.ts GET, PUT Language preference + Edge voice gender management (NERVE_LANGUAGE, EDGE_VOICE_GENDER)
/api/language/support routes/transcribe.ts GET Full provider × language compatibility matrix + current local model multilingual state
/api/voice-phrases routes/voice-phrases.ts GET Merged recognition phrase set (selected language + English fallback)
/api/voice-phrases/status routes/voice-phrases.ts GET Per-language custom phrase configuration status
/api/voice-phrases/:lang routes/voice-phrases.ts GET, PUT Read/save language-specific stop/cancel/wake phrase overrides
/api/agentlog routes/agent-log.ts GET, POST Agent activity log persistence. Zod-validated entries. Mutex-protected file I/O
/api/tokens routes/tokens.ts GET Token usage statistics — scans session transcripts, persists high water mark
/api/memories routes/memories.ts GET, POST, DELETE Agent-scoped memory management — reads MEMORY.md + daily files, stores/deletes via gateway tool invocation
/api/memories/section routes/memories.ts GET, PUT Read/replace a specific memory section by title, scoped via agentId
/api/gateway/models routes/gateway.ts GET Config-backed model catalog from the active OpenClaw config. Returns { models, error, source: "config" }
/api/gateway/session-info routes/gateway.ts GET Current session model/thinking level
/api/gateway/session-patch routes/gateway.ts POST HTTP fallback for model changes. Thinking changes belong on WS sessions.patch
/api/server-info routes/server-info.ts GET Server time, gateway uptime, agent name
/api/version routes/version.ts GET Package version from package.json
/api/version/check routes/version-check.ts GET Check whether a newer published version is available
/api/channels routes/channels.ts GET List configured messaging channels from OpenClaw config
/api/gateway/restart routes/gateway.ts POST Restart the OpenClaw gateway service and verify readiness
/api/sessions/hidden routes/sessions.ts GET List hidden cron-like sessions from stored session metadata
/api/sessions/:id/model routes/sessions.ts GET Read the actual model used by a session from its transcript
/api/workspace routes/workspace.ts GET List allowlisted workspace files for the selected agent workspace
/api/workspace/:key routes/workspace.ts GET, PUT Read/write allowlisted workspace files (soul, tools, identity, user, agents, heartbeat) via agentId
/api/crons routes/crons.ts GET, POST, PATCH, DELETE Cron job CRUD via gateway tool invocation
/api/crons/:id/toggle routes/crons.ts POST Toggle cron enabled/disabled
/api/crons/:id/run routes/crons.ts POST Run cron job immediately
/api/crons/:id/runs routes/crons.ts GET Cron run history
/api/skills routes/skills.ts GET List skills for the selected agent workspace via a scoped OpenClaw config
/api/keys routes/api-keys.ts GET, PUT Read API-key presence and persist updated key values to .env
/api/files routes/files.ts GET Serve local image files (MIME-type restricted, directory traversal blocked)
/api/files/tree routes/file-browser.ts GET Agent-scoped workspace directory tree (excludes node_modules, .git, etc.)
/api/files/read routes/file-browser.ts GET Read scoped file contents with mtime for conflict detection
/api/files/write routes/file-browser.ts PUT Write scoped file contents with optimistic concurrency (409 on conflict)
/api/files/rename routes/file-browser.ts POST Rename a file or directory within the selected workspace
/api/files/move routes/file-browser.ts POST Move a file or directory within the selected workspace
/api/files/trash routes/file-browser.ts POST Trash a file or directory, or permanently delete when using FILE_BROWSER_ROOT
/api/files/restore routes/file-browser.ts POST Restore a trashed file or directory
/api/files/raw routes/file-browser.ts GET Serve scoped image previews from the selected workspace
/api/claude-code-limits routes/claude-code-limits.ts GET Claude Code rate limits via PTY + CLI parsing
/api/codex-limits routes/codex-limits.ts GET Codex rate limits via OpenAI API with local file fallback
/api/kanban/tasks routes/kanban.ts GET, POST Task CRUD -- list (with filters/pagination) and create
/api/kanban/tasks/:id routes/kanban.ts GET, PATCH, DELETE Get, update (CAS-versioned), and delete tasks
/api/kanban/tasks/:id/reorder routes/kanban.ts POST Reorder/move tasks across columns
/api/kanban/tasks/:id/execute routes/kanban.ts POST Spawn agent session for task
/api/kanban/tasks/:id/complete routes/kanban.ts POST Complete a running task (auto-called by poller)
/api/kanban/tasks/:id/approve routes/kanban.ts POST Approve task in review -> done
/api/kanban/tasks/:id/reject routes/kanban.ts POST Reject task in review -> todo
/api/kanban/tasks/:id/abort routes/kanban.ts POST Abort running task -> todo
/api/kanban/proposals routes/kanban.ts GET, POST List and create proposals
/api/kanban/proposals/:id/approve routes/kanban.ts POST Approve pending proposal
/api/kanban/proposals/:id/reject routes/kanban.ts POST Reject pending proposal
/api/kanban/config routes/kanban.ts GET, PUT Board configuration

Server Libraries

File Purpose
lib/config.ts Centralized configuration from env vars — ports, keys, paths, limits, auth settings. Validated at startup
lib/session.ts Session token creation/verification (HMAC-SHA256), password hashing (scrypt), cookie parsing for WS upgrade requests
lib/ws-proxy.ts WebSocket proxy — client→gateway with session cookie auth on upgrade and Ed25519 device identity injection
lib/device-identity.ts Ed25519 keypair generation/persistence (~/.nerve/device-identity.json). Builds signed connect blocks for gateway auth
lib/gateway-client.ts HTTP client for gateway tool invocation API (/tools/invoke)
lib/file-watcher.ts Discovers agent workspaces, watches each MEMORY.md and memory/ directory, and optionally watches full workspaces recursively. Broadcasts agent-tagged memory.changed / file.changed SSE events
lib/file-utils.ts File browser utilities — path validation, directory exclusions, binary file detection
lib/files.ts Async file helpers (readJSON, writeJSON, readText)
lib/mutex.ts Async mutex for serializing file read-modify-write. Includes keyed mutex variant
lib/env-file.ts Mutex-protected .env key upserts for hot-reload settings writes (writeEnvKey)
lib/language.ts Language/provider compatibility helpers (isLanguageSupported, fallback metadata)
lib/voice-phrases.ts Runtime per-language phrase storage/merge (custom overrides + English fallback)
lib/cached-fetch.ts Generic TTL cache with in-flight request deduplication
lib/usage-tracker.ts Persistent token usage high water mark tracking
lib/tts-config.ts TTS voice configuration file management
lib/openclaw-bin.ts Resolves openclaw binary path (env → sibling of node → common paths → PATH)

Services

File Purpose
services/openai-tts.ts OpenAI TTS API client (gpt-4o-mini-tts, tts-1, tts-1-hd)
services/replicate-tts.ts Replicate API client for hosted TTS models (Qwen3-TTS). WAV→MP3 via ffmpeg
services/edge-tts.ts Microsoft Edge Read-Aloud TTS via WebSocket protocol. Free, zero-config. Includes Sec-MS-GEC token generation
services/tts-cache.ts LRU in-memory TTS cache with TTL expiry (100 MB budget)
services/openai-whisper.ts OpenAI Whisper transcription client
services/whisper-local.ts Local whisper.cpp STT via @fugood/whisper.node. Singleton model context, auto-download from HuggingFace, GPU detection
services/claude-usage.ts Claude Code CLI usage/limits parser via node-pty

Updater (server/lib/updater/)

Self-update system invoked via npm run update (entrypoint: bin/nerve-update.tsbin-dist/bin/nerve-update.js).

File Purpose
orchestrator.ts State machine: lock → preflight → resolve → snapshot → update → build → restart → health → rollback
preflight.ts Validates git, Node.js, npm versions and git repo state
release-resolver.ts Finds latest semver tag via git ls-remote --tags, falls back to local tags
snapshot.ts Saves current git ref, version, and .env backup to ~/.nerve/updater/
installer.ts git fetch + checkout --force, npm install, npm run build + build:server
service-manager.ts Auto-detects systemd or launchd, provides restart/status/logs
health.ts Polls /health and /api/version with exponential backoff (60s deadline)
rollback.ts Restores snapshot ref, clean rebuilds, restarts service
lock.ts PID-based exclusive lock file (wx flag) with stale detection
reporter.ts Formatted terminal output with stage progress, colors, and dry-run markers
types.ts Shared types, exit codes, UpdateError class

Compiled separately via config/tsconfig.bin.json to avoid changing the server's rootDir.


Data Flow

WebSocket Proxy

Browser WS → /ws?target=ws://gateway:18789/ws → ws-proxy.ts → OpenClaw Gateway
  1. Client connects to /ws endpoint on Nerve server
  2. When auth is enabled, the session cookie is verified on the HTTP upgrade request (rejects with 401 if invalid)
  3. Proxy validates target URL against WS_ALLOWED_HOSTS allowlist
  4. Proxy opens upstream WebSocket to the gateway
  5. On connect.challenge event, proxy intercepts the client's connect request and injects Ed25519 device identity (device block with signed nonce)
  6. If the gateway rejects the device (close code 1008), proxy retries without device identity (reduced scopes)
  7. After handshake, all messages are transparently forwarded bidirectionally
  8. Pending messages are buffered (capped at 100 messages / 1 MB) while upstream connects

Server-Sent Events (SSE)

Browser → GET /api/events → SSE stream (text/event-stream)

Events pushed by the server:

  • memory.changed — File watcher or memory API detects MEMORY.md / daily file changes, tagged with agentId
  • file.changed — File watcher detects a workspace file change, tagged with agentId
  • tokens.updated — Token usage data changed
  • status.changed — Gateway status changed
  • ping — Keep-alive every 30 seconds

SSE is excluded from compression middleware to avoid buffering.

REST API

REST endpoints serve two purposes:

  1. Proxy to gateway — Routes like /api/crons, /api/memories (POST/DELETE), /api/gateway/* invoke gateway tools via invokeGatewayTool()
  2. Local server data — Routes like /api/tokens, /api/agentlog, /api/server-info read from local files or process info

Gateway RPC (via WebSocket)

The frontend calls gateway methods via GatewayContext.rpc():

Method Purpose
status Get current agent model, thinking level
sessions.list List active sessions
sessions.delete Delete a session
sessions.reset Clear session context
sessions.patch Patch session metadata and settings, including rename/model/thinking flows the gateway supports
chat.send Send a message (with idempotency key)
chat.history Load message history
chat.abort Abort current generation
connect Initial handshake with auth/device identity

Event Types (Gateway → Client)

Event Payload Purpose
connect.challenge { nonce } Auth handshake initiation
chat { sessionKey, state, message?, content? } Chat state changes: started, delta, final, error, aborted
agent { sessionKey, state, stream, data? } Agent lifecycle: lifecycle.start/end/error, tool.start/result, assistant stream
cron { name } Cron job triggered
exec.approval.request Exec approval requested
exec.approval.resolved Exec approval granted
presence Presence updates

Kanban Subsystem

The kanban board provides task management with agent execution, drag-and-drop reordering, and a proposal system for agent-initiated changes.

Store Design

${NERVE_DATA_DIR:-~/.nerve}/kanban/tasks.json   -- single JSON file (tasks + proposals + config)
${NERVE_DATA_DIR:-~/.nerve}/kanban/audit.log    -- append-only audit log (JSONL)

All data lives in one JSON file (StoreData). Every mutation acquires an async mutex, reads the file, applies the change, and writes back atomically via temp-file rename. This guarantees consistency under concurrent requests without a database. On first startup, the store migrates legacy data from server-dist/data/kanban/ or server/data/kanban/ into the canonical runtime directory if needed.

File Purpose
server/lib/kanban-store.ts KanbanStore class -- mutex-protected CRUD, workflow transitions, CAS versioning, proposal management
server/routes/kanban.ts Hono routes, Zod validation, gateway session spawning, poll loop
server/lib/parseMarkers.ts Regex-based [kanban:create]/[kanban:update] marker extraction

The store schema is versioned (meta.schemaVersion). A migrate() function runs on every read to backfill new fields transparently.

State Machine

                    execute          complete (success)        approve
  backlog --------+                  +---- review ------------ done
                  v                  |       |
  todo --------- in-progress -------+       | reject
                  |                          v
                  | abort / error         todo (run cleared)
                  +---------------------> todo
Transition From To Trigger
Execute backlog, todo in-progress POST .../execute
Complete (success) in-progress review Poller or POST .../complete
Complete (error) in-progress todo Poller or POST .../complete with error
Approve review done POST .../approve
Reject review todo POST .../reject (clears run + result)
Abort in-progress todo POST .../abort

The cancelled status exists in the schema but has no automatic transitions -- tasks are moved there manually via PATCH.

CAS Versioning

Every task has a version field (starts at 1, incremented on every mutation). Mutating endpoints (PATCH, reorder, workflow actions) require the client to send the current version. If it doesn't match, the server returns 409 with the latest task so the client can retry:

{ "error": "version_conflict", "serverVersion": 5, "latest": { "..." } }

This prevents stale overwrites from concurrent editors (drag-and-drop, API clients, agent completions).

Agent Execution Flow

1. POST /api/kanban/tasks/:id/execute
   +-- withMutex(`kanban-execute:${id}`) prevents double-launch races
   +-- if task already in-progress: return 409 duplicate_execution
   +-- if task has an assignee root:
   |    +-- resolve assignee root -> agent:<assignee>:main
   |    +-- gatewayRpcCall('sessions.list', ...) confirms the parent root exists
   |    +-- store.executeTask(..., { sessionKey }) -> status = in-progress, run.status = running
   |    +-- launchKanbanFallbackSubagentViaRpc({ label, task, parentSessionKey, model?, thinking? })
   |         +-- gatewayRpcCall('sessions.create', { key: childSessionKey, parentSessionKey, label, model? })
   |         +-- gatewayRpcCall('sessions.send', { key: childSessionKey, message: task, thinking?, idempotencyKey })
   |         +-- if send fails after create: best-effort gatewayRpcCall('sessions.delete', { key: childSessionKey, deleteTranscript: true })
   |         +-- return correlationKey + childSessionKey + runId?
   |         +-- attach childSessionKey / runId immediately when available
   |         +-- start pollFallbackSessionCompletion(taskId, { correlationKey, parentSessionKey, childSessionKey?, expectedChildLabel, knownSessionKeysBefore, runId? })
   |
   +-- else if task is unassigned / operator:
   |    +-- on macOS: return 409 invalid_execution_target
   |    +-- otherwise use invokeGatewayTool('sessions_spawn', { task, mode:'run', label: runSessionKey, model?, thinking? })
   |    +-- attach childSessionKey / runId when available
   |    +-- start pollSessionCompletion(taskId, { correlationKey: runSessionKey, childSessionKey?, runId? })
   |
   +-- if an assigned parent root is missing: return 409 invalid_execution_target
   +-- on launch failure: store.completeRun(taskId, sessionKey, undefined, 'Spawn failed: ...')

2. pollSessionCompletion() / pollFallbackSessionCompletion()
   +-- sessions_spawn path polls gateway subagents by correlation key / childSessionKey / runId
   +-- assignee-root path polls gateway RPC sessions.list every 5s (max 720 attempts / 60 min)
   +-- assignee-root path prefers the known childSessionKey; otherwise it discovers the new child beneath the parent root and attaches it
   +-- if the child completes successfully:
       |   fetch child history via sessions.get / sessions_history
       |   parseKanbanMarkers(resultText) -> create proposals
       |   stripKanbanMarkers(resultText) -> clean result
       |   store.completeRun(taskId, sessionKey, cleanResult)
       +-- gatewayRpcCall('sessions.send', { key: parentSessionKey, message: completionReport })
   +-- if status=error/failed:
       |   store.completeRun(taskId, sessionKey, undefined, errorMsg)
       +-- gatewayRpcCall('sessions.send', { key: parentSessionKey, message: failureReport })
   +-- if task/run no longer matches the active session key:
       +-- stop polling as stale
   +-- otherwise:
       +-- schedule next poll

3. store.completeRun()
   |-- success -> run.status = done, task.status = review
   +-- error   -> run.status = error, task.status = todo

Assigned-root execution now uses real session primitives instead of synthetic marker-message spawn conventions. The model cascade is: execute request model -> task model -> board config defaultModel -> OpenClaw's configured default model. Thinking follows the same pattern with defaultThinking.

Marker Parsing

When agent output arrives (via poller or POST .../complete), it's scanned for kanban markers:

[kanban:create]{"title":"Fix login bug","priority":"high"}[/kanban:create]
[kanban:update]{"id":"abc","status":"done"}[/kanban:update]

Each valid marker creates a proposal in the store. The markers are then stripped from the result text before it's saved on the task. See Agent Markers for the full format.

Proposal System

Proposals let agents suggest task changes without directly modifying the board:

  1. Agent emits [kanban:create] or [kanban:update] markers in its output
  2. Backend parses markers -> creates proposals with status: "pending"
  3. Frontend polls proposals every 5 seconds -> shows inbox notification
  4. Operator approves or rejects each proposal

The proposalPolicy config controls behavior:

  • "confirm" (default) -- proposals stay pending until the operator acts
  • "auto" -- proposals are applied immediately on creation

Polling Architecture

What Who Interval Purpose
Task list Frontend 5s Sync board state across tabs/users
Proposals Frontend 5s Show new proposals in inbox
Gateway subagents Backend 5s Detect when agent runs complete

Backend polling for each running task is independent -- each executeTask call starts its own poll loop (capped at 720 attempts = 60 minutes). Stale runs are reconciled by reconcileStaleRuns().


Build System

Development

npm run dev          # Vite dev server (frontend) — port 3080
npm run dev:server   # tsx watch (backend) — port 3081

Vite proxies /api and /ws to the backend dev server.

Production

npm run prod         # Builds frontend + backend, then starts
# Equivalent to:
npm run build        # tsc -b && vite build → dist/
npm run build:server # tsc -p config/tsconfig.server.json → server-dist/
npm start            # node server-dist/index.js

Vite Configuration

  • Plugins: @vitejs/plugin-react, @tailwindcss/vite
  • Path alias: @/./src/
  • Manual chunks: react-vendor, markdown (react-markdown + highlight.js), ui-vendor (lucide-react), utils (clsx, tailwind-merge, dompurify)
  • HTTPS: Auto-enabled if certs/cert.pem and certs/key.pem exist

TypeScript Configuration

Project references with four configs:

  • config/tsconfig.app.json — Frontend (src/)
  • config/tsconfig.node.json — Vite/build tooling
  • config/tsconfig.server.json — Backend (server/) → compiled to server-dist/
  • config/tsconfig.scripts.json — Setup scripts
  • config/tsconfig.bin.json — CLI tools (bin/) → compiled to bin-dist/

Testing

Framework: Vitest with jsdom environment for React tests.

npm test              # Run all tests
npm run test:coverage # With V8 coverage

Test Files

48 test files, 692 tests. Key areas:

Area Files Coverage
Server lib config, device-identity, env-file, gateway-client, mutex, voice-language-coverage, ws-proxy Auth, config resolution, WS proxy relay, device identity signing
Server routes auth, events, files, gateway, health, sessions, skills API endpoints, error handling, gateway proxy
Server middleware auth, error-handler, rate-limit, security-headers Request pipeline
Chat operations sendMessage, streamEventHandler, loadHistory, mergeRecoveredTail Message lifecycle, streaming, history recovery
Client features extractCharts, extractImages, edit-blocks, MarkdownRenderer, ContextMeter, ErrorBoundary, sessionTree Rendering, parsing, UI components
Hooks useAuth, useInputHistory, useKeyboardShortcuts, useServerEvents, useTTS Client-side state and interaction
Voice/audio audio-feedback, useVoiceInput, voice-prefix Voice input, TTS, wake/stop phrase parsing
Utilities constants, formatting, sanitize, unreadSessions Shared logic

Configuration

  • Environment: jsdom (browser APIs mocked)
  • Setup: src/test/setup.ts
  • Exclusions: node_modules/, server-dist/ (avoids duplicate compiled test files)
  • Coverage: V8 provider, text + HTML + lcov reporters